<file_sep>import {Component, OnInit} from '@angular/core';
import {ApiInterfaceService} from '../providers/api-interface.service';
import {AngularFireDatabase} from 'angularfire2/database';
import {AuthService} from '../providers/auth.service';
import {Router} from '@angular/router';
import {User} from 'firebase';
import {CreateTemplateComponent} from '../create-template/create-template.component';

export enum SortingOptions {
    CREATIONDATE = 'Creation Date',
    LASTDOWNLOADEDDATE = 'Last Downloaded Date',
    NUMBEROFDOWNLOADS = 'Number of downloads',
    RATING = 'Rating'
}

@Component({
    selector: 'app-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

    user: User = null;
    searchTerm: String = null;
    hasSearched = false;
    searchedTemplateList: any = null;
    options: String[];
    selected: any;
    displayList: any[];

    constructor(
        private apiInterfaceService: ApiInterfaceService,
        private authService: AuthService,
        private db: AngularFireDatabase,
        private router: Router
    ) {
    }

    ngOnInit(): void {
        const component = this;
        component.authService.onAuthStateChanged(function (auth) {
            if (auth === null) { // If the user is logged out
                component.router.navigate(['login']);
            } else {
                component.user = component.authService.getAuth().currentUser;
            }
        });
        this.options = Object.values(SortingOptions);
    }

    doSearch() {
        const component = this;
        if (component.searchTerm) {
            component.hasSearched = true;
            component.searchedTemplateList = component.db.list('/template-directory',
                ref =>
                    ref.orderByChild(CreateTemplateComponent.encodeTag(component.searchTerm))
                        .equalTo(true)
            ).valueChanges().subscribe((data) => component.displayList = data);
        }
    }


    openTemplate(templateId) {
        const component = this;
        component.router.navigate(['download-template/' + templateId]);
    }

    createTemplate() {
        this.router.navigate(['create']);
    }

    tutorial() {
        this.router.navigate(['tutorial']);
    }

    myTemplates() {
        this.router.navigate(['my-templates']);
    }

    logout(): void {
        this.authService.logout(null);
    }

    sort(option) {
        const component = this;
        switch (option) {
            case SortingOptions.CREATIONDATE: {
                if (component.displayList != null) {
                    component.displayList.sort((b, a) => a.templateCreateDate - b.templateCreateDate);
                }
                break;
            }
            case SortingOptions.LASTDOWNLOADEDDATE: {
                if (component.displayList != null) {
                    component.displayList.sort((b, a) => a.templateLastDownloadDate - b.templateLastDownloadDate);
                }
                break;
            }
            case SortingOptions.NUMBEROFDOWNLOADS: {
                if (component.displayList != null) {
                    component.displayList.sort((b, a) => a.templateNumDownload - b.templateNumDownload);
                }
                break;
            }
            case SortingOptions.RATING: {
                if (component.displayList != null) {
                    component.displayList.sort((b, a) => a.averageRating - b.averageRating);
                }
                break;
            }
        }
    }
}
<file_sep>import {Injectable} from '@angular/core';
import * as firebase from 'firebase';
import {AngularFireDatabase} from 'angularfire2/database';
import {AngularFireModule} from 'angularfire2';

export class Upload {

    file: File;
    name: string;
    targetName: string;
    userUID: string;
    progress: number;

    constructor(file: File, targetName: string, userUID: string) {
        this.file = file;
        this.targetName = targetName;
        this.userUID = userUID;
    }
}

@Injectable()
export class UploadService {

    constructor() {
    }

    private basePath = '/uploads';

    pushUpload(upload: Upload, callback) {
        const storageRef = firebase.storage().ref();
        const uploadTask = storageRef.child(`${this.basePath}/users/${upload.userUID}/${upload.targetName}`).put(upload.file);

        uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
            (snapshot) => {
                // upload in progress
                const snapshotRef = snapshot as firebase.storage.UploadTaskSnapshot;
                const bytesTransferred = (snapshotRef).bytesTransferred;
                const totalBytes = (snapshotRef).totalBytes;
                upload.progress = (bytesTransferred / totalBytes) * 100;
            },
            (error) => {
                // upload failed
                console.log(error);
            },
            () => {
                // upload success
                upload.name = upload.file.name;
                uploadTask.snapshot.ref.getDownloadURL().then((url) => {
                    callback(url)
                });
            }
        );
    }
}
<file_sep>import {AuthService} from '../providers/auth.service';
import {ActivatedRoute, Router} from '@angular/router';
import {User} from 'firebase';
import {Component, NgZone, OnInit} from '@angular/core';
import {AngularFireDatabase, AngularFireObject} from 'angularfire2/database';
import {Observable} from 'rxjs';
import {ApiInterfaceService} from '../providers/api-interface.service';
import * as firebase from 'firebase';
import {Reference} from 'firebase/database';
import {MatDialog} from '@angular/material/dialog';
import {InputValidateDialogComponent} from '../input-validate-dialog/input-validate-dialog.component';


@Component({
    selector: 'download-template',
    templateUrl: './download-template.component.html',
    styleUrls: ['./download-template.component.css']
})
export class DownloadTemplateComponent implements OnInit {
    user: User = null;
    valueMap: Object = {};
    templateDirectoryInfoRef: AngularFireObject<any>;
    templateRenderInfoRef: AngularFireObject<any>;
    templateRatingsInfoRef: AngularFireObject<any>;
    templateRatingsInfoDatabaseRef: Reference;
    templateDirectoryInfoDatabaseRef: Reference;
    templateRenderInfo: Observable<any> = null;
    templateDirectoryInfo: Observable<any> = null;
    templateRatingsInfo: Observable<any> = null;
    allRatingsList: Observable<any[]> = null;
    ratingText: String = '';
    ratingVal: number = null;

    constructor(
        private authService: AuthService,
        private db: AngularFireDatabase,
        private ngZone: NgZone,
        private route: ActivatedRoute,
        private router: Router,
        private api: ApiInterfaceService,
        private dialog: MatDialog
    ) {
    }

    ngOnInit() {
        const component = this;
        component.authService.onAuthStateChanged(function (auth) {
            if (auth === null) { // If the user is logged out
                component.router.navigate(['login']);
            } else {
                component.user = component.authService.getAuth().currentUser;
                component.route.params.subscribe(params => {
                    component.ngZone.run(() => { // Need to do this using NgZone since we're calling a third party API
                        component.templateDirectoryInfoRef = component.db.object('template-directory/' + params.id);
                        component.templateRenderInfoRef = component.db.object('template-render-info/' + params.id);
                        component.templateRatingsInfoRef = component.db.object('template-ratings/' + params.id);
                        component.templateRatingsInfoDatabaseRef = firebase.database().ref(
                            'template-ratings/' + params.id + '/' + component.user.uid);
                        component.templateDirectoryInfoDatabaseRef = firebase.database().ref('template-directory/' + params.id);
                        component.templateRatingsInfo = component.templateRatingsInfoRef.valueChanges();
                        component.templateDirectoryInfo = component.templateDirectoryInfoRef.valueChanges();
                        component.templateRenderInfo = component.templateRenderInfoRef.valueChanges();
                        component.templateDirectoryInfoDatabaseRef = firebase.database().ref('template-directory/' + params.id);
                        component.templateRenderInfo.subscribe((response) => {
                            if (response == null) {
                                component.router.navigate(['home']);
                            }
                        });
                        component.allRatingsList = component.db.list('template-ratings/' + params.id).valueChanges();
                    });
                });
            }
        });
    }

    goHome() {
        this.router.navigate(['']);
    }

    logout(): void {
        this.authService.logout(null);
    }

    storeRating() {
        if (this.validateRating()) {
            const component = this;
            component.templateRatingsInfoDatabaseRef.once('value').then(snapshot => {
                const old_rating = snapshot.child('ratingValue').val(); // value of previous rating
                const currentRatingVal = component.ratingVal;
                component.templateRatingsInfoDatabaseRef.set({
                    'ratingValue': component.ratingVal,
                    'ratingText': component.ratingText,
                    'ratingUserDisplayName': component.user.displayName
                });
                component.templateDirectoryInfoDatabaseRef.child('/numberRatings').transaction(function (numberRatings) {
                    if (old_rating != null) {
                        return numberRatings;
                    } else {
                        return numberRatings + 1;
                    }
                }).then(function (numberRatingsUpdated) {
                    component.templateDirectoryInfoDatabaseRef.child('/ratingSum').transaction(function (ratingSum) {
                        if (old_rating != null) {
                            if (ratingSum !== 0) {
                                return ratingSum - old_rating + currentRatingVal;
                            } else {
                                return currentRatingVal;
                            }
                        } else {
                            if (ratingSum !== 0) {
                                return ratingSum + currentRatingVal;
                            } else {
                                return currentRatingVal;
                            }
                        }
                    }).then(function (ratingSumForAverage) {
                        if (ratingSumForAverage !== null && numberRatingsUpdated.snapshot.val() !== null) {
                            component.templateDirectoryInfoDatabaseRef.child('averageRating').set(
                                ratingSumForAverage.snapshot.val() / numberRatingsUpdated.snapshot.val());
                        }
                    });
                });
            });
        }
    }

    saveRatingVal(star_number) {
        this.ratingVal = star_number;
    }

    validateRating() {
        if (this.ratingVal) {
            return true;
        } else {
            this.dialog.open(InputValidateDialogComponent, {
                data: {message: 'Please enter a star rating for this template.'}
            });
            return false;
        }
    }

    downloadTemplate() {
        const component = this;
        component.validateEnteredVariables();
        component.templateRenderInfoRef.snapshotChanges().subscribe(data => {
            const payload_val = data.payload.val();
            const fileEndings = payload_val.fileEndings;
            for (let i = 0; i < payload_val.fileEndings.length; i++) {
                fileEndings[i] = fileEndings[i].name;
            }
            const request = encodeURIComponent(JSON.stringify({
                'variables': component.valueMap,
                'fileEndings': fileEndings,
                'url': encodeURI(payload_val.templateArchiveUrl)
            }));

            component.api.getZipFile(request, function (downloadedData) {
                const linkElement = document.createElement('a');
                const url = window.URL.createObjectURL(downloadedData);
                linkElement.setAttribute('href', url);
                linkElement.setAttribute('download', 'project');
                const clickEvent = new MouseEvent('click', {
                    'view': window,
                    'bubbles': true,
                    'cancelable': false
                });
                linkElement.dispatchEvent(clickEvent);
                component.templateDirectoryInfoDatabaseRef.child('/templateNumDownload').transaction(function (snapshot) {
                    return snapshot + 1;
                });
                component.templateDirectoryInfoDatabaseRef.child('/templateLastDownloadDate').set(Date.now());
            });

        });
    }

    createTemplate() {
        this.router.navigate(['create']);
    }

    myTemplates() {
        this.router.navigate(['my-templates']);
    }

    validateEnteredVariables() {
        const component = this;
        Object.keys(component.valueMap).forEach(function (variable) {
            if (!component.valueMap[variable]) {
                component.valueMap[variable] = '';
            }
        });
    }
}
<file_sep>// Note: It's okay to commit this to version control, since Firebase handles security
// This should be swapped out for a different config during final deployment
export const config = {
    apiKey: "<KEY>",
    authDomain: "templyte.firebaseapp.com",
    databaseURL: "https://templyte.firebaseio.com",
    projectId: "templyte",
    storageBucket: "templyte.appspot.com",
    messagingSenderId: "993257440730"
};
<file_sep>import {Injectable} from '@angular/core';
import {AngularFireAuth} from 'angularfire2/auth';
import {auth} from 'firebase';

@Injectable({
    providedIn: 'root'
})
export class AuthService {

    constructor(public afAuth: AngularFireAuth) {
    }

    login(callback): void {
        this.afAuth.auth.signInWithPopup(new auth.GithubAuthProvider()).then(function (res) {
            if (callback) {
                callback(res);
            }
        });
    }

    logout(callback): void {
        this.afAuth.auth.signOut().then(function (res) {
            if (callback) {
                callback(res);
            }
        });
    }

    onAuthStateChanged(callback) {
        this.afAuth.auth.onAuthStateChanged(function (auth) {
            callback(auth);
        });
    }

    getAuth() {
        return this.afAuth.auth;
    }
}
<file_sep>import {Component, Inject, OnInit} from '@angular/core';
import {MatDialogRef} from '@angular/material';

@Component({
    selector: 'delete-confirm-dialog',
    templateUrl: './delete-confirm-dialog.component.html',
    styleUrls: ['./delete-confirm-dialog.component.css']
})
export class DeleteConfirmDialogComponent implements OnInit {

    constructor(public dialogRef: MatDialogRef<DeleteConfirmDialogComponent>) {}

    ngOnInit() {}

    onYesClick() {
        this.dialogRef.close(true);
    }

    onNoClick() {
        this.dialogRef.close(false);
    }
}
<file_sep>import {AuthService} from '../providers/auth.service';
import {Router} from '@angular/router';
import {User} from 'firebase';

import {Component, OnInit} from '@angular/core';

import {UploadService, Upload} from '../upload/upload.service';
import {AngularFireDatabase} from 'angularfire2/database';
import {MatDialog} from '@angular/material/dialog';
import {InputValidateDialogComponent} from '../input-validate-dialog/input-validate-dialog.component';
import {UploadSuccessDialogComponent} from '../upload-success-dialog/upload-success-dialog.component';


@Component({
    selector: 'create-template',
    templateUrl: './create-template.component.html',
    styleUrls: ['./create-template.component.css']
})
export class CreateTemplateComponent implements OnInit {
    user: User = null;
    selectedFiles: FileList;
    currentUpload: Upload;
    templateName: String = null;
    templateDescription: String = null;
    variableArray: Array<any> = [];
    tagArray: Array<any> = [];
    fileEndingsArray: Array<any> = [];
    isUploading: boolean;

    constructor(
        private authService: AuthService,
        private db: AngularFireDatabase,
        private router: Router,
        private upSvc: UploadService,
        private dialog: MatDialog,
    ) {
    }


    static encodeTag(tag) {
        return '__TAG__' + tag.replace(/\s*/g, '').replace(/[^\w\s\d]/gi, '').toLowerCase();
    }

    ngOnInit() {
        const component = this;
        component.authService.onAuthStateChanged(function (auth) {
            if (auth === null) { // If the user is logged out
                component.router.navigate(['login']);
            } else {
                component.user = component.authService.getAuth().currentUser;
            }
        });
    }

    addVariableValue() {
        this.variableArray.push({});
    }

    deleteVariableValue(index) {
        this.variableArray.splice(index, 1);
    }

    deleteTagValue(index) {
        this.tagArray.splice(index, 1);
    }

    addTagValue() {
        this.tagArray.push({});
    }

    deleteFileEndingsValue(index) {
        this.fileEndingsArray.splice(index, 1);
    }

    addFileEndingsValue() {
        this.fileEndingsArray.push({});
    }

    goHome() {
        this.router.navigate(['']);
    }

    logout(): void {
        this.authService.logout(null);
    }


    detectFiles(event) {
        this.selectedFiles = event.target.files;
    }

    uploadTemplate() {
        const component = this;
        if (component.validateInput() && !component.isUploading) {
            component.isUploading = true;
            const renderInfoObject = component.db.list('template-render-info');
            renderInfoObject.push({
                'variables': component.variableArray,
                'fileEndings': component.fileEndingsArray,
                'authorUID': component.user.uid
            }).then((renderInfoResult) => {
                const targetKey = renderInfoResult.key;
                const directoryObject = component.db.object('template-directory/' + targetKey);
                const templateDirectoryData = {
                    'uid': targetKey,
                    'templateName': component.templateName,
                    'templateDescription': component.templateDescription,
                    'tags': component.tagArray,
                    'authorName': component.user.displayName,
                    'authorUID': component.user.uid,
                    'authorPhotoUrl': component.user.photoURL,
                    'averageRating': 0,
                    'ratingSum': 0,
                    'numberRatings': 0,
                    'templateNumDownload': 0,
                    'templateLastDownloadDate': null,
                    'templateCreateDate': Date.now()
                };
                for (let i = 0; i < component.tagArray.length; i++) {
                    templateDirectoryData[CreateTemplateComponent.encodeTag(component.tagArray[i].name)] = true;
                }
                templateDirectoryData[CreateTemplateComponent.encodeTag(component.templateName)] = true;
                const nameSplit = component.templateName.split(/\s+/);
                for (let i = 0; i < nameSplit.length; i++) {
                    templateDirectoryData[CreateTemplateComponent.encodeTag(nameSplit[i])] = true;
                }
                directoryObject.set(templateDirectoryData);
                component.uploadFile(targetKey + '.zip', function (templateUrl) {
                    component.db.object('template-render-info/' + targetKey + '/templateArchiveUrl')
                        .set(templateUrl).then(() => {
                        const dialogRef = component.dialog.open(UploadSuccessDialogComponent);
                        dialogRef.afterClosed().subscribe(() => {
                            component.router.navigate(['my-templates']);
                        });
                    });
                });
            });
        }
    }

    validateInput() {
        let returnVal: Boolean = true;
        if (!this.templateName) { // will evaluate to true if templateName is an empty string, for more info google 'typescript truthiness'
            returnVal = false;
            this.dialog.open(InputValidateDialogComponent, {
                data: {message: 'Please enter a name for your template.'}
            });
        }
        for (let i = 0; i < this.variableArray.length - 1; i++) {
            for (let j = i + 1; j < this.variableArray.length; j++) {
                if (this.variableArray[i].name === this.variableArray[j].name) {
                    returnVal = false;
                    this.dialog.open(InputValidateDialogComponent, {
                        data: {message: 'Please do not enter duplicate variables'}
                    });
                }
            }
        }
        const invalidCharactersArray: Array<String> = [' ', '!', '#', '$', '%', '&', '\\',
            '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[',
            `/`, ']', '^', '_', '`', '{', '|', '}', '~'];
        for (let i = 0; i < this.variableArray.length; i++) {
            for (const char in invalidCharactersArray) {
                if (this.variableArray[i].name.includes(invalidCharactersArray[char])) {
                    returnVal = false;
                    this.dialog.open(InputValidateDialogComponent, {
                        data: {
                            message: `The variable name "` + this.variableArray[i].name +
                                `" contains an invalid character: ` + invalidCharactersArray[char] +
                                '\n Please do not include spaces or special characters in your variable names.'
                        }
                    });
                    break; // once you find one invalid character in a variable name, the user doesn't need to be told if there are more
                }
            }
        }
        for (let i = 0; i < this.tagArray.length - 1; i++) {
            for (let j = i + 1; j < this.tagArray.length; j++) {
                if (this.tagArray[i].name === this.tagArray[j].name) {
                    returnVal = false;
                    this.dialog.open(InputValidateDialogComponent, {
                        data: {message: 'Please do not enter duplicate tags'}
                    });
                }
            }
        }

        for (let i = 0; i < this.fileEndingsArray.length - 1; i++) {
            for (let j = i + 1; j < this.fileEndingsArray.length; j++) {
                if (this.fileEndingsArray[i].name === this.fileEndingsArray[j].name) {
                    returnVal = false;
                    this.dialog.open(InputValidateDialogComponent, {
                        data: {message: 'Please do not enter duplicate file endings'}
                    });
                }
            }
        }

        if (!/zip/i.test(this.selectedFiles[0].type)) {
            returnVal = false;
            this.dialog.open(InputValidateDialogComponent, {
                data: {message: 'Please make sure that the file you are uploading is a .zip file'}
            });
        }
        return returnVal;
    }

    uploadFile(targetName: string, callback) {
        const component = this;
        const file = component.selectedFiles.item(0);
        component.currentUpload = new Upload(file, targetName, component.user.uid);
        component.upSvc.pushUpload(component.currentUpload, callback);
    }

    tutorial() {
        this.router.navigate(['tutorial']);
    }

    myTemplates() {
        this.router.navigate(['my-templates']);
    }
}
<file_sep>import {Component} from '@angular/core';

@Component({
    selector: 'rating-success-dialog',
    templateUrl: './rating-success-dialog.component.html',
    styleUrls: ['./rating-success-dialog.component.css']
})
export class RatingSuccessDialogComponent {}
<file_sep>import {Component, Inject, OnInit} from '@angular/core';
import { MAT_DIALOG_DATA,  } from '@angular/material';

@Component({
    selector: 'input-validate-dialog',
    templateUrl: './input-validate-dialog.component.html',
    styleUrls: ['./input-validate-dialog.component.css']
})
export class InputValidateDialogComponent implements OnInit {
    constructor( @Inject(MAT_DIALOG_DATA) public data) {}

    ngOnInit() {
    }
}
<file_sep>import {Component, NgZone, OnInit} from '@angular/core';
import {AuthService} from '../providers/auth.service';
import {Router} from '@angular/router';

@Component({
    selector: 'app-login',
    templateUrl: './login.component.html',
    styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

    constructor(
        private authService: AuthService,
        private ngZone: NgZone,
        private router: Router) {
    }

    ngOnInit() {
        const component = this;
        component.authService.onAuthStateChanged(function (auth) {
            if (auth !== null) {
                component.goHome();
            }
        });
    }

    login(): void {
        const component = this;
        component.authService.login(function () {
            component.goHome();
        });
    }

    tutorial() {
        this.router.navigate(['tutorial']);
    }

    goHome() {
        const component = this;
        component.ngZone.run(() => { // Need to do this using NgZone since we're calling a third party API
            component.router.navigate(['']);
        });
    }
}
<file_sep>import {Component} from '@angular/core';

@Component({
    selector: 'upload-success-dialog',
    templateUrl: './upload-success-dialog.component.html',
    styleUrls: ['./upload-success-dialog.component.css']
})
export class UploadSuccessDialogComponent {}
<file_sep>import {Component, NgZone, OnInit} from '@angular/core';
import {User} from 'firebase';
import {AuthService} from '../providers/auth.service';
import {AngularFireDatabase} from 'angularfire2/database';
import {Router} from '@angular/router';
import {AngularFireStorage} from 'angularfire2/storage';
import * as firebase from 'firebase';
import {MatDialog} from '@angular/material';
import {DeleteConfirmDialogComponent} from '../delete-confirm-dialog/delete-confirm-dialog.component';

@Component({
    selector: 'app-my-templates',
    templateUrl: './my-templates.component.html',
    styleUrls: ['./my-templates.component.css']
})
export class MyTemplatesComponent implements OnInit {

    user: User = null;
    templateList: any = null;

    constructor(
        private authService: AuthService,
        private db: AngularFireDatabase,
        private ngZone: NgZone,
        private router: Router,
        private storage: AngularFireStorage,
        private dialog: MatDialog
    ) {
    }

    ngOnInit(): void {
        const component = this;

        component.authService.onAuthStateChanged(function (auth) {
            if (auth === null) { // If the user is logged out
                component.router.navigate(['login']);
            } else {
                component.user = component.authService.getAuth().currentUser;

                component.ngZone.run(() => {
                    component.createTemplateList();
                });
            }
        });

    }

    private createTemplateList() {
        const component = this;
        component.templateList = component.db.list('/template-directory',
            ref =>
                ref.orderByChild('authorUID')
                    .equalTo(component.user.uid)
        ).valueChanges();
    }

    createTemplate() {
        this.router.navigate(['create']);
    }

    deleteTemplate(templateUID) {
        const component = this;
        const dialogRef = component.dialog.open(DeleteConfirmDialogComponent);
        dialogRef.afterClosed().subscribe( (result) => {
            if (result) {
                component.db.object('template-directory/' + templateUID).remove().then(() => {
                    const renderInfoRef = component.db.object('template-render-info/' + templateUID);
                    renderInfoRef.valueChanges().subscribe((renderInfoValueRes: any) => {
                        if (renderInfoValueRes) {
                            const archiveURL = renderInfoValueRes.templateArchiveUrl;
                            renderInfoRef.remove().then(() => {
                                firebase.storage().refFromURL(archiveURL).delete();
                            });
                        }
                    });
                });
            }
        });
    }

    openTemplate(templateId) {
        const component = this;
        component.router.navigate(['download-template/' + templateId]);
    }

    logout(): void {
        this.authService.logout(null);
    }

    tutorial() {
        this.router.navigate(['tutorial']);
    }

    goHome(): void {
        this.router.navigate(['']);
    }
}
<file_sep>import {AuthService} from '../providers/auth.service';
import {Router} from '@angular/router';
import {User} from 'firebase';
import {ApiInterfaceService} from '../providers/api-interface.service';
import {Component, OnInit} from '@angular/core';

@Component({
    selector: 'app-tutorial',
    templateUrl: './tutorial.component.html',
    styleUrls: ['./tutorial.component.css']
})

export class TutorialComponent implements OnInit {
    user: User = null;

    constructor(
        private authService: AuthService,
        private router: Router
    ) {
    }

    ngOnInit(): void {

    }

    createTemplate() {
        this.router.navigate(['create']);
    }

    myTemplates() {
        this.router.navigate(['my-templates']);
    }

    goHome() {
        this.router.navigate(['']);
    }

    logout(): void {
        const component = this;
        component.authService.onAuthStateChanged(function (auth) {
            if (auth === null) { // If the user is logged out
                component.router.navigate(['login']);
            } else {
                component.user = component.authService.getAuth().currentUser;
                this.authService.logout(null);
            }
        });
    }
}
<file_sep>import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {config} from './config/firebase-config';
import {AppComponent} from './app.component';
import {RouterModule, Routes} from '@angular/router';
import {LoginComponent} from './login/login.component';
import {HomeComponent} from './home/home.component';
import {TutorialComponent} from './tutorial/tutorial.component';
import {ApiInterfaceService} from './providers/api-interface.service';
import {HttpClientModule} from '@angular/common/http';
import {AngularFireModule} from 'angularfire2';
import {AngularFireDatabase} from 'angularfire2/database';
import {AngularFireAuth} from 'angularfire2/auth';
import {CreateTemplateComponent} from './create-template/create-template.component';
import {AngularFireStorage} from 'angularfire2/storage';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatButtonModule, MatCardModule, MatDialogModule, MatInputModule} from '@angular/material';
import {MatToolbarModule} from '@angular/material/toolbar';
import {FormsModule} from '@angular/forms';
import {UploadService} from './upload/upload.service';
import {InputValidateDialogComponent} from './input-validate-dialog/input-validate-dialog.component';
import {UploadSuccessDialogComponent} from './upload-success-dialog/upload-success-dialog.component';
import {MyTemplatesComponent} from './my-templates/my-templates.component';
import {MatChipsModule} from '@angular/material/chips';
import {MatDividerModule} from '@angular/material/divider';
import {MatSelectModule} from '@angular/material/select';
import {DownloadTemplateComponent} from './download-template/download-template.component';
import {DeleteConfirmDialogComponent} from './delete-confirm-dialog/delete-confirm-dialog.component';
import {RatingSuccessDialogComponent} from './rating-success-dialog/rating-success-dialog.component';

const routes: Routes = [
    {path: '', component: HomeComponent, pathMatch: 'full'},
    {path: 'login', component: LoginComponent, pathMatch: 'full'},
    {path: 'tutorial', component: TutorialComponent, pathMatch: 'full'},
    {path: 'create', component: CreateTemplateComponent, pathMatch: 'full'},
    {path: 'download-template/:id', component: DownloadTemplateComponent, pathMatch: 'full'},
    {path: 'my-templates', component: MyTemplatesComponent, pathMatch: 'full'},
    {path: '**', redirectTo: '', pathMatch: 'full'} // Redirect everything else to the home page
];

@NgModule({
    declarations: [
        AppComponent,
        HomeComponent,
        LoginComponent,
        CreateTemplateComponent,
        DownloadTemplateComponent,
        InputValidateDialogComponent,
        UploadSuccessDialogComponent,
        DeleteConfirmDialogComponent,
        RatingSuccessDialogComponent,
        MyTemplatesComponent,
        TutorialComponent,
    ],
    imports: [
        AngularFireModule.initializeApp(config),
        BrowserAnimationsModule,
        BrowserModule,
        HttpClientModule,
        MatButtonModule,
        MatCardModule,
        MatDialogModule,
        MatDividerModule,
        MatToolbarModule,
        MatInputModule,
        MatSelectModule,
        RouterModule.forRoot(routes),
        FormsModule,
        MatChipsModule
    ],
    entryComponents: [
        InputValidateDialogComponent,
        UploadSuccessDialogComponent,
        DeleteConfirmDialogComponent,
        RatingSuccessDialogComponent
    ],
    providers: [
        AngularFireAuth,
        AngularFireDatabase,
        AngularFireStorage,
        ApiInterfaceService,
        UploadService
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule {
}
<file_sep># Templyte

Templyte is a web application which is designed to help users create and share code templates to allow for fast bootstrapping of a variety of projects.

## Release Notes

### Features

* Users can create and upload templates that are populated with user defined variables at download time, allowing for highly customisable starter code for any project.
* Users can download templates which are intelligently rendered to use custom variables defined by the user.
* Templates are language agnostic, so users are not limited according to the technologies that their projects use.
* The application contains a tutorial page that guides users through creating templates.
* Secure login through GitHub is supported.
* The application allows users to rate templates as well as leave text reviews.
* The application supports searching the database for templates based on the title or user defined tags. Partial fuzzy matching is supported, so users do not need to provide an exact much for the search term. (in terms of capitalization or spacing) Search results can also be ordered according to rating, number of downloads, creation date and last downloaded date.

### Known Issues
 
Due to development time limitations, the search functionality of the application is not able to handle more complex search terms. Additionally, it is not as efficient as it could be, due to the need to do the search through Firebase. This triggers a warning in the user's browser console when doing a search, which states that the query is not as efficient as it could be. Though this should be fixed, it is not as huge of an issue as it may appear to be, since the database information has been flattened in order to speed up searches.

## Install Guide

Templyte is a web application with both a front end and a backend component, as well as a database component in order to allow for persistence.

### Prerequisites
As the dependencies are handled by [npm](https://www.npmjs.com/get-npm), Templyte can be deployed on any operating system that supports npm.

### Acquiring Source Code
The latest version of the application source code can be downloaded directly from [GitHub](https://github.com/debkbanerji/templyte)

### Dependency Management

The dependencies of the application are handled by [npm](https://www.npmjs.com/get-npm), the Node package manager which can be installed alongside [Node.js](https://nodejs.org/en/download). The application requires Node.js in order to run the server, which both serves the static frontend components to users and carries out any backend logic for the A.P.I. operations. Therefore, both npm and Node.js must be installed in order to develop or deploy the application. The frontend of the application is built using Angular, but any dependencies required for building the Angular components are handled through npm as well.

In order to install all the required dependencies, run `npm install` from the application's base directory.

### Database Setup

The persistence layer, as well as the login functionality of the application are handled through [Firebase](https://firebase.google.com/). In order to set up the database, one must first create a new Firebase project through the [Firebase console](https://firebase.google.com/). After the project has been created, one must connect it to the application by overriding the Firebase configuration object in `/static/templyte/src/app/config/firebase-config.ts` with the one provided by the console. The fields that must be replaced should be fairly self explanatory - if the configuration object provided by the console does not contain all the required fields, first set up GitHub login (described later) and enable the realtime database as well as the datastore functionality through the console. In order to enable GitHub login, follow the steps provided within the 'Authentication' tab of the Firebase console.

Once the database has been connected, it is extremely important to set up rules for both the realtime database and the datastore, to allow for securing as well as fficient indexing of data. These rules can be found in `database-rules.json` and `datastore-rules.txt` respectively and can simply be copied into the 'rules' section of their respective firebase console pages. It is extremely important to set both sets of rules up before deployment.

Note that currently, the configuration object in `/static/templyte/src/app/config/firebase-config.ts` refers to an example development database. When deploying the application, it is recommended that you replace this information with the connection information for the production database.

### Development Server

Once the dependencies have been installed, running the application in development mode requires the developer to run two commands in separate shells. The first is `npm run development-server` which starts up a development version of the A.P.I. that runs on port 3000. (this development server automatically restarts upon any file changes, to allow for faster development) The second command is `npm run development-gui` which starts the Angular application that makes up the front end. This runs on port 4200 and can be accessed by entering `localhost:4200` into the address bar of the developer's web browser. Note that this development version of the Angular application automatically refreshes the web page upon any code changes.

### Deployment to Production

After running `npm install`, running `npm start` will start a production version of the application on port 3000. Most modern web application hosting services, such as Heroku and Amazon Web Services will be able to automatically recognize the npm information defined in `package.json` and run both the `npm install` and `npm start` commands for you.

Note that once you deploy the application to production, you will need to register the domain name of the deployed version within the Firebase console as a trusted login source, or login will be blocked.

### Troubleshooting
While troubleshooting, first steps should always include clearing the `node_modules` folder and rerunning `npm install`. Most issues will raise errors on the frontend which can be debugged through use of the browser developer console, with the exception of A.P.I. calls (template rendering logic) for which error messages will show up in the server logs.
<file_sep>// Get dependencies
const express = require('express');
const path = require('path');
const http = require('http');
const bodyParser = require('body-parser');

// Get our API routes
const api = require(path.join(__dirname, 'api', 'api.js'));

const app = express();
app.use(bodyParser.json());

/**
 * Get port from environment and store in Express.
 */
const port = process.env.PORT || '3000';
app.set('port', port);

let isDevMode = false;

process.argv.forEach(function (val) {
    if (/DEVELOP/.test(val)) {
        isDevMode = true;
    }
});

if (isDevMode) {
    console.log('RUNNING IN DEVELOPMENT MODE');

    // Add headers
    app.use(function (req, res, next) {

        // Website you wish to allow to connect
        res.setHeader('Access-Control-Allow-Origin', '*');

        // Request methods you wish to allow
        res.setHeader('Access-Control-Allow-Methods', 'GET');

        // Request headers you wish to allow
        res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');

        // Pass to next layer of middleware
        next();
    });
}

const frontendFolder = path.join(__dirname, 'static', 'templyte', 'dist', 'templyte');
// Point static path to frontend folder
app.use(express.static(frontendFolder));
console.log("Serving static from " + frontendFolder);

// Set our api routes
app.use('/api', api);

console.log("Setting api routes");

// Catch all other routes and return the index file
app.get('*', (req, res) => {
    res.sendFile(path.join(frontendFolder, 'index.html'));
});

console.log("Catching all other routes and returning the index file");

/**
 * Create HTTP server.
 */
const server = http.createServer(app);

console.log("Created Server");
/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port, () => {
    if (isDevMode) {
        console.log('\nPlease use \'npm run development-gui\' to start a development gui and navigate to localhost:4200');
        console.log('Node that the api is still running on port ' + port.toString())
    } else {
        console.log(`\nServer running on port: ${port}`);
    }
});
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../environments/environment';

@Injectable({
    providedIn: 'root'
})
export class ApiInterfaceService {

    private readonly API_ADDRESS: string;

    constructor(
        private http: HttpClient
    ) {
        this.API_ADDRESS = environment.production
            ? '/api' :
            'http://localhost:3000/api'; // Expect a development server to be running if running in prod mode
    }

    getZipFile(request: string, callback): void {
        const targetAddress = this.API_ADDRESS + '/download-template';

        const options = {responseType: 'blob' as 'blob'};
        this.http.get(targetAddress + '?request=' + request, options)
            .subscribe(downloadedData => {
                callback(downloadedData);

            }, (error => {
                callback('Error connecting to API: ' + error.message);
            }));
    }
}
