diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..8764d29
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,32 @@
+root = true;
+
+[*]
+charset=utf-8
+end_of_line=crlf
+insert_final_newline=true
+indent_style=tab
+indent_size=4
+
+[{*.sht,*.htm,*.html,*.shtm,*.shtml,*.handlebars,*.hb,*.hbs}]
+indent_style=tab
+tab_width=4
+
+[{.babelrc,.stylelintrc,.eslintrc,*.bowerrc,*.jsb3,*.jsb2,*.json}]
+indent_style=tab
+tab_width=4
+
+[{*.js}]
+indent_style=tab
+tab_width=4
+
+[{*.sql}]
+indent_style=tab
+tab_width=4
+
+[*.scss]
+indent_style=tab
+tab_width=4
+
+[{*.yml,*.yaml}]
+indent_style=tab
+indent_size=2
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..15cf758
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,263 @@
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+
+### Eclipse ###
+.idea
+.mvn
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+
+# Eclipse Core
+.project
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
+
+### WebStorm ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+
+# Sensitive or high-churn files:
+.idea/dataSources.ids
+.idea/dataSources.xml
+.idea/dataSources.local.xml
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# Gradle:
+.idea/gradle.xml
+.idea/libraries
+
+# Mongo Explorer plugin:
+.idea/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+### WebStorm Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+*.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+### Git ###
+*.orig
+
+### JDeveloper ###
+# default application storage directory used by the IDE Performance Cache feature
+.data/
+
+# used for ADF styles caching
+temp/
+
+# default output directories
+classes/
+deploy/
+javadoc/
+
+# lock file, a part of Oracle Credential Store Framework
+cwallet.sso.lck
+
+### SublimeText ###
+# cache files for sublime text
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+
+# workspace files are user-specific
+*.sublime-workspace
+
+# project files should be checked into the repository, unless a significant
+# proportion of contributors will probably not be using SublimeText
+# *.sublime-project
+
+# sftp configuration file
+sftp-config.json
+
+# Package control specific files
+Package Control.last-run
+Package Control.ca-list
+Package Control.ca-bundle
+Package Control.system-ca-bundle
+Package Control.cache/
+Package Control.ca-certs/
+bh_unicode_properties.cache
+
+# Sublime-github package stores a github token in this file
+# https://packagecontrol.io/packages/sublime-github
+GitHub.sublime-settings
+
+
+### Java ###
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Gradle ###
+.gradle
+/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules
+jspm_packages
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+### Bower ###
+bower_components
+.bower-cache
+.bower-registry
+.bower-tmp
+
+### Sass ###
+.sass-cache/
+*.css.map
+
+### Distributed folder ###
+dist/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..2142943
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+language: java
+jdk:
+ - oraclejdk8
+sudo: required
+install: true
+cache:
+ directories:
+ - $HOME/.m2
+install: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3e25a39
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,15 @@
+# Changelog
+
+
+## May 2017 - 0.7.0
+### First verion released
+#### Features:
+* Login/Register using JSON Web Token
+* Listing products
+* Popup window with details
+* Search bar with auto-completion
+* Product page view
+* PayPal Payment
+* Google Maps with geo-location
+* Saving user data into HTML5 localstorage
+* Updating basket using AJAX
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..5a3d0f5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,16 @@
+
Contributing
+
+
Fork and clone into your local environment
+
Implement changes you would like
+
Run JUnit tests - make sure all execute correctly
+
Run Karma - use CLI by typing "gulp karma"
+
Write JUnit and/or Jasmine test (if appropriate)
+
Run your unit tests
+
Run linters
+
+
gulp eslint
+
gulp scss-lint
+
+
+
Make merge request :)
+
diff --git a/LEGAL.md b/LEGAL.md
new file mode 100644
index 0000000..eb2e6e4
--- /dev/null
+++ b/LEGAL.md
@@ -0,0 +1,18 @@
+# Legal
+
+PayPal logo provided by PayPal Logo Centre:
+https://www.paypal.com/uk/webapps/mpp/logo-center
+
+Background image provided in the **assets** directory under Creative Commons licence (CC):
+
+Find out more at:
+https://creativecommons.org/licenses/
+
+Favico created using Font Awesome, under OFL licence:
+http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
+
+Example images of books are fetched using REST API created and shared by
+Open Library.
+
+Find out more at:
+https://openlibrary.org/dev/docs/api/covers
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2b678b5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 - 2017 Bartosz-D3V
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bc4afbb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,94 @@
+# SPA & RESTful E-commerce
+
+[![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/Bartosz-D3V/E-commerce-full-stack-website/blob/master/LICENSE) [![license](https://img.shields.io/badge/New%20contributors-Welcome!-brightgreen.svg)]() [![built with gulp](https://img.shields.io/badge/gulp-built_project-eb4a4b.svg?logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAAAYAAAAOCAMAAAA7QZ0XAAAABlBMVEUAAAD%2F%2F%2F%2Bl2Z%2FdAAAAAXRSTlMAQObYZgAAABdJREFUeAFjAAFGRjSSEQzwUgwQkjAFAAtaAD0Ls2nMAAAAAElFTkSuQmCC)](http://gulpjs.com/)
+
+
+Table of contents
+- Introduction
+- Rationale and justification for website
+- Security consideration
+- List of technologies
+- Backend
+- Frontend
+- Installation
+- Conclusions
+- License
+
+## Introduction
+Most of the online shops you can find on the web use standard server-side rendered views which not only makes it impossible to use the webapp offline, but what is more important, it makes the page slower, bigger and lowers final user experience.
+The following project has been created to overcome the above issues.
+It exchange all data with backend application using RESTful api which makes it also extremely easy to create additional app for iOS or Android.
+It also follows the SPA (Single Page Application) architecture pattern which means that is is not only very easy to add new views, but also the final user experience is way better due to minimal time loading.
+
+## Rationale and justification for website
+My initial idea was to create e-commerce project that would be built using Java EE technologies with Thymeleaf which is typicall technology stack for most of the website with most parts of the website being rendered on the server side and sent back to the user.
+Later on I understand that this approach has a lot of boundaries, and although might look easier it makes the application less user friendly.
+This is due to long response time, re-loading all pages on views switching.
+It also makes it more difficult if in the future appropriate iOS/Android application would be created that will need to consume API of the project.
+I decided to create a RESTful API that could accesible for public - except from unsafe actions like removing or adding new products, or performing checkout of the customer's basket.
+This not only make the website incredibly fast and user-friendly, but very easy to extend and maintain in the future.
+
+## Security consideration
+Application is accessible only from specific internet address and port by using CORS set up in appropriate Java configuration file.
+Crucial parts of application are secured using Spring Security - appropriate JSON Token needs to be requested and sent back.
+JSON Tokens are encrypted using SALT that are re-generated every time application is restarted.
+All passwords are encrypted using BCrypt with set strength of 8. This operation is then repeated 12 times.
+BCrypt is one-way encryption function, meaning that once encoded, it is impossible to decode - validation is performed by comparing the cyphertexts.
+All operations between server and database are transactional, meaning that in case of unexpected failure application will recover and revert all changes done to database. This has been implemented using Hibernate and Javax.
+Frontend application has standard security features including appropriate input types form elements and cleaning the form data after performing AJAX requests.
+Also, appropriate HTTP address endpoints are accessible only when appropriate action has taken place – for example localhost:3000/checkout/success will not appear when user will try to open it by manually – it will only appear when PayPal payment will be confirmed.
+Payment mechanism is secured by HTTPS protocol and handled by PayPal vendor.
+In addition, credit card details are never stored in the database.
+To increase security SSL certificate could be bought from approved provider and added to website which would make the front-end application even more secure by using encryption.
+Soon, I would like to implement admin dashboard where workers will be able to edit content of the website, once done, JSON Web Token will also contain a role of user so each user of the website will be able to perform only valid actions for specific role. For example, only worker will be able to add new products, but will not be able to make orders – similarly, customer will be able to make orders, but will not be able to edit content of the website.
+
+## List of technologies
+
+### Backend:
+* Java 8
+* Spring Boot
+* Spring MVC
+* Spring Security
+* Hibernate ORM
+* Hibernate Validator
+* JSON Web Tokens - Java Implementation
+### Frontend:
+* JavaScript (ES6)
+* HTML5
+* CSS3
+* Handlebars
+* MarionetteJS
+* BackboneJS
+* jQuery
+* BlazeCSS
+* Backbone.localstorage
+* Backbone.Radio
+* SCSS
+* NodeJS
+* Browserify
+* Gulp
+
+## Installation
+
+First create an empty database called e-commerce.
+Hibernate will create the whole schema at the bootstrap time.
+### Running server side application
+```batch
+$ ./mvnw install
+$ ./mvnw spring-boot:run
+```
+### Running web application
+```batch
+$ npm install
+$ gulp serve
+```
+You can now open localhost:3000
+
+## Conclusions
+Developing of application is never-ending process - there is always number of features to be implemented, it is not different for this particular project.
+There are number of ideas that I would like to implement soon, including admin dashboard panel that would allow not only to edit the content of the page, but also to handle orders.
+I truely believe that Single Page Applications backed by RESTful API is the future of the web and more and more application will us these architecture pattern, including large enterprise projects.
+
+## License
+[MIT](https://github.com/Bartosz-D3V/E-commerce-full-stack-website/blob/master/LICENSE)
+
diff --git a/backend/mvnw b/backend/mvnw
new file mode 100644
index 0000000..5bf251c
--- /dev/null
+++ b/backend/mvnw
@@ -0,0 +1,225 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+echo $MAVEN_PROJECTBASEDIR
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd
new file mode 100644
index 0000000..019bd74
--- /dev/null
+++ b/backend/mvnw.cmd
@@ -0,0 +1,143 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/backend/pom.xml b/backend/pom.xml
new file mode 100644
index 0000000..1370b04
--- /dev/null
+++ b/backend/pom.xml
@@ -0,0 +1,94 @@
+
+
+ 4.0.0
+
+ com.ecommerce
+ ecommerce-project
+ 0.0.1-SNAPSHOT
+ jar
+
+ eCommerce Project
+ eCommerce project for 3rd year module
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.4.0.RELEASE
+
+
+
+
+ UTF-8
+ UTF-8
+ 4.3.11.Final
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ mysql
+ mysql-connector-java
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ junit
+ junit
+
+
+
+ org.mockito
+ mockito-core
+
+
+
+ org.springframework
+ spring-test
+ 4.3.2.RELEASE
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ io.jsonwebtoken
+ jjwt
+ 0.7.0
+
+
+
+ it.ozimov
+ spring-boot-email-core
+ 0.5.1
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/backend/src/main/java/com/ecommerce/ECommerceProjectApplication.java b/backend/src/main/java/com/ecommerce/ECommerceProjectApplication.java
new file mode 100644
index 0000000..62ebc55
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/ECommerceProjectApplication.java
@@ -0,0 +1,13 @@
+package com.ecommerce;
+
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ECommerceProjectApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ECommerceProjectApplication.class, args);
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/domain/products/Audiobook.java b/backend/src/main/java/com/ecommerce/bean/domain/products/Audiobook.java
new file mode 100644
index 0000000..d69a5c2
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/domain/products/Audiobook.java
@@ -0,0 +1,63 @@
+package com.ecommerce.bean.domain.products;
+
+
+import com.ecommerce.enumeration.AudioExtension;
+import com.ecommerce.enumeration.Genre;
+
+import javax.persistence.AttributeOverride;
+import javax.persistence.AttributeOverrides;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import java.math.BigDecimal;
+
+@Entity
+@AttributeOverrides({
+ @AttributeOverride(name = "title", column = @Column(name = "title")),
+ @AttributeOverride(name = "author", column = @Column(name = "author")),
+ @AttributeOverride(name = "publisher", column = @Column(name = "publisher")),
+ @AttributeOverride(name = "genre", column = @Column(name = "genre")),
+ @AttributeOverride(name = "description", column = @Column(name = "description")),
+ @AttributeOverride(name = "publicityYear", column = @Column(name = "publicity_year")),
+ @AttributeOverride(name = "price", column = @Column(name = "price")),
+ @AttributeOverride(name = "quantity", column = @Column(name = "quantity"))
+})
+public class Audiobook extends Product {
+
+ @Column(name = "audio_extension")
+ @Enumerated(EnumType.STRING)
+ private AudioExtension audioExtension;
+
+ @Column(name = "duration_time")
+ private BigDecimal durationTime;
+
+ public Audiobook(long id, String title, String author, String category, Genre genre, String description,
+ int publicityYear, BigDecimal price, AudioExtension audioExtension, BigDecimal durationTime,
+ int quantity, String olid) {
+ super(id, title, author, category, "Audiobooks", genre, description, publicityYear, price, quantity, olid);
+ this.audioExtension = audioExtension;
+ this.durationTime = durationTime;
+ }
+
+ public Audiobook() {
+ super();
+ }
+
+ public AudioExtension getAudioExtension() {
+ return audioExtension;
+ }
+
+ public void setAudioExtension(AudioExtension audioExtension) {
+ this.audioExtension = audioExtension;
+ }
+
+ public BigDecimal getDurationTime() {
+ return durationTime;
+ }
+
+ public void setDurationTime(BigDecimal durationTime) {
+ this.durationTime = durationTime;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/domain/products/Book.java b/backend/src/main/java/com/ecommerce/bean/domain/products/Book.java
new file mode 100644
index 0000000..3baeefb
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/domain/products/Book.java
@@ -0,0 +1,57 @@
+package com.ecommerce.bean.domain.products;
+
+
+import com.ecommerce.enumeration.Genre;
+
+import javax.persistence.AttributeOverride;
+import javax.persistence.AttributeOverrides;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import java.math.BigDecimal;
+
+@Entity
+@AttributeOverrides({
+ @AttributeOverride(name = "title", column = @Column(name = "title")),
+ @AttributeOverride(name = "author", column = @Column(name = "author")),
+ @AttributeOverride(name = "publisher", column = @Column(name = "publisher")),
+ @AttributeOverride(name = "genre", column = @Column(name = "genre")),
+ @AttributeOverride(name = "description", column = @Column(name = "description")),
+ @AttributeOverride(name = "publicityYear", column = @Column(name = "publicity_year")),
+ @AttributeOverride(name = "price", column = @Column(name = "price")),
+ @AttributeOverride(name = "quantity", column = @Column(name = "quantity"))
+})
+public class Book extends Product {
+
+ @Column(name = "cover_type")
+ private String coverType;
+ @Column(name = "number_of_pages")
+ private int numberOfPages;
+
+ public Book(long id, String title, String author, String category, Genre genre, String description,
+ int publicityYear, BigDecimal price, String coverType, int numberOfPages, int quantity, String olid) {
+ super(id, title, author, category, "Books", genre, description, publicityYear, price, quantity, olid);
+ this.coverType = coverType;
+ this.numberOfPages = numberOfPages;
+ }
+
+ public Book() {
+ super();
+ }
+
+ public String getCoverType() {
+ return coverType;
+ }
+
+ public void setCoverType(String coverType) {
+ this.coverType = coverType;
+ }
+
+ public int getNumberOfPages() {
+ return numberOfPages;
+ }
+
+ public void setNumberOfPages(int numberOfPages) {
+ this.numberOfPages = numberOfPages;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/domain/products/Ebook.java b/backend/src/main/java/com/ecommerce/bean/domain/products/Ebook.java
new file mode 100644
index 0000000..c2d4f91
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/domain/products/Ebook.java
@@ -0,0 +1,51 @@
+package com.ecommerce.bean.domain.products;
+
+
+import com.ecommerce.enumeration.Genre;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+
+@Entity
+@AttributeOverrides({
+ @AttributeOverride(name = "title", column = @Column(name = "title")),
+ @AttributeOverride(name = "author", column = @Column(name = "author")),
+ @AttributeOverride(name = "publisher", column = @Column(name = "publisher")),
+ @AttributeOverride(name = "genre", column = @Column(name = "genre")),
+ @AttributeOverride(name = "description", column = @Column(name = "description")),
+ @AttributeOverride(name = "publicityYear", column = @Column(name = "publicity_year")),
+ @AttributeOverride(name = "price", column = @Column(name = "price")),
+ @AttributeOverride(name = "quantity", column = @Column(name = "quantity"))
+})
+public class Ebook extends Product {
+
+ private String extension;
+ private BigDecimal size;
+
+ public Ebook(long id, String title, String author, String category, Genre genre, String description,
+ int publicityYear, BigDecimal price, String extension, BigDecimal size, int quantity, String olid) {
+ super(id, title, author, category, "Ebooks", genre, description, publicityYear, price, quantity, olid);
+ this.extension = extension;
+ this.size = size;
+ }
+
+ public Ebook() {
+ super();
+ }
+
+ public String getExtension() {
+ return extension;
+ }
+
+ public void setExtension(String extension) {
+ this.extension = extension;
+ }
+
+ public BigDecimal getSize() {
+ return size;
+ }
+
+ public void setSize(BigDecimal size) {
+ this.size = size;
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/domain/products/Product.java b/backend/src/main/java/com/ecommerce/bean/domain/products/Product.java
new file mode 100644
index 0000000..11bfd90
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/domain/products/Product.java
@@ -0,0 +1,152 @@
+package com.ecommerce.bean.domain.products;
+
+
+import com.ecommerce.enumeration.Genre;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Inheritance;
+import javax.persistence.InheritanceType;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class Product {
+
+ @Id
+ @NotNull
+ @GeneratedValue(strategy = GenerationType.TABLE)
+ private long id;
+
+ @NotNull
+ private String title;
+
+ private String author;
+ private String category;
+ private String publisher;
+
+ @Enumerated(EnumType.STRING)
+ private Genre genre;
+
+ private String description;
+
+ @Column(name = "publicity_year")
+ private int publicityYear;
+ private BigDecimal price;
+
+ private int quantity;
+ private String olid;
+
+ public Product(long id, String title, String author, String category, String publisher, Genre genre,
+ String description, int publicityYear, BigDecimal price, int quantity, String olid) {
+ this.id = id;
+ this.title = title;
+ this.author = author;
+ this.category = category;
+ this.publisher = publisher;
+ this.genre = genre;
+ this.description = description;
+ this.publicityYear = publicityYear;
+ this.price = price;
+ this.quantity = quantity;
+ this.olid = olid;
+ }
+
+ public Product() {
+ super();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public String getPublisher() {
+ return publisher;
+ }
+
+ public void setPublisher(String publisher) {
+ this.publisher = publisher;
+ }
+
+ public Genre getGenre() {
+ return genre;
+ }
+
+ public void setGenre(Genre genre) {
+ this.genre = genre;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public int getPublicityYear() {
+ return publicityYear;
+ }
+
+ public void setPublicityYear(int publicityYear) {
+ this.publicityYear = publicityYear;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ public String getOlid() {
+ return olid;
+ }
+
+ public void setOlid(String olid) {
+ this.olid = olid;
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/domain/transaction/Basket.java b/backend/src/main/java/com/ecommerce/bean/domain/transaction/Basket.java
new file mode 100644
index 0000000..2a5c78c
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/domain/transaction/Basket.java
@@ -0,0 +1,63 @@
+package com.ecommerce.bean.domain.transaction;
+
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+@Entity
+public class Basket {
+
+ @Id
+ @NotNull
+ @GeneratedValue
+ private long id;
+
+ @ManyToMany(cascade = {CascadeType.ALL})
+ @JoinTable(name = "delivery", joinColumns = {@JoinColumn(name = "basket_id")},
+ inverseJoinColumns = {@JoinColumn(name = "order_item_id")})
+ private Set orderItems;
+
+ @Column(name = "customer_email")
+ private String customerEmail;
+
+ public Basket(Set orderItems, String customerEmail) {
+ this.orderItems = orderItems;
+ this.customerEmail = customerEmail;
+ }
+
+ public Basket() {
+ super();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public Set getBasket() {
+ return orderItems;
+ }
+
+ public void setBasket(Set orderItems) {
+ this.orderItems = orderItems;
+ }
+
+ public String getCustomerEmail() {
+ return customerEmail;
+ }
+
+ public void setCustomerEmail(String customerEmail) {
+ this.customerEmail = customerEmail;
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/domain/transaction/OrderItem.java b/backend/src/main/java/com/ecommerce/bean/domain/transaction/OrderItem.java
new file mode 100644
index 0000000..5e7c844
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/domain/transaction/OrderItem.java
@@ -0,0 +1,81 @@
+package com.ecommerce.bean.domain.transaction;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.Table;
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+@Entity
+@Table(name = "order_item")
+public class OrderItem {
+
+ @Id
+ @NotNull
+ @GeneratedValue
+ @Column(name = "order_item_id")
+ private long id;
+
+ @NotNull
+ private int quantity;
+
+ @Column(name = "product_id")
+ private long productId;
+
+ @JsonIgnore
+ @ManyToMany(cascade = {CascadeType.ALL})
+ @JoinTable(name = "delivery", joinColumns = {@JoinColumn(name = "order_item_id")},
+ inverseJoinColumns = {@JoinColumn(name = "basket_id")})
+ private Set basket;
+
+ public OrderItem(int quantity, long productId) {
+ this.quantity = quantity;
+ this.productId = productId;
+ }
+
+ public OrderItem() {
+ super();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ public Set getBasket() {
+ return basket;
+ }
+
+ public void setBasket(Set basket) {
+ this.basket = basket;
+ }
+
+ public long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(long productId) {
+ this.productId = productId;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/security/AccountCredentials.java b/backend/src/main/java/com/ecommerce/bean/security/AccountCredentials.java
new file mode 100644
index 0000000..2a30a46
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/security/AccountCredentials.java
@@ -0,0 +1,34 @@
+package com.ecommerce.bean.security;
+
+
+public class AccountCredentials {
+
+ private String email;
+ private String password;
+
+ public AccountCredentials(String email, String password) {
+ this.email = email;
+ this.password = password;
+ }
+
+ public AccountCredentials() {
+ super();
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/security/JwtResponse.java b/backend/src/main/java/com/ecommerce/bean/security/JwtResponse.java
new file mode 100644
index 0000000..1833709
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/security/JwtResponse.java
@@ -0,0 +1,22 @@
+package com.ecommerce.bean.security;
+
+public class JwtResponse {
+
+ private String token;
+
+ public JwtResponse(String token) {
+ this.token = token;
+ }
+
+ public JwtResponse() {
+ super();
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public String getToken() {
+ return token;
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/system/APIKey.java b/backend/src/main/java/com/ecommerce/bean/system/APIKey.java
new file mode 100644
index 0000000..a2ad1bb
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/system/APIKey.java
@@ -0,0 +1,18 @@
+package com.ecommerce.bean.system;
+
+public class APIKey {
+
+ private String token;
+
+ public APIKey(String token) {
+ this.token = token;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/users/Customer.java b/backend/src/main/java/com/ecommerce/bean/users/Customer.java
new file mode 100644
index 0000000..4b3a07f
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/users/Customer.java
@@ -0,0 +1,40 @@
+package com.ecommerce.bean.users;
+
+
+import javax.persistence.AttributeOverride;
+import javax.persistence.AttributeOverrides;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@AttributeOverrides({
+ @AttributeOverride(name = "id", column = @Column(name = "id")),
+ @AttributeOverride(name = "email", column = @Column(name = "email")),
+ @AttributeOverride(name = "password", column = @Column(name = "password")),
+ @AttributeOverride(name = "firstName", column = @Column(name = "first_name")),
+ @AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
+})
+public class Customer extends User {
+
+ @NotNull
+ private String address;
+
+ public Customer(String email, String password, String firstName, String lastName, String address) {
+ super(email, password, firstName, lastName);
+ this.address = address;
+ }
+
+ public Customer() {
+ super();
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/bean/users/User.java b/backend/src/main/java/com/ecommerce/bean/users/User.java
new file mode 100644
index 0000000..81f8289
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/bean/users/User.java
@@ -0,0 +1,90 @@
+package com.ecommerce.bean.users;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Inheritance;
+import javax.persistence.InheritanceType;
+import javax.validation.constraints.NotNull;
+
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class User {
+
+ @Id
+ @NotNull
+ @JsonIgnore
+ @GeneratedValue(strategy = GenerationType.TABLE)
+ private long id;
+
+ @NotNull
+ private String email;
+
+ @NotNull
+ private String password;
+
+ @NotNull
+ @Column(name = "first_name")
+ private String firstName;
+
+ @NotNull
+ @Column(name = "last_name")
+ private String lastName;
+
+ public User(String email, String password, String firstName, String lastName) {
+ this.email = email;
+ this.password = password;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public User() {
+ super();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/config/DatabaseConfig.java b/backend/src/main/java/com/ecommerce/config/DatabaseConfig.java
new file mode 100644
index 0000000..d7ecb65
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/config/DatabaseConfig.java
@@ -0,0 +1,75 @@
+package com.ecommerce.config;
+
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.orm.hibernate4.HibernateTransactionManager;
+import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+import java.util.Properties;
+
+@Configuration
+@EnableTransactionManagement
+public class DatabaseConfig {
+
+ @Value("${spring.datasource.driver-class-name}")
+ private String DB_DRIVER;
+
+ @Value("${spring.datasource.password}")
+ private String DB_PASSWORD;
+
+ @Value("${spring.datasource.url}")
+ private String DB_URL;
+
+ @Value("${spring.datasource.username}")
+ private String DB_USERNAME;
+
+ @Value("${hibernate.dialect}")
+ private String HIBERNATE_DIALECT;
+
+ @Value("${hibernate.show_sql}")
+ private String HIBERNATE_SHOW_SQL;
+
+ @Value("${hibernate.hbm2ddl.auto}")
+ private String HIBERNATE_HBM2DDL_AUTO;
+
+ @Value("${entitymanager.packagesToScan}")
+ private String ENTITYMANAGER_PACKAGES_TO_SCAN;
+
+ @Bean
+ public DataSource dataSource() {
+ DriverManagerDataSource dataSource = new DriverManagerDataSource();
+ dataSource.setDriverClassName(DB_DRIVER);
+ dataSource.setUrl(DB_URL);
+ dataSource.setUsername(DB_USERNAME);
+ dataSource.setPassword(DB_PASSWORD);
+ return dataSource;
+ }
+
+ @Bean
+ public LocalSessionFactoryBean sessionFactory() {
+ LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
+ sessionFactoryBean.setDataSource(dataSource());
+ sessionFactoryBean.setPackagesToScan(ENTITYMANAGER_PACKAGES_TO_SCAN);
+ Properties hibernateProperties = new Properties();
+ hibernateProperties.put("hibernate.dialect", HIBERNATE_DIALECT);
+ hibernateProperties.put("hibernate.show_sql", HIBERNATE_SHOW_SQL);
+ hibernateProperties.put("hibernate.hbm2ddl.auto", HIBERNATE_HBM2DDL_AUTO);
+ sessionFactoryBean.setHibernateProperties(hibernateProperties);
+
+ return sessionFactoryBean;
+ }
+
+ @Bean
+ public HibernateTransactionManager transactionManager() {
+ HibernateTransactionManager transactionManager =
+ new HibernateTransactionManager();
+ transactionManager.setSessionFactory(sessionFactory().getObject());
+ return transactionManager;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/config/WebConfig.java b/backend/src/main/java/com/ecommerce/config/WebConfig.java
new file mode 100644
index 0000000..def480b
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/config/WebConfig.java
@@ -0,0 +1,18 @@
+package com.ecommerce.config;
+
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+@Configuration
+@EnableWebMvc
+public class WebConfig extends WebMvcConfigurerAdapter {
+
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**");
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/config/WebSecurityConfig.java b/backend/src/main/java/com/ecommerce/config/WebSecurityConfig.java
new file mode 100644
index 0000000..a57e607
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/config/WebSecurityConfig.java
@@ -0,0 +1,43 @@
+package com.ecommerce.config;
+
+
+import com.ecommerce.tools.security.JwtFilter;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+
+@Configuration
+@EnableAutoConfiguration
+@ComponentScan
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+ @Bean
+ public FilterRegistrationBean jwtFilter() {
+ final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+ registrationBean.setFilter(new JwtFilter());
+ registrationBean.addUrlPatterns("/checkout/**", "/admin/**");
+
+ return registrationBean;
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.headers().cacheControl();
+ http
+ .csrf().disable()
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+ .authorizeRequests()
+ .antMatchers(
+ "/books/**", "/ebooks/**", "/audiobooks/**", "/customer/**", "/register/**", "/order/**"
+ ).permitAll()
+ .antMatchers("/auth").permitAll();
+
+ http.headers().cacheControl();
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/domain/products/AudiobookController.java b/backend/src/main/java/com/ecommerce/controller/domain/products/AudiobookController.java
new file mode 100644
index 0000000..2f13afa
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/domain/products/AudiobookController.java
@@ -0,0 +1,41 @@
+package com.ecommerce.controller.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Audiobook;
+import com.ecommerce.facade.domain.products.ProductFacade;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/audiobooks")
+public class AudiobookController {
+
+ private final ProductFacade productFacade;
+
+ public AudiobookController(ProductFacade productFacade) {
+ this.productFacade = productFacade;
+ }
+
+ @RequestMapping(method = RequestMethod.POST)
+ public void addSingleAudiobook(@RequestBody @Valid Audiobook audiobook) {
+ productFacade.addSingleAudiobook(audiobook);
+ }
+
+ @RequestMapping(method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public Audiobook getSingleAudiobook(@RequestParam(value = "id") long id) {
+ return productFacade.getSingleAudiobook(id);
+ }
+
+ @RequestMapping(value = ("/all"), method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public List getAllAudiobooks() {
+ return productFacade.getAllAudiobooks();
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/domain/products/BookController.java b/backend/src/main/java/com/ecommerce/controller/domain/products/BookController.java
new file mode 100644
index 0000000..8b3ca9b
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/domain/products/BookController.java
@@ -0,0 +1,41 @@
+package com.ecommerce.controller.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Book;
+import com.ecommerce.facade.domain.products.ProductFacade;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/books")
+public class BookController {
+
+ private final ProductFacade productFacade;
+
+ public BookController(ProductFacade productFacade) {
+ this.productFacade = productFacade;
+ }
+
+ @RequestMapping(method = RequestMethod.POST)
+ public void addSingleBook(@RequestBody @Valid Book book) {
+ productFacade.addSingleBook(book);
+ }
+
+ @RequestMapping(method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public Book getSingleBook(@RequestParam(value = "id") long id) {
+ return productFacade.getSingleBook(id);
+ }
+
+ @RequestMapping(value = ("/all"), method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public List getAllBooks() {
+ return productFacade.getAllBooks();
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/domain/products/EbookController.java b/backend/src/main/java/com/ecommerce/controller/domain/products/EbookController.java
new file mode 100644
index 0000000..59a7af3
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/domain/products/EbookController.java
@@ -0,0 +1,41 @@
+package com.ecommerce.controller.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Ebook;
+import com.ecommerce.facade.domain.products.ProductFacade;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/ebooks")
+public class EbookController {
+
+ private final ProductFacade productFacade;
+
+ public EbookController(ProductFacade productFacade) {
+ this.productFacade = productFacade;
+ }
+
+ @RequestMapping(method = RequestMethod.POST)
+ public void addSingleEbook(@RequestBody @Valid Ebook ebook) {
+ productFacade.addSingleEbook(ebook);
+ }
+
+ @RequestMapping(method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public Ebook getSingleEbooks(@RequestParam(value = "id") long id) {
+ return productFacade.getSingleEbook(id);
+ }
+
+ @RequestMapping(value = ("/all"), method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public List getAllEbooks() {
+ return productFacade.getAllEbooks();
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/domain/products/ProductController.java b/backend/src/main/java/com/ecommerce/controller/domain/products/ProductController.java
new file mode 100644
index 0000000..71e0aff
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/domain/products/ProductController.java
@@ -0,0 +1,50 @@
+package com.ecommerce.controller.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Product;
+import com.ecommerce.facade.domain.products.ProductFacade;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Set;
+
+@RestController
+@RequestMapping(value = "/products")
+public class ProductController {
+
+ private final ProductFacade productFacade;
+
+ public ProductController(ProductFacade productFacade) {
+ this.productFacade = productFacade;
+ }
+
+ @RequestMapping(value = "/all", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public List extends Product> getAllProducts() {
+ return productFacade.getAllProducts();
+ }
+
+ @RequestMapping(value = "get-by-id", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public Product getSingleProduct(@RequestParam(value = "id") long id) {
+ return productFacade.getSingleProduct(id);
+ }
+
+ @RequestMapping(value = "get-by-title", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public Product getSingleProduct(@RequestParam(value = "title") String title) {
+ return productFacade.getSingleProduct(title);
+ }
+
+ @RequestMapping(value = "available-titles", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public List> getAllAvailableTitles() {
+ return productFacade.getAllAvailableTitles();
+ }
+
+ // @RequestMapping(method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ // public Set searchByPhrase(@RequestParam(value = "phrase") String phrase) {
+ // return productFacade.searchByPhrase(phrase);
+ // }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/domain/transaction/OrderController.java b/backend/src/main/java/com/ecommerce/controller/domain/transaction/OrderController.java
new file mode 100644
index 0000000..5229ecb
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/domain/transaction/OrderController.java
@@ -0,0 +1,44 @@
+package com.ecommerce.controller.domain.transaction;
+
+
+import com.ecommerce.bean.domain.transaction.Basket;
+import com.ecommerce.bean.domain.transaction.OrderItem;
+import com.ecommerce.exception.CustomerDoesNotExist;
+import com.ecommerce.exception.TransactionCorrupted;
+import com.ecommerce.facade.domain.transaction.OrderFacade;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/basket")
+public class OrderController {
+
+ private final OrderFacade orderFacade;
+
+ public OrderController(OrderFacade orderFacade) {
+ this.orderFacade = orderFacade;
+ }
+
+ @RequestMapping(value = "/checkBasket", method = RequestMethod.POST)
+ public HttpServletResponse checkBasket(@RequestBody List basket, HttpServletResponse response) {
+ try {
+ orderFacade.checkBasket(basket);
+ response.setStatus(200);
+ } catch (TransactionCorrupted transactionCorrupted) {
+ response.setStatus(409);
+ }
+
+ return response;
+ }
+
+ @RequestMapping(value = "checkout", method = RequestMethod.POST)
+ public void checkout(@RequestBody Basket basket) throws CustomerDoesNotExist {
+ orderFacade.checkout(basket);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/security/AuthenticationController.java b/backend/src/main/java/com/ecommerce/controller/security/AuthenticationController.java
new file mode 100644
index 0000000..bf48da2
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/security/AuthenticationController.java
@@ -0,0 +1,38 @@
+package com.ecommerce.controller.security;
+
+import com.ecommerce.bean.security.AccountCredentials;
+import com.ecommerce.bean.security.JwtResponse;
+import com.ecommerce.facade.security.AuthenticationFacade;
+import com.ecommerce.tools.security.SaltGenerator;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.ServletException;
+import java.util.Date;
+
+@RestController
+public class AuthenticationController {
+
+ private final AuthenticationFacade authenticationFacade;
+
+ public AuthenticationController(AuthenticationFacade authenticationFacade) {
+ this.authenticationFacade = authenticationFacade;
+ }
+
+ @RequestMapping(value = "auth", method = RequestMethod.POST)
+ public JwtResponse login(@RequestBody final AccountCredentials accountCredentials)
+ throws ServletException {
+ if (accountCredentials.getEmail() == null || !authenticationFacade.userExists(accountCredentials.getEmail())
+ || !authenticationFacade.passwordMatch(accountCredentials)) {
+ throw new ServletException("Invalid login");
+ }
+ return new JwtResponse(Jwts.builder().setSubject(accountCredentials.getEmail())
+ .claim("roles", "user").setIssuedAt(new Date())
+ .signWith(SignatureAlgorithm.HS256, SaltGenerator.getInstance().getSalt()).compact());
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/system/SystemVariablesController.java b/backend/src/main/java/com/ecommerce/controller/system/SystemVariablesController.java
new file mode 100644
index 0000000..df7bfdd
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/system/SystemVariablesController.java
@@ -0,0 +1,34 @@
+package com.ecommerce.controller.system;
+
+
+import com.ecommerce.bean.system.APIKey;
+import com.ecommerce.facade.system.SystemVariablesFacade;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping(value = "system")
+public class SystemVariablesController {
+
+ private final SystemVariablesFacade systemVariablesFacade;
+
+ public SystemVariablesController(SystemVariablesFacade systemVariablesFacade) {
+ this.systemVariablesFacade = systemVariablesFacade;
+ }
+
+ @ResponseBody
+ @RequestMapping(value = "google-maps", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public APIKey getGoogleMapsAPIKey() {
+ return systemVariablesFacade.getGoogleMapsAPIKey();
+ }
+
+ @ResponseBody
+ @RequestMapping(value = "paypal", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
+ public APIKey getPayPalAPIKey() {
+ return systemVariablesFacade.getPayPalAPIKey();
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/controller/users/CustomerController.java b/backend/src/main/java/com/ecommerce/controller/users/CustomerController.java
new file mode 100644
index 0000000..a515308
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/controller/users/CustomerController.java
@@ -0,0 +1,34 @@
+package com.ecommerce.controller.users;
+
+
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.exception.EmailAlreadyExists;
+import com.ecommerce.facade.users.CustomerFacade;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+@RestController
+public class CustomerController {
+
+ private final CustomerFacade customerFacade;
+
+ public CustomerController(CustomerFacade customerFacade) {
+ this.customerFacade = customerFacade;
+ }
+
+ @RequestMapping(value = "register", method = RequestMethod.POST)
+ public long registerCustomer(@RequestBody @Valid Customer customer) throws EmailAlreadyExists {
+ return customerFacade.registerCustomer(customer);
+ }
+
+ @RequestMapping(value = "customer-id", method = RequestMethod.POST)
+ public long getCustomerId(@RequestParam String email) {
+ return customerFacade.getCustomerIdByEmail(email);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/enumeration/AudioExtension.java b/backend/src/main/java/com/ecommerce/enumeration/AudioExtension.java
new file mode 100644
index 0000000..47c3491
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/enumeration/AudioExtension.java
@@ -0,0 +1,23 @@
+package com.ecommerce.enumeration;
+
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+
+public enum AudioExtension {
+
+ MP3("MP3"),
+ WAV("WAV"),
+ CDA("CDA");
+
+ private String audioExtension;
+
+ AudioExtension(String audioExtension) {
+ audioExtension = audioExtension;
+ }
+
+ @Enumerated(EnumType.STRING)
+ public String getAudioExtension() {
+ return audioExtension;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/enumeration/Genre.java b/backend/src/main/java/com/ecommerce/enumeration/Genre.java
new file mode 100644
index 0000000..c762a05
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/enumeration/Genre.java
@@ -0,0 +1,25 @@
+package com.ecommerce.enumeration;
+
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+
+public enum Genre {
+
+ Fantasy("Fantasy"),
+ IT("IT"),
+ PN("Philosophical Novel"),
+ Historical("Historical"),
+ EP("Epic Poem");
+
+ private String type;
+
+ Genre(String type) {
+ this.type = type;
+ }
+
+ @Enumerated(EnumType.STRING)
+ public String type() {
+ return type;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/exception/CustomerDoesNotExist.java b/backend/src/main/java/com/ecommerce/exception/CustomerDoesNotExist.java
new file mode 100644
index 0000000..d672258
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/exception/CustomerDoesNotExist.java
@@ -0,0 +1,21 @@
+package com.ecommerce.exception;
+
+public class CustomerDoesNotExist extends Exception {
+
+ public CustomerDoesNotExist() {
+ super();
+ }
+
+ public CustomerDoesNotExist(String message) {
+ super(message);
+ }
+
+ public CustomerDoesNotExist(Throwable cause) {
+ super(cause);
+ }
+
+ public CustomerDoesNotExist(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/exception/EmailAlreadyExists.java b/backend/src/main/java/com/ecommerce/exception/EmailAlreadyExists.java
new file mode 100644
index 0000000..cd8ac5c
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/exception/EmailAlreadyExists.java
@@ -0,0 +1,22 @@
+package com.ecommerce.exception;
+
+
+public class EmailAlreadyExists extends Exception {
+
+ public EmailAlreadyExists() {
+ super();
+ }
+
+ public EmailAlreadyExists(String message) {
+ super(message);
+ }
+
+ public EmailAlreadyExists(Throwable cause) {
+ super(cause);
+ }
+
+ public EmailAlreadyExists(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/exception/ProductOutOfStock.java b/backend/src/main/java/com/ecommerce/exception/ProductOutOfStock.java
new file mode 100644
index 0000000..2f437cf
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/exception/ProductOutOfStock.java
@@ -0,0 +1,22 @@
+package com.ecommerce.exception;
+
+
+public class ProductOutOfStock extends Exception {
+
+ public ProductOutOfStock() {
+ super();
+ }
+
+ public ProductOutOfStock(String message) {
+ super(message);
+ }
+
+ public ProductOutOfStock(Throwable cause) {
+ super(cause);
+ }
+
+ public ProductOutOfStock(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/exception/TransactionCorrupted.java b/backend/src/main/java/com/ecommerce/exception/TransactionCorrupted.java
new file mode 100644
index 0000000..a385820
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/exception/TransactionCorrupted.java
@@ -0,0 +1,22 @@
+package com.ecommerce.exception;
+
+
+public class TransactionCorrupted extends Exception {
+
+ public TransactionCorrupted() {
+ super();
+ }
+
+ public TransactionCorrupted(String message) {
+ super(message);
+ }
+
+ public TransactionCorrupted(Throwable cause) {
+ super(cause);
+ }
+
+ public TransactionCorrupted(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/domain/products/ProductFacade.java b/backend/src/main/java/com/ecommerce/facade/domain/products/ProductFacade.java
new file mode 100644
index 0000000..4ff71ff
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/domain/products/ProductFacade.java
@@ -0,0 +1,42 @@
+package com.ecommerce.facade.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Audiobook;
+import com.ecommerce.bean.domain.products.Book;
+import com.ecommerce.bean.domain.products.Ebook;
+import com.ecommerce.bean.domain.products.Product;
+
+import java.util.List;
+import java.util.Set;
+
+public interface ProductFacade {
+
+ List extends Product> getAllProducts();
+
+ void addSingleBook(Book book);
+
+ Book getSingleBook(long id);
+
+ List getAllBooks();
+
+ void addSingleEbook(Ebook ebook);
+
+ Ebook getSingleEbook(long id);
+
+ List getAllEbooks();
+
+ void addSingleAudiobook(Audiobook audiobook);
+
+ Audiobook getSingleAudiobook(long id);
+
+ List getAllAudiobooks();
+
+ Set searchByPhrase(String phrase);
+
+ List> getAllAvailableTitles();
+
+ Product getSingleProduct(long id);
+
+ Product getSingleProduct(String title);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/domain/products/impl/ProductFacadeImpl.java b/backend/src/main/java/com/ecommerce/facade/domain/products/impl/ProductFacadeImpl.java
new file mode 100644
index 0000000..b987e36
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/domain/products/impl/ProductFacadeImpl.java
@@ -0,0 +1,94 @@
+package com.ecommerce.facade.domain.products.impl;
+
+
+import com.ecommerce.bean.domain.products.Audiobook;
+import com.ecommerce.bean.domain.products.Book;
+import com.ecommerce.bean.domain.products.Ebook;
+import com.ecommerce.bean.domain.products.Product;
+import com.ecommerce.facade.domain.products.ProductFacade;
+import com.ecommerce.service.domain.products.ProductService;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Set;
+
+@Component
+public class ProductFacadeImpl implements ProductFacade {
+
+ private final ProductService productService;
+
+ public ProductFacadeImpl(ProductService productService) {
+ this.productService = productService;
+ }
+
+ @Override
+ public List extends Product> getAllProducts() {
+ return productService.getAllProducts();
+ }
+
+ @Override
+ public void addSingleBook(Book book) {
+ productService.addSingleBook(book);
+ }
+
+ @Override
+ public Book getSingleBook(long id) {
+ return productService.getSingleBook(id);
+ }
+
+ @Override
+ public List getAllBooks() {
+ return productService.getAllBooks();
+ }
+
+ @Override
+ public void addSingleEbook(Ebook ebook) {
+ productService.addSingleEbook(ebook);
+ }
+
+ @Override
+ public Ebook getSingleEbook(long id) {
+ return productService.getSingleEbook(id);
+ }
+
+ @Override
+ public List getAllEbooks() {
+ return productService.getAllEbooks();
+ }
+
+ @Override
+ public void addSingleAudiobook(Audiobook audiobook) {
+ productService.addSingleAudiobook(audiobook);
+ }
+
+ @Override
+ public Audiobook getSingleAudiobook(long id) {
+ return productService.getSingleAudiobook(id);
+ }
+
+ @Override
+ public List getAllAudiobooks() {
+ return productService.getAllAudiobooks();
+ }
+
+ @Override
+ public Set searchByPhrase(String phrase) {
+ return productService.searchByPhrase(phrase);
+ }
+
+ @Override
+ public List> getAllAvailableTitles() {
+ return productService.getAllAvailableTitles();
+ }
+
+ @Override
+ public Product getSingleProduct(long id) {
+ return productService.getSingleProduct(id);
+ }
+
+ @Override
+ public Product getSingleProduct(String title) {
+ return productService.getSingleProduct(title);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/domain/transaction/OrderFacade.java b/backend/src/main/java/com/ecommerce/facade/domain/transaction/OrderFacade.java
new file mode 100644
index 0000000..322e0ff
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/domain/transaction/OrderFacade.java
@@ -0,0 +1,16 @@
+package com.ecommerce.facade.domain.transaction;
+
+
+import com.ecommerce.bean.domain.transaction.Basket;
+import com.ecommerce.bean.domain.transaction.OrderItem;
+import com.ecommerce.exception.CustomerDoesNotExist;
+import com.ecommerce.exception.TransactionCorrupted;
+
+import java.util.List;
+
+public interface OrderFacade {
+
+ void checkBasket(List basket) throws TransactionCorrupted;
+
+ void checkout(Basket basket) throws CustomerDoesNotExist;
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/domain/transaction/impl/OrderFacadeImpl.java b/backend/src/main/java/com/ecommerce/facade/domain/transaction/impl/OrderFacadeImpl.java
new file mode 100644
index 0000000..d509d20
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/domain/transaction/impl/OrderFacadeImpl.java
@@ -0,0 +1,45 @@
+package com.ecommerce.facade.domain.transaction.impl;
+
+
+import com.ecommerce.bean.domain.transaction.Basket;
+import com.ecommerce.bean.domain.transaction.OrderItem;
+import com.ecommerce.exception.CustomerDoesNotExist;
+import com.ecommerce.exception.TransactionCorrupted;
+import com.ecommerce.facade.domain.transaction.OrderFacade;
+import com.ecommerce.service.domain.products.ProductService;
+import com.ecommerce.service.domain.transaction.OrderService;
+import com.ecommerce.service.users.CustomerService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class OrderFacadeImpl implements OrderFacade {
+
+ private final OrderService orderService;
+ private final ProductService productService;
+ private final CustomerService customerService;
+
+ public OrderFacadeImpl(OrderService orderService, ProductService productService, CustomerService customerService) {
+ this.orderService = orderService;
+ this.productService = productService;
+ this.customerService = customerService;
+ }
+
+ @Override
+ public void checkBasket(List basket) throws TransactionCorrupted {
+ productService.recalculatePrice(basket);
+ }
+
+ @Override
+ public void checkout(Basket basket) throws CustomerDoesNotExist {
+ if (customerService.userExists(basket.getCustomerEmail())) {
+ orderService.checkout(basket);
+ productService.updateProducts(basket.getBasket());
+ // orderService.sendConfirmationMail();
+ } else {
+ throw new CustomerDoesNotExist("Provided customer does not exist");
+ }
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/security/AuthenticationFacade.java b/backend/src/main/java/com/ecommerce/facade/security/AuthenticationFacade.java
new file mode 100644
index 0000000..017738a
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/security/AuthenticationFacade.java
@@ -0,0 +1,11 @@
+package com.ecommerce.facade.security;
+
+import com.ecommerce.bean.security.AccountCredentials;
+
+public interface AuthenticationFacade {
+
+ boolean userExists(String email);
+
+ boolean passwordMatch(AccountCredentials accountCredentials);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/security/impl/AuthenticationFacadeImpl.java b/backend/src/main/java/com/ecommerce/facade/security/impl/AuthenticationFacadeImpl.java
new file mode 100644
index 0000000..861d4a2
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/security/impl/AuthenticationFacadeImpl.java
@@ -0,0 +1,30 @@
+package com.ecommerce.facade.security.impl;
+
+import com.ecommerce.bean.security.AccountCredentials;
+import com.ecommerce.facade.security.AuthenticationFacade;
+import com.ecommerce.service.security.AuthenticationService;
+import com.ecommerce.service.users.CustomerService;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AuthenticationFacadeImpl implements AuthenticationFacade {
+
+ private final AuthenticationService authenticationService;
+ private final CustomerService customerService;
+
+ public AuthenticationFacadeImpl(AuthenticationService authenticationService, CustomerService customerService) {
+ this.authenticationService = authenticationService;
+ this.customerService = customerService;
+ }
+
+ @Override
+ public boolean userExists(String email) {
+ return customerService.userExists(email);
+ }
+
+ @Override
+ public boolean passwordMatch(AccountCredentials accountCredentials) {
+ return authenticationService.passwordMatch(accountCredentials);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/system/SystemVariablesFacade.java b/backend/src/main/java/com/ecommerce/facade/system/SystemVariablesFacade.java
new file mode 100644
index 0000000..ac13bd4
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/system/SystemVariablesFacade.java
@@ -0,0 +1,11 @@
+package com.ecommerce.facade.system;
+
+import com.ecommerce.bean.system.APIKey;
+
+public interface SystemVariablesFacade {
+
+ APIKey getGoogleMapsAPIKey();
+
+ APIKey getPayPalAPIKey();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/system/impl/SystemVariablesFacadeImpl.java b/backend/src/main/java/com/ecommerce/facade/system/impl/SystemVariablesFacadeImpl.java
new file mode 100644
index 0000000..8ec023b
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/system/impl/SystemVariablesFacadeImpl.java
@@ -0,0 +1,27 @@
+package com.ecommerce.facade.system.impl;
+
+import com.ecommerce.bean.system.APIKey;
+import com.ecommerce.facade.system.SystemVariablesFacade;
+import com.ecommerce.service.system.SystemVariablesService;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SystemVariablesFacadeImpl implements SystemVariablesFacade {
+
+ private SystemVariablesService systemVariablesService;
+
+ public SystemVariablesFacadeImpl(SystemVariablesService systemVariablesService) {
+ this.systemVariablesService = systemVariablesService;
+ }
+
+ @Override
+ public APIKey getGoogleMapsAPIKey() {
+ return systemVariablesService.getGoogleMapsAPIKey();
+ }
+
+ @Override
+ public APIKey getPayPalAPIKey() {
+ return systemVariablesService.getPayPalAPIKey();
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/users/CustomerFacade.java b/backend/src/main/java/com/ecommerce/facade/users/CustomerFacade.java
new file mode 100644
index 0000000..a05fe8a
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/users/CustomerFacade.java
@@ -0,0 +1,14 @@
+package com.ecommerce.facade.users;
+
+
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.exception.EmailAlreadyExists;
+
+public interface CustomerFacade {
+
+ long registerCustomer(Customer customer) throws EmailAlreadyExists;
+
+ long getCustomerId(Customer customer);
+
+ long getCustomerIdByEmail(String email);
+}
diff --git a/backend/src/main/java/com/ecommerce/facade/users/impl/CustomerFacadeImpl.java b/backend/src/main/java/com/ecommerce/facade/users/impl/CustomerFacadeImpl.java
new file mode 100644
index 0000000..39dfa2c
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/facade/users/impl/CustomerFacadeImpl.java
@@ -0,0 +1,34 @@
+package com.ecommerce.facade.users.impl;
+
+
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.exception.EmailAlreadyExists;
+import com.ecommerce.facade.users.CustomerFacade;
+import com.ecommerce.service.users.CustomerService;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CustomerFacadeImpl implements CustomerFacade {
+
+ private final CustomerService customerService;
+
+ public CustomerFacadeImpl(CustomerService customerService) {
+ this.customerService = customerService;
+ }
+
+ @Override
+ public long registerCustomer(Customer customer) throws EmailAlreadyExists {
+ return customerService.registerCustomer(customer);
+ }
+
+ @Override
+ public long getCustomerId(Customer customer) {
+ return customerService.getCustomerId(customer);
+ }
+
+ @Override
+ public long getCustomerIdByEmail(String email) {
+ return customerService.getCustomerIdByEmail(email);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/AudiobookRepository.java b/backend/src/main/java/com/ecommerce/repository/domain/products/AudiobookRepository.java
new file mode 100644
index 0000000..581f0aa
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/AudiobookRepository.java
@@ -0,0 +1,16 @@
+package com.ecommerce.repository.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Audiobook;
+
+import java.util.List;
+
+public interface AudiobookRepository {
+
+ void addSingleAudiobook(Audiobook audiobook);
+
+ Audiobook getSingleAudiobook(long id);
+
+ List getAllAudiobooks();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/BookRepository.java b/backend/src/main/java/com/ecommerce/repository/domain/products/BookRepository.java
new file mode 100644
index 0000000..6e20b98
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/BookRepository.java
@@ -0,0 +1,16 @@
+package com.ecommerce.repository.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Book;
+
+import java.util.List;
+
+public interface BookRepository {
+
+ void addSingleBook(Book book);
+
+ Book getSingleBook(long id);
+
+ List getAllBooks();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/EbookRepository.java b/backend/src/main/java/com/ecommerce/repository/domain/products/EbookRepository.java
new file mode 100644
index 0000000..625d4a1
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/EbookRepository.java
@@ -0,0 +1,16 @@
+package com.ecommerce.repository.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Ebook;
+
+import java.util.List;
+
+public interface EbookRepository {
+
+ void addSingleEbook(Ebook ebook);
+
+ Ebook getSingleEbook(long id);
+
+ List getAllEbooks();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/ProductRepository.java b/backend/src/main/java/com/ecommerce/repository/domain/products/ProductRepository.java
new file mode 100644
index 0000000..be34e6a
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/ProductRepository.java
@@ -0,0 +1,28 @@
+package com.ecommerce.repository.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Product;
+import com.ecommerce.bean.domain.transaction.OrderItem;
+import com.ecommerce.exception.TransactionCorrupted;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Set;
+
+public interface ProductRepository {
+
+ List getAllProducts();
+
+ BigDecimal recalculatePrice(List orderItems) throws TransactionCorrupted;
+
+ void updateProducts(Set basket);
+
+ Set searchByPhrase(String phrase);
+
+ List> getAllAvailableTitles();
+
+ Product getSingleProduct(long id);
+
+ Product getSingleProduct(String title);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/impl/AudiobookRepositoryImpl.java b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/AudiobookRepositoryImpl.java
new file mode 100644
index 0000000..a24585c
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/AudiobookRepositoryImpl.java
@@ -0,0 +1,53 @@
+package com.ecommerce.repository.domain.products.impl;
+
+import com.ecommerce.bean.domain.products.Audiobook;
+import com.ecommerce.repository.domain.products.AudiobookRepository;
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.hibernate.criterion.Restrictions;
+import org.springframework.stereotype.Repository;
+
+import javax.transaction.Transactional;
+import java.util.List;
+
+@Repository
+@Transactional
+public class AudiobookRepositoryImpl implements AudiobookRepository {
+
+ private final SessionFactoryWrapper sessionFactoryWrapper;
+
+ public AudiobookRepositoryImpl(SessionFactoryWrapper sessionFactoryWrapper) {
+ this.sessionFactoryWrapper = sessionFactoryWrapper;
+ }
+
+ @Override
+ public void addSingleAudiobook(Audiobook audiobook) {
+ Session session = sessionFactoryWrapper.openSession();
+ Transaction transaction = session.beginTransaction();
+ session.persist(audiobook);
+ transaction.commit();
+ sessionFactoryWrapper.closeSession();
+ }
+
+ @Override
+ public Audiobook getSingleAudiobook(long id) {
+ Session session = sessionFactoryWrapper.openSession();
+ Audiobook audiobook = (Audiobook) session.get(Audiobook.class, id);
+ sessionFactoryWrapper.closeSession();
+
+ return audiobook;
+ }
+
+ @Override
+ public List getAllAudiobooks() {
+ Session session = sessionFactoryWrapper.openSession();
+ List audiobooks = session.createCriteria(Audiobook.class)
+ .add(Restrictions.between("quantity", 1, 100))
+ .list();
+ sessionFactoryWrapper.closeSession();
+
+ return audiobooks;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/impl/BookRepositoryImpl.java b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/BookRepositoryImpl.java
new file mode 100644
index 0000000..79a54be
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/BookRepositoryImpl.java
@@ -0,0 +1,54 @@
+package com.ecommerce.repository.domain.products.impl;
+
+
+import com.ecommerce.bean.domain.products.Book;
+import com.ecommerce.repository.domain.products.BookRepository;
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.hibernate.criterion.Restrictions;
+import org.springframework.stereotype.Repository;
+
+import javax.transaction.Transactional;
+import java.util.List;
+
+@Repository
+@Transactional
+public class BookRepositoryImpl implements BookRepository {
+
+ private final SessionFactoryWrapper sessionFactoryWrapper;
+
+ public BookRepositoryImpl(SessionFactoryWrapper sessionFactoryWrapper) {
+ this.sessionFactoryWrapper = sessionFactoryWrapper;
+ }
+
+ @Override
+ public void addSingleBook(Book book) {
+ Session session = sessionFactoryWrapper.openSession();
+ Transaction transaction = session.beginTransaction();
+ session.persist(book);
+ transaction.commit();
+ sessionFactoryWrapper.closeSession();
+ }
+
+ @Override
+ public Book getSingleBook(long id) {
+ Session session = sessionFactoryWrapper.openSession();
+ Book book = (Book) session.get(Book.class, id);
+ sessionFactoryWrapper.closeSession();
+
+ return book;
+ }
+
+ @Override
+ public List getAllBooks() {
+ Session session = sessionFactoryWrapper.openSession();
+ List books = session.createCriteria(Book.class)
+ .add(Restrictions.between("quantity", 1, 100))
+ .list();
+ sessionFactoryWrapper.closeSession();
+
+ return books;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/impl/EbookRepositoryImpl.java b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/EbookRepositoryImpl.java
new file mode 100644
index 0000000..1c54604
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/EbookRepositoryImpl.java
@@ -0,0 +1,53 @@
+package com.ecommerce.repository.domain.products.impl;
+
+import com.ecommerce.bean.domain.products.Ebook;
+import com.ecommerce.repository.domain.products.EbookRepository;
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.hibernate.criterion.Restrictions;
+import org.springframework.stereotype.Repository;
+
+import javax.transaction.Transactional;
+import java.util.List;
+
+@Repository
+@Transactional
+public class EbookRepositoryImpl implements EbookRepository {
+
+ private final SessionFactoryWrapper sessionFactoryWrapper;
+
+ public EbookRepositoryImpl(SessionFactoryWrapper sessionFactoryWrapper) {
+ this.sessionFactoryWrapper = sessionFactoryWrapper;
+ }
+
+ @Override
+ public void addSingleEbook(Ebook ebook) {
+ Session session = sessionFactoryWrapper.openSession();
+ Transaction transaction = session.beginTransaction();
+ session.persist(ebook);
+ transaction.commit();
+ sessionFactoryWrapper.closeSession();
+ }
+
+ @Override
+ public Ebook getSingleEbook(long id) {
+ Session session = sessionFactoryWrapper.openSession();
+ Ebook ebook = (Ebook) session.get(Ebook.class, id);
+ sessionFactoryWrapper.closeSession();
+
+ return ebook;
+ }
+
+ @Override
+ public List getAllEbooks() {
+ Session session = sessionFactoryWrapper.openSession();
+ List ebooks = session.createCriteria(Ebook.class)
+ .add(Restrictions.between("quantity", 1, 100))
+ .list();
+ sessionFactoryWrapper.closeSession();
+
+ return ebooks;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/products/impl/ProductRepositoryImpl.java b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/ProductRepositoryImpl.java
new file mode 100644
index 0000000..cbb95f5
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/products/impl/ProductRepositoryImpl.java
@@ -0,0 +1,106 @@
+package com.ecommerce.repository.domain.products.impl;
+
+import com.ecommerce.bean.domain.products.Product;
+import com.ecommerce.bean.domain.transaction.OrderItem;
+import com.ecommerce.exception.TransactionCorrupted;
+import com.ecommerce.repository.domain.products.ProductRepository;
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.Criteria;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.hibernate.criterion.Projections;
+import org.hibernate.criterion.Restrictions;
+import org.springframework.stereotype.Repository;
+
+import javax.transaction.Transactional;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Set;
+
+@Repository
+@Transactional
+public class ProductRepositoryImpl implements ProductRepository {
+
+ private final SessionFactoryWrapper sessionFactoryWrapper;
+
+ public ProductRepositoryImpl(SessionFactoryWrapper sessionFactoryWrapper) {
+ this.sessionFactoryWrapper = sessionFactoryWrapper;
+ }
+
+ @Override
+ public List getAllProducts() {
+ Session session = sessionFactoryWrapper.openSession();
+ List products = session.createCriteria(Product.class)
+ .add(Restrictions.between("quantity", 1, 100))
+ .list();
+ sessionFactoryWrapper.closeSession();
+
+ return products;
+ }
+
+ @Override
+ public BigDecimal recalculatePrice(List orderItems) throws TransactionCorrupted {
+ BigDecimal totalPrice = new BigDecimal(0);
+ for (OrderItem orderItem : orderItems) {
+ Product product = getSingleProduct(orderItem.getProductId());
+ if (product == null) {
+ throw new TransactionCorrupted("Transaction was corrupted");
+ }
+ totalPrice = totalPrice.add(product.getPrice());
+ }
+
+ return totalPrice;
+ }
+
+ @Override
+ public void updateProducts(Set basket) {
+ Session session = sessionFactoryWrapper.openSession();
+ Transaction transaction = session.beginTransaction();
+ for (OrderItem orderItem : basket) {
+ Product product = (Product) session.get(Product.class, orderItem.getProductId());
+ int newQuantity = product.getQuantity() - orderItem.getQuantity();
+ product.setQuantity(newQuantity);
+ session.persist(product);
+ }
+ transaction.commit();
+ sessionFactoryWrapper.closeSession();
+ }
+
+
+ @Override
+ public Product getSingleProduct(long id) {
+ Session session = sessionFactoryWrapper.openSession();
+ Product product = (Product) session.get(Product.class, id);
+ sessionFactoryWrapper.closeSession();
+
+ return product;
+ }
+
+ @Override
+ public Product getSingleProduct(String title) {
+ Session session = sessionFactoryWrapper.openSession();
+ Criteria criteria = session.createCriteria(Product.class);
+ criteria.add(Restrictions.eq("title", title));
+
+ return (Product) criteria.uniqueResult();
+ }
+
+ @Override
+ public Set searchByPhrase(String phrase) {
+
+ return null;
+ }
+
+ @Override
+ public List> getAllAvailableTitles() {
+ Session session = sessionFactoryWrapper.openSession();
+ Criteria cr = session.createCriteria(Product.class)
+ .setProjection(Projections.projectionList()
+ .add(Projections.property("title"), "title"));
+ List list = cr.list();
+ sessionFactoryWrapper.closeSession();
+
+ return list;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/transaction/OrderRepository.java b/backend/src/main/java/com/ecommerce/repository/domain/transaction/OrderRepository.java
new file mode 100644
index 0000000..c208caa
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/transaction/OrderRepository.java
@@ -0,0 +1,10 @@
+package com.ecommerce.repository.domain.transaction;
+
+
+import com.ecommerce.bean.domain.transaction.Basket;
+
+public interface OrderRepository {
+
+ void checkout(Basket basket);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/domain/transaction/impl/OrderRepositoryImpl.java b/backend/src/main/java/com/ecommerce/repository/domain/transaction/impl/OrderRepositoryImpl.java
new file mode 100644
index 0000000..fd3e1b7
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/domain/transaction/impl/OrderRepositoryImpl.java
@@ -0,0 +1,37 @@
+package com.ecommerce.repository.domain.transaction.impl;
+
+
+import com.ecommerce.bean.domain.transaction.Basket;
+import com.ecommerce.repository.domain.transaction.OrderRepository;
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.HibernateError;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+@Repository
+@Transactional
+public class OrderRepositoryImpl implements OrderRepository {
+
+ private final SessionFactoryWrapper sessionFactoryWrapper;
+
+ public OrderRepositoryImpl(SessionFactoryWrapper sessionFactoryWrapper) {
+ this.sessionFactoryWrapper = sessionFactoryWrapper;
+ }
+
+ @Override
+ public void checkout(Basket basket) {
+ Session session = sessionFactoryWrapper.openSession();
+ Transaction transaction = session.beginTransaction();
+ try {
+ session.save(basket);
+ transaction.commit();
+ } catch (HibernateError error) {
+ transaction.rollback();
+ } finally {
+ sessionFactoryWrapper.closeSession();
+ }
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/security/AuthenticationRepository.java b/backend/src/main/java/com/ecommerce/repository/security/AuthenticationRepository.java
new file mode 100644
index 0000000..2f3f695
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/security/AuthenticationRepository.java
@@ -0,0 +1,9 @@
+package com.ecommerce.repository.security;
+
+import com.ecommerce.bean.security.AccountCredentials;
+
+public interface AuthenticationRepository {
+
+ boolean passwordMatch(AccountCredentials accountCredentials);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/security/impl/AuthenticationRepositoryImpl.java b/backend/src/main/java/com/ecommerce/repository/security/impl/AuthenticationRepositoryImpl.java
new file mode 100644
index 0000000..5600af1
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/security/impl/AuthenticationRepositoryImpl.java
@@ -0,0 +1,35 @@
+package com.ecommerce.repository.security.impl;
+
+import com.ecommerce.bean.security.AccountCredentials;
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.repository.security.AuthenticationRepository;
+import com.ecommerce.tools.cryptography.PasswordEncoderGenerator;
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.Query;
+import org.hibernate.Session;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class AuthenticationRepositoryImpl implements AuthenticationRepository {
+
+ private final SessionFactoryWrapper sessionFactoryWrapper;
+ private final PasswordEncoderGenerator passwordEncoderGenerator;
+
+ public AuthenticationRepositoryImpl(SessionFactoryWrapper sessionFactoryWrapper,
+ PasswordEncoderGenerator passwordEncoderGenerator) {
+ this.sessionFactoryWrapper = sessionFactoryWrapper;
+ this.passwordEncoderGenerator = passwordEncoderGenerator;
+ }
+
+ @Override
+ public boolean passwordMatch(AccountCredentials accountCredentials) {
+ Session session = sessionFactoryWrapper.openSession();
+ Query query = session.createQuery("SELECT password FROM Customer s WHERE s.email=:email");
+ query.setParameter("email", accountCredentials.getEmail());
+ String password = (String) query.uniqueResult();
+ sessionFactoryWrapper.closeSession();
+
+ return passwordEncoderGenerator.passwordMatches(accountCredentials.getPassword(), password);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/users/CustomerRepository.java b/backend/src/main/java/com/ecommerce/repository/users/CustomerRepository.java
new file mode 100644
index 0000000..1122f09
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/users/CustomerRepository.java
@@ -0,0 +1,23 @@
+package com.ecommerce.repository.users;
+
+
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.exception.EmailAlreadyExists;
+
+public interface CustomerRepository {
+
+ long registerCustomer(Customer customer) throws EmailAlreadyExists;
+
+ Customer getUserByEmail(String email);
+
+ long getCustomerIdByEmail(String email);
+
+ boolean userExists(String email);
+
+ boolean emailExists(String email);
+
+ boolean userExists(long customerId);
+
+ long getCustomerId(Customer customer);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/repository/users/impl/CustomerRepositoryImpl.java b/backend/src/main/java/com/ecommerce/repository/users/impl/CustomerRepositoryImpl.java
new file mode 100644
index 0000000..7c07e17
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/repository/users/impl/CustomerRepositoryImpl.java
@@ -0,0 +1,106 @@
+package com.ecommerce.repository.users.impl;
+
+
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.bean.security.AccountCredentials;
+import com.ecommerce.bean.users.User;
+import com.ecommerce.exception.EmailAlreadyExists;
+import com.ecommerce.repository.users.CustomerRepository;
+import com.ecommerce.tools.cryptography.PasswordEncoderGenerator;
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.springframework.stereotype.Repository;
+
+import java.io.Serializable;
+
+@Repository
+public class CustomerRepositoryImpl implements CustomerRepository {
+
+ private final SessionFactoryWrapper sessionFactoryWrapper;
+ private final PasswordEncoderGenerator passwordEncoderGenerator;
+
+ public CustomerRepositoryImpl(SessionFactoryWrapper sessionFactoryWrapper,
+ PasswordEncoderGenerator passwordEncoderGenerator) {
+ this.sessionFactoryWrapper = sessionFactoryWrapper;
+ this.passwordEncoderGenerator = passwordEncoderGenerator;
+ }
+
+ @Override
+ public long registerCustomer(Customer customer) throws EmailAlreadyExists {
+ if (!emailExists(customer.getEmail())) {
+ AccountCredentials accountCredentials = new AccountCredentials(customer.getEmail(), customer.getPassword());
+ String encodedPassword = passwordEncoderGenerator.encodePassword(accountCredentials.getPassword());
+ customer.setPassword(encodedPassword);
+ Session session = sessionFactoryWrapper.openSession();
+ Transaction transaction = session.beginTransaction();
+ long customerId = (long) session.save(customer);
+ transaction.commit();
+ sessionFactoryWrapper.closeSession();
+
+ return customerId;
+ } else {
+ throw new EmailAlreadyExists("Provided email was already used");
+ }
+ }
+
+ @Override
+ public Customer getUserByEmail(String email) {
+ Session session = sessionFactoryWrapper.openSession();
+ Customer customerFound = (Customer) session.get(Customer.class, email);
+ sessionFactoryWrapper.closeSession();
+
+ return customerFound;
+ }
+
+ @Override
+ public long getCustomerId(Customer customer) {
+ Session session = sessionFactoryWrapper.openSession();
+ User userFound = (User) session.get(User.class, (Serializable) customer);
+ sessionFactoryWrapper.closeSession();
+
+ return userFound.getId();
+ }
+
+ @Override
+ public long getCustomerIdByEmail(String email) {
+ String hql = "SELECT customer.id AS customerId" +
+ "FROM Customer customer " +
+ "WHERE customer.email =:email";
+ Session session = sessionFactoryWrapper.openSession();
+ long customerId = (long) session.createQuery(hql).setParameter("email", email).uniqueResult();
+ sessionFactoryWrapper.closeSession();
+
+ return customerId;
+ }
+
+ @Override
+ public boolean userExists(String email) {
+ return emailExists(email);
+ }
+
+ @Override
+ public boolean userExists(long customerId) {
+ return customerIdExists(customerId);
+ }
+
+ @Override
+ public boolean emailExists(String email) {
+ Session session = sessionFactoryWrapper.openSession();
+ String hql = "SELECT COUNT(*) AS cnt " +
+ "FROM Customer customer " +
+ "WHERE customer.email =:email";
+ Long emailNum = (Long) session.createQuery(hql).setParameter("email", email).uniqueResult();
+ sessionFactoryWrapper.closeSession();
+ return emailNum > 0;
+ }
+
+ private boolean customerIdExists(long customerId) {
+ Session session = sessionFactoryWrapper.openSession();
+ Customer customer = (Customer) session.get(User.class, customerId);
+ sessionFactoryWrapper.closeSession();
+
+ return customer != null;
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/domain/products/ProductService.java b/backend/src/main/java/com/ecommerce/service/domain/products/ProductService.java
new file mode 100644
index 0000000..3c84fd2
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/domain/products/ProductService.java
@@ -0,0 +1,50 @@
+package com.ecommerce.service.domain.products;
+
+
+import com.ecommerce.bean.domain.products.Audiobook;
+import com.ecommerce.bean.domain.products.Book;
+import com.ecommerce.bean.domain.products.Ebook;
+import com.ecommerce.bean.domain.products.Product;
+import com.ecommerce.bean.domain.transaction.OrderItem;
+import com.ecommerce.exception.TransactionCorrupted;
+
+import java.util.List;
+import java.util.Set;
+
+public interface ProductService {
+
+ List extends Product> getAllProducts();
+
+ void addSingleBook(Book book);
+
+ Book getSingleBook(Long id);
+
+ List getAllBooks();
+
+ void addSingleEbook(Ebook ebook);
+
+ Ebook getSingleEbook(long id);
+
+ List getAllEbooks();
+
+ void addSingleAudiobook(Audiobook audiobook);
+
+ Audiobook getSingleAudiobook(long id);
+
+ List getAllAudiobooks();
+
+ void recalculatePrice(List orderItems) throws TransactionCorrupted;
+
+ void updateProducts(Set basket);
+
+ Product getProductById(long productId);
+
+ Set searchByPhrase(String phrase);
+
+ List> getAllAvailableTitles();
+
+ Product getSingleProduct(long id);
+
+ Product getSingleProduct(String title);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/domain/products/impl/ProductServiceImpl.java b/backend/src/main/java/com/ecommerce/service/domain/products/impl/ProductServiceImpl.java
new file mode 100644
index 0000000..79a8d8d
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/domain/products/impl/ProductServiceImpl.java
@@ -0,0 +1,123 @@
+package com.ecommerce.service.domain.products.impl;
+
+
+import com.ecommerce.bean.domain.products.Audiobook;
+import com.ecommerce.bean.domain.products.Book;
+import com.ecommerce.bean.domain.products.Ebook;
+import com.ecommerce.bean.domain.products.Product;
+import com.ecommerce.bean.domain.transaction.OrderItem;
+import com.ecommerce.exception.TransactionCorrupted;
+import com.ecommerce.repository.domain.products.AudiobookRepository;
+import com.ecommerce.repository.domain.products.BookRepository;
+import com.ecommerce.repository.domain.products.EbookRepository;
+import com.ecommerce.repository.domain.products.ProductRepository;
+import com.ecommerce.service.domain.products.ProductService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Set;
+
+@Service
+public class ProductServiceImpl implements ProductService {
+
+ private final ProductRepository productRepository;
+ private final BookRepository bookRepository;
+ private final EbookRepository ebookRepository;
+ private final AudiobookRepository audiobookRepository;
+
+ public ProductServiceImpl(ProductRepository productRepository,
+ BookRepository bookRepository,
+ EbookRepository ebookRepository,
+ AudiobookRepository audiobookRepository) {
+ this.productRepository = productRepository;
+ this.bookRepository = bookRepository;
+ this.ebookRepository = ebookRepository;
+ this.audiobookRepository = audiobookRepository;
+ }
+
+ @Override
+ public List extends Product> getAllProducts() {
+ return productRepository.getAllProducts();
+ }
+
+ @Override
+ public void addSingleBook(Book book) {
+ bookRepository.addSingleBook(book);
+ }
+
+ @Override
+ public Book getSingleBook(Long id) {
+ return bookRepository.getSingleBook(id);
+ }
+
+ @Override
+ public List getAllBooks() {
+ return bookRepository.getAllBooks();
+ }
+
+ @Override
+ public void addSingleEbook(Ebook ebook) {
+ ebookRepository.addSingleEbook(ebook);
+ }
+
+ @Override
+ public Ebook getSingleEbook(long id) {
+ return ebookRepository.getSingleEbook(id);
+ }
+
+ @Override
+ public List getAllEbooks() {
+ return ebookRepository.getAllEbooks();
+ }
+
+ @Override
+ public void addSingleAudiobook(Audiobook audiobook) {
+ audiobookRepository.addSingleAudiobook(audiobook);
+ }
+
+ @Override
+ public Audiobook getSingleAudiobook(long id) {
+ return audiobookRepository.getSingleAudiobook(id);
+ }
+
+ @Override
+ public List getAllAudiobooks() {
+ return audiobookRepository.getAllAudiobooks();
+ }
+
+ @Override
+ public void recalculatePrice(List orderItems) throws TransactionCorrupted {
+ productRepository.recalculatePrice(orderItems);
+ }
+
+ @Override
+ public void updateProducts(Set basket) {
+ productRepository.updateProducts(basket);
+ }
+
+ @Override
+ public Product getProductById(long productId) {
+ return productRepository.getSingleProduct(productId);
+ }
+
+ @Override
+ public Set searchByPhrase(String phrase) {
+ return productRepository.searchByPhrase(phrase);
+ }
+
+ @Override
+ public List> getAllAvailableTitles() {
+ return productRepository.getAllAvailableTitles();
+ }
+
+ @Override
+ public Product getSingleProduct(long id) {
+ return productRepository.getSingleProduct(id);
+ }
+
+ @Override
+ public Product getSingleProduct(String title) {
+ return productRepository.getSingleProduct(title);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/domain/transaction/OrderService.java b/backend/src/main/java/com/ecommerce/service/domain/transaction/OrderService.java
new file mode 100644
index 0000000..a00d5bd
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/domain/transaction/OrderService.java
@@ -0,0 +1,15 @@
+package com.ecommerce.service.domain.transaction;
+
+
+import com.ecommerce.bean.domain.transaction.Basket;
+
+import javax.jws.Oneway;
+
+public interface OrderService {
+
+ void checkout(Basket basket);
+
+ @Oneway
+ void sendConfirmationMail();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/domain/transaction/impl/OrderServiceImpl.java b/backend/src/main/java/com/ecommerce/service/domain/transaction/impl/OrderServiceImpl.java
new file mode 100644
index 0000000..29f0e9b
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/domain/transaction/impl/OrderServiceImpl.java
@@ -0,0 +1,33 @@
+package com.ecommerce.service.domain.transaction.impl;
+
+
+import com.ecommerce.bean.domain.transaction.Basket;
+import com.ecommerce.repository.domain.transaction.OrderRepository;
+import com.ecommerce.service.domain.transaction.OrderService;
+import com.ecommerce.tools.communication_manager.CommunicationManager;
+import org.springframework.stereotype.Service;
+
+import javax.jws.Oneway;
+
+@Service
+public class OrderServiceImpl implements OrderService {
+
+ private final OrderRepository orderRepository;
+ private final CommunicationManager communicationManager;
+
+ public OrderServiceImpl(OrderRepository orderRepository, CommunicationManager communicationManager) {
+ this.orderRepository = orderRepository;
+ this.communicationManager = communicationManager;
+ }
+
+ @Override
+ public void checkout(Basket basket) {
+ orderRepository.checkout(basket);
+ }
+
+ @Override
+ @Oneway
+ public void sendConfirmationMail() {
+ communicationManager.sendConfirmationMail();
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/service/security/AuthenticationService.java b/backend/src/main/java/com/ecommerce/service/security/AuthenticationService.java
new file mode 100644
index 0000000..0cf2ae7
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/security/AuthenticationService.java
@@ -0,0 +1,9 @@
+package com.ecommerce.service.security;
+
+import com.ecommerce.bean.security.AccountCredentials;
+
+public interface AuthenticationService {
+
+ boolean passwordMatch(AccountCredentials accountCredentials);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/security/impl/AuthenticationServiceImpl.java b/backend/src/main/java/com/ecommerce/service/security/impl/AuthenticationServiceImpl.java
new file mode 100644
index 0000000..ef45804
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/security/impl/AuthenticationServiceImpl.java
@@ -0,0 +1,22 @@
+package com.ecommerce.service.security.impl;
+
+import com.ecommerce.bean.security.AccountCredentials;
+import com.ecommerce.repository.security.AuthenticationRepository;
+import com.ecommerce.service.security.AuthenticationService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AuthenticationServiceImpl implements AuthenticationService {
+
+ private final AuthenticationRepository authenticationRepository;
+
+ public AuthenticationServiceImpl(AuthenticationRepository authenticationRepository) {
+ this.authenticationRepository = authenticationRepository;
+ }
+
+ @Override
+ public boolean passwordMatch(AccountCredentials accountCredentials) {
+ return authenticationRepository.passwordMatch(accountCredentials);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/system/SystemVariablesService.java b/backend/src/main/java/com/ecommerce/service/system/SystemVariablesService.java
new file mode 100644
index 0000000..2c541d6
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/system/SystemVariablesService.java
@@ -0,0 +1,11 @@
+package com.ecommerce.service.system;
+
+import com.ecommerce.bean.system.APIKey;
+
+public interface SystemVariablesService {
+
+ APIKey getGoogleMapsAPIKey();
+
+ APIKey getPayPalAPIKey();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/system/impl/SystemVariablesServiceImpl.java b/backend/src/main/java/com/ecommerce/service/system/impl/SystemVariablesServiceImpl.java
new file mode 100644
index 0000000..6a9051d
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/system/impl/SystemVariablesServiceImpl.java
@@ -0,0 +1,27 @@
+package com.ecommerce.service.system.impl;
+
+import com.ecommerce.bean.system.APIKey;
+import com.ecommerce.service.system.SystemVariablesService;
+import com.ecommerce.tools.api_keys_retriever.APIKeysRetriever;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SystemVariablesServiceImpl implements SystemVariablesService {
+
+ private final APIKeysRetriever apiKeysRetriever;
+
+ public SystemVariablesServiceImpl(APIKeysRetriever apiKeysRetriever) {
+ this.apiKeysRetriever = apiKeysRetriever;
+ }
+
+ @Override
+ public APIKey getGoogleMapsAPIKey() {
+ return apiKeysRetriever.getGoogleMapsAPIKey();
+ }
+
+ @Override
+ public APIKey getPayPalAPIKey() {
+ return apiKeysRetriever.getPayPalAPIKey();
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/service/users/CustomerService.java b/backend/src/main/java/com/ecommerce/service/users/CustomerService.java
new file mode 100644
index 0000000..e0489e0
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/users/CustomerService.java
@@ -0,0 +1,18 @@
+package com.ecommerce.service.users;
+
+
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.exception.EmailAlreadyExists;
+
+public interface CustomerService {
+
+ long registerCustomer(Customer customer) throws EmailAlreadyExists;
+
+ boolean userExists(String email);
+
+ boolean userExists(long customerId);
+
+ long getCustomerId(Customer customer);
+
+ long getCustomerIdByEmail(String email);
+}
diff --git a/backend/src/main/java/com/ecommerce/service/users/impl/CustomerServiceImpl.java b/backend/src/main/java/com/ecommerce/service/users/impl/CustomerServiceImpl.java
new file mode 100644
index 0000000..5d72f89
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/service/users/impl/CustomerServiceImpl.java
@@ -0,0 +1,43 @@
+package com.ecommerce.service.users.impl;
+
+
+import com.ecommerce.bean.users.Customer;
+import com.ecommerce.exception.EmailAlreadyExists;
+import com.ecommerce.repository.users.CustomerRepository;
+import com.ecommerce.service.users.CustomerService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CustomerServiceImpl implements CustomerService {
+
+ private final CustomerRepository customerRepository;
+
+ public CustomerServiceImpl(CustomerRepository customerRepository) {
+ this.customerRepository = customerRepository;
+ }
+
+ @Override
+ public long registerCustomer(Customer customer) throws EmailAlreadyExists {
+ return customerRepository.registerCustomer(customer);
+ }
+
+ @Override
+ public boolean userExists(String email) {
+ return customerRepository.userExists(email);
+ }
+
+ @Override
+ public boolean userExists(long customerId) {
+ return customerRepository.userExists(customerId);
+ }
+
+ @Override
+ public long getCustomerId(Customer customer) {
+ return customerRepository.getCustomerId(customer);
+ }
+
+ @Override
+ public long getCustomerIdByEmail(String email) {
+ return customerRepository.getCustomerIdByEmail(email);
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/api_keys_retriever/APIKeysRetriever.java b/backend/src/main/java/com/ecommerce/tools/api_keys_retriever/APIKeysRetriever.java
new file mode 100644
index 0000000..fd1882a
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/api_keys_retriever/APIKeysRetriever.java
@@ -0,0 +1,11 @@
+package com.ecommerce.tools.api_keys_retriever;
+
+import com.ecommerce.bean.system.APIKey;
+
+public interface APIKeysRetriever {
+
+ APIKey getGoogleMapsAPIKey();
+
+ APIKey getPayPalAPIKey();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/api_keys_retriever/impl/APIKeysRetrieverImpl.java b/backend/src/main/java/com/ecommerce/tools/api_keys_retriever/impl/APIKeysRetrieverImpl.java
new file mode 100644
index 0000000..b3dc328
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/api_keys_retriever/impl/APIKeysRetrieverImpl.java
@@ -0,0 +1,26 @@
+package com.ecommerce.tools.api_keys_retriever.impl;
+
+import com.ecommerce.bean.system.APIKey;
+import com.ecommerce.tools.api_keys_retriever.APIKeysRetriever;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class APIKeysRetrieverImpl implements APIKeysRetriever {
+
+ @Value("${google.maps.api.key}")
+ private String googleMapsAPIKey;
+
+ @Value("${paypal.api.key}")
+ private String payPalAPIKey;
+
+ @Override
+ public APIKey getGoogleMapsAPIKey() {
+ return new APIKey(googleMapsAPIKey);
+ }
+
+ @Override
+ public APIKey getPayPalAPIKey() {
+ return new APIKey(payPalAPIKey);
+ }
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/communication_manager/CommunicationManager.java b/backend/src/main/java/com/ecommerce/tools/communication_manager/CommunicationManager.java
new file mode 100644
index 0000000..3bcdf79
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/communication_manager/CommunicationManager.java
@@ -0,0 +1,11 @@
+package com.ecommerce.tools.communication_manager;
+
+
+import javax.jws.Oneway;
+
+public interface CommunicationManager {
+
+ @Oneway
+ void sendConfirmationMail();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/communication_manager/impl/CommunicationManagerImpl.java b/backend/src/main/java/com/ecommerce/tools/communication_manager/impl/CommunicationManagerImpl.java
new file mode 100644
index 0000000..2896608
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/communication_manager/impl/CommunicationManagerImpl.java
@@ -0,0 +1,44 @@
+package com.ecommerce.tools.communication_manager.impl;
+
+
+import com.ecommerce.tools.communication_manager.CommunicationManager;
+import com.google.common.collect.Lists;
+import it.ozimov.springboot.mail.configuration.EnableEmailTools;
+import it.ozimov.springboot.mail.model.Email;
+import it.ozimov.springboot.mail.model.defaultimpl.DefaultEmail;
+import it.ozimov.springboot.mail.service.EmailService;
+import org.springframework.stereotype.Component;
+
+import javax.jws.Oneway;
+import javax.mail.internet.InternetAddress;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+@Component
+@EnableEmailTools
+public class CommunicationManagerImpl implements CommunicationManager {
+
+ private final EmailService emailService;
+
+ public CommunicationManagerImpl(EmailService emailService) {
+ this.emailService = emailService;
+ }
+
+ @Override
+ @Oneway
+ public void sendConfirmationMail() {
+ final Email email;
+ try {
+ email = DefaultEmail.builder()
+ .from(new InternetAddress("Bookify", "Bookify - your web book store"))
+ .to(Lists.newArrayList(new InternetAddress("nix.noxi379@gmail.com", "Dear customer")))
+ .subject("Your order has been placed")
+ .body("Firmamentum autem stabilitatis constantiaeque eius, quam in amicitia quaerimus, fides est.")
+ .encoding(String.valueOf(Charset.forName("UTF-8"))).build();
+ emailService.send(email);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/cryptography/PasswordEncoderGenerator.java b/backend/src/main/java/com/ecommerce/tools/cryptography/PasswordEncoderGenerator.java
new file mode 100644
index 0000000..0ac1246
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/cryptography/PasswordEncoderGenerator.java
@@ -0,0 +1,10 @@
+package com.ecommerce.tools.cryptography;
+
+
+public interface PasswordEncoderGenerator {
+
+ String encodePassword(String password);
+
+ boolean passwordMatches(CharSequence rawPassword, String encodedPassword);
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/cryptography/impl/PasswordEncoderGeneratorImpl.java b/backend/src/main/java/com/ecommerce/tools/cryptography/impl/PasswordEncoderGeneratorImpl.java
new file mode 100644
index 0000000..d28aa09
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/cryptography/impl/PasswordEncoderGeneratorImpl.java
@@ -0,0 +1,33 @@
+package com.ecommerce.tools.cryptography.impl;
+
+
+import com.ecommerce.tools.cryptography.PasswordEncoderGenerator;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PasswordEncoderGeneratorImpl implements PasswordEncoderGenerator {
+
+ private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(8);
+
+ public PasswordEncoderGeneratorImpl() {
+ super();
+ }
+
+ public String encodePassword(String password) {
+ String encodedPassword = password;
+ int i = 0;
+ while (i < 12) {
+ encodedPassword = passwordEncoder.encode(password);
+ ++i;
+ }
+
+ return encodedPassword;
+ }
+
+ public boolean passwordMatches(CharSequence rawPassword, String encodedPassword) {
+
+ return passwordEncoder.matches(rawPassword, encodedPassword);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/jpa_helper/SessionFactoryWrapper.java b/backend/src/main/java/com/ecommerce/tools/jpa_helper/SessionFactoryWrapper.java
new file mode 100644
index 0000000..b052739
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/jpa_helper/SessionFactoryWrapper.java
@@ -0,0 +1,15 @@
+package com.ecommerce.tools.jpa_helper;
+
+
+import org.hibernate.Session;
+
+import javax.jws.Oneway;
+
+public interface SessionFactoryWrapper {
+
+ Session openSession();
+
+ @Oneway
+ void closeSession();
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/jpa_helper/impl/SessionFactoryWrapperImpl.java b/backend/src/main/java/com/ecommerce/tools/jpa_helper/impl/SessionFactoryWrapperImpl.java
new file mode 100644
index 0000000..4a8a38b
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/jpa_helper/impl/SessionFactoryWrapperImpl.java
@@ -0,0 +1,39 @@
+package com.ecommerce.tools.jpa_helper.impl;
+
+
+import com.ecommerce.tools.jpa_helper.SessionFactoryWrapper;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.springframework.stereotype.Component;
+
+import javax.jws.Oneway;
+
+@Component
+public class SessionFactoryWrapperImpl implements SessionFactoryWrapper {
+
+ private final SessionFactory _sessionFactory;
+ private Session session;
+
+ public SessionFactoryWrapperImpl(SessionFactory _sessionFactory) {
+ this._sessionFactory = _sessionFactory;
+ }
+
+ @Override
+ public Session openSession() {
+ if (this.session == null) {
+ this.session = _sessionFactory.openSession();
+ }
+
+ return this.session;
+ }
+
+ @Oneway
+ public void closeSession() {
+ if (this.session != null) {
+ this.session.clear();
+ this.session.close();
+ this.session = null;
+ }
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/security/JwtFilter.java b/backend/src/main/java/com/ecommerce/tools/security/JwtFilter.java
new file mode 100644
index 0000000..56e06da
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/security/JwtFilter.java
@@ -0,0 +1,40 @@
+package com.ecommerce.tools.security;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureException;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+public class JwtFilter extends GenericFilterBean {
+
+ @Override
+ public void doFilter(final ServletRequest req,
+ final ServletResponse res,
+ final FilterChain chain) throws IOException, ServletException {
+ final HttpServletRequest request = (HttpServletRequest) req;
+
+ final String authHeader = request.getHeader("Authorization");
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ throw new ServletException("Missing or invalid Authorization header.");
+ }
+ final String token = authHeader.substring(7);
+
+ try {
+ final Claims claims = Jwts.parser().setSigningKey(SaltGenerator.getInstance().getSalt())
+ .parseClaimsJws(token).getBody();
+ request.setAttribute("claims", claims);
+ } catch (final SignatureException e) {
+ throw new ServletException("Invalid token.");
+ }
+
+ chain.doFilter(req, res);
+ }
+
+}
diff --git a/backend/src/main/java/com/ecommerce/tools/security/SaltGenerator.java b/backend/src/main/java/com/ecommerce/tools/security/SaltGenerator.java
new file mode 100644
index 0000000..274baa1
--- /dev/null
+++ b/backend/src/main/java/com/ecommerce/tools/security/SaltGenerator.java
@@ -0,0 +1,23 @@
+package com.ecommerce.tools.security;
+
+
+import java.util.UUID;
+
+public class SaltGenerator {
+
+ private static SaltGenerator ourInstance = new SaltGenerator();
+ private static String salt = UUID.randomUUID().toString();
+
+ public static SaltGenerator getInstance() {
+ return ourInstance;
+ }
+
+ private SaltGenerator() {
+ }
+
+ public String getSalt() {
+
+ return salt;
+ }
+
+}
diff --git a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000..ec9e3ae
--- /dev/null
+++ b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,14 @@
+{
+ "properties": [
+ {
+ "name": "google.maps.api.key",
+ "type": "java.lang.String",
+ "description": "Placeholder for GoogleMaps API key"
+ },
+ {
+ "name": "paypal.api.key",
+ "type": "java.lang.String",
+ "description": "Placeholder for PayPal API key."
+ }
+ ]
+}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
new file mode 100644
index 0000000..1c4a9c2
--- /dev/null
+++ b/backend/src/main/resources/application.properties
@@ -0,0 +1,28 @@
+### Hibernate Properties ###
+spring.datasource.url=jdbc:mysql://localhost/ecommerce
+spring.datasource.username=root
+spring.datasource.password=password
+spring.datasource.driver-class-name=com.mysql.jdbc.Driver
+spring.jpa.hibernate.ddl-auto=update
+hibernate.dialect=org.hibernate.dialect.MySQLDialect
+hibernate.show_sql=true;
+hibernate.hbm2ddl.auto=update;
+entitymanager.packagesToScan=com.ecommerce
+
+### Email Service Properties ###
+spring.mail.host=
+spring.mail.port=587
+spring.mail.username=
+spring.mail.password=
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.starttls.enable=true
+spring.mail.properties.mail.smtp.starttls.required=true
+spring.mail.scheduler.persistence.enabled=false
+spring.mail.scheduler.persistence.redis.embedded=false
+spring.mail.scheduler.persistence.redis.enabled=false
+
+### Google Maps Properties ###
+google.maps.api.key=
+
+### PayPal Properties ###
+paypal.api.key=
diff --git a/backend/src/main/resources/banner.txt b/backend/src/main/resources/banner.txt
new file mode 100644
index 0000000..7c4264c
--- /dev/null
+++ b/backend/src/main/resources/banner.txt
@@ -0,0 +1,10 @@
+ _____ _____ _____ ______ _____ _______
+ / ____| __ \ /\ ___ | __ \| ____|/ ____|__ __|
+ | (___ | |__) / \ ( _ ) | |__) | |__ | (___ | |
+ \___ \| ___/ /\ \ / _ \/\ | _ /| __| \___ \ | |
+ ____) | | / ____ \ | (_> < | | \ \| |____ ____) | | |
+ |_____/|_| /_/ \_\ \___/\/ |_| \_\______|_____/ |_|
+ ___ ___ ___ _ __ ___ _ __ ___ ___ _ __ ___ ___
+ / _ \/ __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '__/ __/ _ \
+ | __/ (_| (_) | | | | | | | | | | | __/ | | (_| __/
+ \___|\___\___/|_| |_| |_|_| |_| |_|\___|_| \___\___|
diff --git a/backend/src/main/resources/sql_statements/create_table_audiobook b/backend/src/main/resources/sql_statements/create_table_audiobook
new file mode 100644
index 0000000..8c5a054
--- /dev/null
+++ b/backend/src/main/resources/sql_statements/create_table_audiobook
@@ -0,0 +1,17 @@
+CREATE TABLE `audiobook` (
+ `id` BIGINT(20) NOT NULL,
+ `author` VARCHAR(255) DEFAULT NULL,
+ `category` VARCHAR(255) DEFAULT NULL,
+ `description` VARCHAR(255) DEFAULT NULL,
+ `genre` VARCHAR(255) DEFAULT NULL,
+ `price` DECIMAL(19, 2) DEFAULT NULL,
+ `publicity_year` INT(11) DEFAULT NULL,
+ `publisher` VARCHAR(255) DEFAULT NULL,
+ `quantity` INT(11) NOT NULL,
+ `title` VARCHAR(255) NOT NULL,
+ `audio_extension` VARCHAR(255) DEFAULT NULL,
+ `duration_time` DECIMAL(19, 2) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+)
+ ENGINE = InnoDB
+ DEFAULT CHARSET = utf8;
diff --git a/backend/src/main/resources/sql_statements/create_table_book b/backend/src/main/resources/sql_statements/create_table_book
new file mode 100644
index 0000000..3379442
--- /dev/null
+++ b/backend/src/main/resources/sql_statements/create_table_book
@@ -0,0 +1,17 @@
+CREATE TABLE `book` (
+ `id` BIGINT(20) NOT NULL,
+ `author` VARCHAR(255) DEFAULT NULL,
+ `category` VARCHAR(255) DEFAULT NULL,
+ `description` VARCHAR(255) DEFAULT NULL,
+ `genre` VARCHAR(255) DEFAULT NULL,
+ `price` DECIMAL(19, 2) DEFAULT NULL,
+ `publicity_year` INT(11) DEFAULT NULL,
+ `publisher` VARCHAR(255) DEFAULT NULL,
+ `quantity` INT(11) NOT NULL,
+ `title` VARCHAR(255) NOT NULL,
+ `cover_type` VARCHAR(255) DEFAULT NULL,
+ `number_of_pages` INT(11) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+)
+ ENGINE = InnoDB
+ DEFAULT CHARSET = utf8;
diff --git a/backend/src/main/resources/sql_statements/create_table_customer b/backend/src/main/resources/sql_statements/create_table_customer
new file mode 100644
index 0000000..8da88a3
--- /dev/null
+++ b/backend/src/main/resources/sql_statements/create_table_customer
@@ -0,0 +1,11 @@
+CREATE TABLE `customer` (
+ `id` BIGINT(20) NOT NULL,
+ `email` VARCHAR(255) NOT NULL,
+ `first_name` VARCHAR(255) NOT NULL,
+ `last_name` VARCHAR(255) NOT NULL,
+ `password` VARCHAR(255) NOT NULL,
+ `address` VARCHAR(255) NOT NULL,
+ PRIMARY KEY (`id`)
+)
+ ENGINE = InnoDB
+ DEFAULT CHARSET = utf8;
diff --git a/backend/src/main/resources/sql_statements/create_table_ebook b/backend/src/main/resources/sql_statements/create_table_ebook
new file mode 100644
index 0000000..6248b2a
--- /dev/null
+++ b/backend/src/main/resources/sql_statements/create_table_ebook
@@ -0,0 +1,17 @@
+CREATE TABLE `ebook` (
+ `id` BIGINT(20) NOT NULL,
+ `author` VARCHAR(255) DEFAULT NULL,
+ `category` VARCHAR(255) DEFAULT NULL,
+ `description` VARCHAR(255) DEFAULT NULL,
+ `genre` VARCHAR(255) DEFAULT NULL,
+ `price` DECIMAL(19, 2) DEFAULT NULL,
+ `publicity_year` INT(11) DEFAULT NULL,
+ `publisher` VARCHAR(255) DEFAULT NULL,
+ `quantity` INT(11) NOT NULL,
+ `title` VARCHAR(255) NOT NULL,
+ `extension` VARCHAR(255) DEFAULT NULL,
+ `size` DECIMAL(19, 2) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+)
+ ENGINE = InnoDB
+ DEFAULT CHARSET = utf8;
diff --git a/backend/src/main/resources/sql_statements/insert_sample_audiobooks.sql b/backend/src/main/resources/sql_statements/insert_sample_audiobooks.sql
new file mode 100644
index 0000000..36755fa
--- /dev/null
+++ b/backend/src/main/resources/sql_statements/insert_sample_audiobooks.sql
@@ -0,0 +1,34 @@
+INSERT INTO audiobook
+(`id`,
+ `author`,
+ `category`,
+ `description`,
+ `genre`,
+ `price`,
+ `publicity_year`,
+ `publisher`,
+ `quantity`,
+ `title`,
+ `audio_extension`,
+ `duration_time`)
+VALUES
+ (19, "Sun Tzu", "Audiobooks", "Bassus magister diligenter prensionems lumen est.", "Historical", 8.99, -2000,
+ "None", 50, "Art Of War", "MP3", 2),
+ (20, "Richard Sharpe", "Audiobooks", "Hilotaes ridetis!", "Historical", 4.99, 1940, "Penguin Books", 50,
+ "Waterloo", "WAV", 20),
+ (21, "Marcel Proust", "Audiobooks", "Sunt musaes promissio regius, clemens elogiumes.", "PN", 7.65, 1845,
+ "Grasset and Gallimard", 50, "In Search Of Lost Time", "WAV", 2),
+ (22, "Dmitry Glukhovsky", "Audiobooks", "Pol, a bene abactor.", "Fantasy", 11.21, 2005, "Penguin Books", 50, "Futu.Re",
+ "WAV", 2),
+ (23, "William Shakespeare", "Audiobooks", "Castors unda!", "Fantasy", 6.00, 1834, "None", 50, "Romeo And Juliet", "CDA",
+ 1),
+ (24, "J. R. R. Tolkien", "Audiobooks", "A falsis, gemna altus zirbus.", "Fantasy", 7.00, 1949, "George Allen & Unwin", 50,
+ "The Return Of The King", "MP3", 2),
+ (25, "Friedrich Nietzsche", "Audiobooks", "Prarere absolute ducunt ad fidelis adelphis.", "Fantasy", 12.99, 1902,
+ "Ernst Schmeitzner", 50, "Thus Spoke Zarathustra",
+ "MP3", 1),
+ (26, "Dante Alighieri", "Audiobooks", "Idoleums sunt elogiums de noster orgia.", "Fantasy", 27.12, 1911, "Heinemann", 50,
+ "Divine Comedy", "CDA", 2),
+ (27, "Margaret Mitchell", "Audiobooks", "Est noster repressor, cesaris.", "Fantasy", 5.99, 1813,
+ "Macmillan Publishers", 50,
+ "Gone With The Wind", "MP3", 3);
\ No newline at end of file
diff --git a/backend/src/main/resources/sql_statements/insert_sample_books b/backend/src/main/resources/sql_statements/insert_sample_books
new file mode 100644
index 0000000..a1d95ae
--- /dev/null
+++ b/backend/src/main/resources/sql_statements/insert_sample_books
@@ -0,0 +1,25 @@
+INSERT INTO book
+(id, author, category, description, genre, price, publicity_year, publisher, title, cover_type, number_of_pages, quantity,
+olid)
+VALUES
+ (1, "Leo Tolstoy", "Books", "Bassus magister diligenter prensionems lumen est.", "PN", 8.99, 1899,
+ "Pinguin Publishing", "War And Peace", "Soft", 1225, 20, "OL7355253M"),
+ (2, "Leo Tolstoy", "Books", "Hilotaes ridetis!", "PN", 4.99, 1877, "Pinguin Publishing",
+ "Anna Karenina", "Hard", 864, 20, "OL11652682M"),
+ (3, "Adam Mickiewicz", "Books", "Sunt musaes promissio regius, clemens elogiumes.", "EP", 7.65, 1834,
+ "Pinguin Publishing", "Pan Tadeusz", "Hard", 334, 20, "OL22352974M"),
+ (4, "J. R. R. Tolkien", "Books", "Pol, a bene abactor.", "Fantasy", 11.21, 1937, "George Allen & Unwin",
+ "The Hobbit",
+ "Hard", 304, 20, "OL24209616M"),
+ (5, "Andrzej Sapkowski", "Books", "Castors unda!", "Fantasy", 6.00, 1834, "SuperNOWA", "Blood Of Elves", "Hard",
+ 320, 20, "OL25788501M"),
+ (7, "Aleksandr Solzhenitsyn", "Books", "Prarere absolute ducunt ad fidelis adelphis.", "Historical", 10.00, 1973,
+ "Éditions du Seuil", "The Gulag Archipelago",
+ "Hard",
+ 1728, 20, "OL11349387M"),
+ (8, "The \"Gang of Four\"", "Books", "Idoleums sunt elogiums de noster orgia.", "IT", 27.12, 1994, "Addison-Wesley",
+ "Design Patterns", "Soft", 395, 20, "OL22173620M"),
+ (9, "Fyodor Dostoevsky", "Books", "Est noster repressor, cesaris.", "PN", 5.99, 1879,
+ "Pinguin Publishing",
+ "The Brothers Karamazov", "Soft",
+ 496, 20, "OL9130238M");
diff --git a/backend/src/main/resources/sql_statements/insert_sample_ebooks.sql b/backend/src/main/resources/sql_statements/insert_sample_ebooks.sql
new file mode 100644
index 0000000..67eb568
--- /dev/null
+++ b/backend/src/main/resources/sql_statements/insert_sample_ebooks.sql
@@ -0,0 +1,37 @@
+INSERT INTO ebook
+(`id`,
+ `author`,
+ `category`,
+ `description`,
+ `genre`,
+ `price`,
+ `publicity_year`,
+ `publisher`,
+ `quantity`,
+ `title`,
+ `extension`,
+ `size`)
+VALUES
+ (10, "Gabriel García Márquez", "Ebooks", "Bassus magister diligenter prensionems lumen est.", "PN", 8.99, 1899,
+ "Jonathan Cape", 50, "100 Years Of Solitude", "mobi", 2),
+ (11, "Ernest Hemingway", "Ebooks", "Hilotaes ridetis!", "PN", 4.99, 1940, "Charles Scribner's Sons", 50,
+ "For Whom the Bell Tolls", "epub", 20),
+ (12, "Alexandre Dumas", "Ebooks", "Sunt musaes promissio regius, clemens elogiumes.", "EP", 7.65, 1845,
+ "Pinguin Publishing", 50, "The Count Of Monte Christo", "epub", 2),
+ (13, "Maynard Solomon", "Ebooks", "Pol, a bene abactor.", "Historical", 11.21, 2005, "George Allen & Unwin", 50,
+ "Mozart",
+ "epub", 2),
+ (14, "Joseph Conrad", "Ebooks", "Castors unda!", "PN", 6.00, 1834, "Blackwood's Magazine", 50, "Heart Of Darkness",
+ "AZW3",
+ 1),
+ (15, "George Orwell", "Ebooks", "A falsis, gemna altus zirbus.", "Fantasy", 7.00, 1949, "Secker & Warburg", 50,
+ "1984", "mobi", 2),
+ (16, "Sir Arthur Conan Doyle", "Ebooks", "Prarere absolute ducunt ad fidelis adelphis.", "Fantasy", 12.99, 1902,
+ "George Newnes", 50, "The Hound Of The Baskervilles",
+ "mobi", 1),
+ (17, "Frances Hodgson Burnett", "Ebooks", "Idoleums sunt elogiums de noster orgia.", "Fantasy", 27.12, 1911,
+ "Heinemann", 50,
+ "The Secret Garden", "AZW3", 2),
+ (18, "Jane Austen", "Ebooks", "Est noster repressor, cesaris.", "Fantasy", 5.99, 1813,
+ "Pinguin Publishing", 50,
+ "Pride and Prejudice", "mobi", 3);
\ No newline at end of file
diff --git a/frontend/.babelrc b/frontend/.babelrc
new file mode 100644
index 0000000..af0f0c3
--- /dev/null
+++ b/frontend/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015"]
+}
\ No newline at end of file
diff --git a/frontend/config/browserify/browserify_scanner.json b/frontend/config/browserify/browserify_scanner.json
new file mode 100644
index 0000000..9b85e50
--- /dev/null
+++ b/frontend/config/browserify/browserify_scanner.json
@@ -0,0 +1,32 @@
+{
+ "paths": [
+ "web/app/js/components",
+ "web/app/js/components/collection",
+ "web/app/js/components/collection/domain",
+ "web/app/js/components/collection/domain/transaction",
+ "web/app/js/components/collection/domain/products",
+ "web/app/js/components/model",
+ "web/app/js/components/model/domain",
+ "web/app/js/components/model/domain/products",
+ "web/app/js/components/model/domain/transaction/",
+ "web/app/js/components/model/users",
+ "web/app/js/components/object/",
+ "web/app/js/components/object/view_helpers",
+ "web/app/js/components/object/key_vendor",
+ "web/app/js/components/router",
+ "web/app/js/components/vendor",
+ "web/app/js/components/view",
+ "web/app/js/components/view/collection_view",
+ "web/app/js/components/view/components_view",
+ "web/app/js/components/view/item_view",
+ "web/app/js/components/view/item_view/static",
+ "web/app/js/components/controller",
+ "web/app/templates",
+ "web/app/templates/components",
+ "web/app/templates/components/checkout",
+ "web/app/templates/components/checkout/basket",
+ "web/app/templates/components/product",
+ "web/app/templates/page_view",
+ "web/app/templates/page_view/static"
+ ]
+}
diff --git a/frontend/config/eslint/.eslintignore b/frontend/config/eslint/.eslintignore
new file mode 100644
index 0000000..24770d9
--- /dev/null
+++ b/frontend/config/eslint/.eslintignore
@@ -0,0 +1,5 @@
+/temp
+/dist
+/node_modules
+/gulp-tasks
+/config
diff --git a/frontend/config/eslint/.eslintrc b/frontend/config/eslint/.eslintrc
new file mode 100644
index 0000000..79caa61
--- /dev/null
+++ b/frontend/config/eslint/.eslintrc
@@ -0,0 +1,17 @@
+{
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "jsx": false
+ }
+ },
+ "extends": "google",
+ "rules": {
+ "semi": 2,
+ "linebreak-style": [
+ 2,
+ "windows"
+ ]
+ }
+}
diff --git a/frontend/config/gulp/gulp_config.json b/frontend/config/gulp/gulp_config.json
new file mode 100644
index 0000000..78d464d
--- /dev/null
+++ b/frontend/config/gulp/gulp_config.json
@@ -0,0 +1,29 @@
+{
+ "const_settings": {
+ "distLocation": "./dist",
+ "indexLocation": "web/app/index.html",
+ "imagesLocation": "web/app/assets/images",
+ "scssManifestLocation": "web/app/assets/styles/manifest.scss",
+ "allScssFiles": "web/app/assets/styles/**/*.s+(a|c)ss",
+ "allJsFiles": "web/app/{js,templates}/**/*.{js,hbs}",
+ "allLintableJsFiles": "web/app/js/**/*.js",
+ "allImgFiles": "web/app/assets/images/**/*.{jpg,jpeg,png,gif,svg,ico}",
+ "jsInitializerLocation": "web/app/js/Initializer.js",
+ "jsTestDir": "web/test",
+ "concatCSSName": "bundle.css",
+ "concatJSName": "bundle.js",
+ "eslint_config": "./config/eslint/.eslintrc",
+ "sass_lint": "./config/sass-lint/.sass-lint.yml",
+ "karma_config": "./config/karma/Karma.conf.js"
+ },
+ "libraries": {
+ "normalize": "node_modules/normalize.css/normalize.css",
+ "popups": "node_modules/popups/css/popupS.css",
+ "blaze": "node_modules/blaze/dist/blaze.min.css",
+ "sweetalert": "node_modules/sweetalert/dist/sweetalert.css",
+ "awesomplete": "node_modules/awesomplete/awesomplete.css",
+ "font_awesome": "node_modules/font-awesome/css/font-awesome.css",
+ "font_awesome_font": "node_modules/font-awesome/fonts/fontawesome-webfont.woff2",
+ "shiv": "node_modules/html5shiv/dist/html5shiv.js"
+ }
+}
diff --git a/frontend/config/karma/Karma.conf.js b/frontend/config/karma/Karma.conf.js
new file mode 100644
index 0000000..c241130
--- /dev/null
+++ b/frontend/config/karma/Karma.conf.js
@@ -0,0 +1,71 @@
+// Karma configuration
+// Generated on Thu Apr 27 2017 10:41:12 GMT+0100 (GMT Daylight Time)
+
+var browserify_scanner = require('../browserify/browserify_scanner');
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '../../',
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['browserify', 'jasmine'],
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'web/app/js/**/*.{js,hbs}',
+ 'web/test/**/*.js'
+ ],
+
+ // list of files to exclude
+ exclude: [],
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'web/app/js/**/*.js': ['browserify'],
+ 'web/test/**/*.js': ['browserify'],
+ },
+
+ browserify: {
+ paths: browserify_scanner.paths,
+ transform: ['babelify', 'hbsfy'],
+ extensions: ['.js', '.hbs']
+ },
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress'],
+
+ // web server port
+ port: 9876,
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.INFO,
+ client: {
+ captureConsole: true,
+ },
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['Chrome'],
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity
+ });
+};
diff --git a/frontend/config/sass-lint/.sass-lint.yml b/frontend/config/sass-lint/.sass-lint.yml
new file mode 100644
index 0000000..cfc521b
--- /dev/null
+++ b/frontend/config/sass-lint/.sass-lint.yml
@@ -0,0 +1,40 @@
+options:
+ merge-default-rules: false
+ formatter: stylish
+
+rules:
+ extends-before-mixins: 2
+ extends-before-declarations: 2
+ placeholder-in-extend: 2
+ mixins-before-declarations:
+ - 2
+ -
+ exclude:
+ - breakpoint
+ - mq
+
+ no-warn: 1
+ no-debug: 1
+ no-ids: 2
+ no-important: 2
+ hex-notation:
+ - 2
+ -
+ style: uppercase
+ indentation:
+ - 1
+ -
+ size: tab
+ property-sort-order:
+ - 1
+ -
+ order:
+ - display
+ - margin
+ ignore-custom-properties: true
+ variable-for-property:
+ - 2
+ -
+ properties:
+ - margin
+ - content
\ No newline at end of file
diff --git a/frontend/gulp-tasks/common/Clean.js b/frontend/gulp-tasks/common/Clean.js
new file mode 100644
index 0000000..42d2142
--- /dev/null
+++ b/frontend/gulp-tasks/common/Clean.js
@@ -0,0 +1,12 @@
+'use strict';
+
+import gulp from 'gulp';
+import clean from 'gulp-clean';
+
+gulp.task('clean', () => {
+
+ return gulp.src(global.config.const_settings.distLocation)
+
+ .pipe(clean());
+
+});
diff --git a/frontend/gulp-tasks/common/Fonts.js b/frontend/gulp-tasks/common/Fonts.js
new file mode 100644
index 0000000..126e5da
--- /dev/null
+++ b/frontend/gulp-tasks/common/Fonts.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import gulp from 'gulp';
+
+gulp.task('fonts', () => {
+
+ return gulp.src(global.config.libraries.font_awesome_font)
+
+ .pipe(gulp.dest('./dist/fonts'));
+
+});
diff --git a/frontend/gulp-tasks/distributable/Html.js b/frontend/gulp-tasks/distributable/Html.js
new file mode 100644
index 0000000..32b1ed1
--- /dev/null
+++ b/frontend/gulp-tasks/distributable/Html.js
@@ -0,0 +1,23 @@
+'use strict';
+
+import gulp from 'gulp';
+import minifyHTML from 'gulp-htmlmin';
+
+gulp.task('html', () => {
+
+ return gulp.src(global.config.const_settings.indexLocation)
+ .pipe(minifyHTML({
+ 'html5': true,
+ 'caseSensitive': false,
+ 'minifyURLs': true,
+ 'removeEmptyAttributes': true,
+ 'collapseWhitespace': true,
+ 'collapseBooleanAttributes': true,
+ 'removeComments': true,
+ 'useShortDoctype': true,
+ 'keepClosingSlash': true,
+ 'decodeEntities': true,
+ }))
+ .pipe(gulp.dest(global.config.const_settings.distLocation));
+
+});
diff --git a/frontend/gulp-tasks/distributable/Html5shiv.js b/frontend/gulp-tasks/distributable/Html5shiv.js
new file mode 100644
index 0000000..f1a4945
--- /dev/null
+++ b/frontend/gulp-tasks/distributable/Html5shiv.js
@@ -0,0 +1,17 @@
+'use strict';
+
+import gulp from 'gulp';
+import uglify from 'gulp-uglify';
+import rename from 'gulp-rename';
+
+gulp.task('shiv', () => {
+
+ return gulp.src(global.config.libraries.shiv)
+ .pipe(uglify())
+ .pipe(rename({
+ suffix: '.min',
+ extname: '.js',
+ }))
+ .pipe(gulp.dest(global.config.const_settings.distLocation + '/js'));
+
+});
diff --git a/frontend/gulp-tasks/distributable/Images.js b/frontend/gulp-tasks/distributable/Images.js
new file mode 100644
index 0000000..a87eae2
--- /dev/null
+++ b/frontend/gulp-tasks/distributable/Images.js
@@ -0,0 +1,16 @@
+'use strict';
+
+import gulp from 'gulp';
+import imageMin from 'gulp-imagemin';
+
+gulp.task('images', () => {
+
+ return gulp.src(global.config.const_settings.allImgFiles)
+ .pipe(imageMin({
+ optimizationLevel: 7,
+ progressive: true,
+ interlaced: true,
+ }))
+ .pipe(gulp.dest(global.config.const_settings.distLocation + '/images'));
+
+});
diff --git a/frontend/gulp-tasks/distributable/Scripts.js b/frontend/gulp-tasks/distributable/Scripts.js
new file mode 100644
index 0000000..1072d26
--- /dev/null
+++ b/frontend/gulp-tasks/distributable/Scripts.js
@@ -0,0 +1,64 @@
+'use strict';
+
+import gulp from 'gulp';
+import uglify from 'gulp-uglify';
+import hbsfy from 'hbsfy';
+import source from 'vinyl-source-stream';
+import buffer from 'vinyl-buffer';
+import browserify from 'browserify';
+import rename from 'gulp-rename';
+
+gulp.task('scripts', () => {
+
+ let bundler = browserify(global.config.const_settings.jsInitializerLocation, {
+ transform: [
+ ['babelify', {
+ 'presets': ['es2015']
+ }],
+ ['hbsfy', {
+ extensions: ['hbs'],
+ }],
+ ],
+ paths: global.browserify_paths,
+ debug: false,
+ });
+
+ let bundle = () => {
+ return bundler
+ .bundle()
+ .pipe(source('bundle.js'))
+ .pipe(buffer())
+ .pipe(uglify({
+ compress: {
+ sequences: true,
+ properties: true,
+ dead_code: true,
+ drop_debugger: true,
+ conditionals: true,
+ comparisons: true,
+ evaluate: true,
+ booleans: true,
+ loops: true,
+ unused: true,
+ if_return: true,
+ join_vars: true,
+ cascade: true,
+ side_effects: true,
+ warnings: true,
+ drop_console: true,
+ unsafe: false,
+ },
+ mangle: true,
+ }))
+ .pipe(rename({
+ suffix: '.min',
+ extname: '.js',
+ })
+ )
+ .pipe(gulp.dest(global.config.const_settings.distLocation + '/js'))
+
+ };
+
+ return bundle();
+
+});
diff --git a/frontend/gulp-tasks/distributable/Serve.js b/frontend/gulp-tasks/distributable/Serve.js
new file mode 100644
index 0000000..fcc68ed
--- /dev/null
+++ b/frontend/gulp-tasks/distributable/Serve.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import gulp from 'gulp';
+import serve from 'gulp-serve';
+
+gulp.task('serve', ['build'], serve({
+
+ root: global.config.const_settings.distLocation,
+ port: 3000,
+
+}));
diff --git a/frontend/gulp-tasks/distributable/Styles.js b/frontend/gulp-tasks/distributable/Styles.js
new file mode 100644
index 0000000..84d7842
--- /dev/null
+++ b/frontend/gulp-tasks/distributable/Styles.js
@@ -0,0 +1,73 @@
+'use strict';
+
+import gulp from 'gulp';
+import sass from 'gulp-sass';
+import resetCSS from 'node-reset-scss';
+import concatCSS from 'gulp-concat-css';
+import purify from 'gulp-purifycss';
+import cleanCSS from 'gulp-clean-css';
+import combineMQ from 'gulp-combine-mq';
+import rename from 'gulp-rename';
+
+gulp.task('styles', () => {
+
+ const bundler = [
+ global.config.libraries.normalize,
+ global.config.const_settings.scssManifestLocation,
+ global.config.libraries.blaze,
+ global.config.libraries.font_awesome,
+ global.config.libraries.popups,
+ global.config.libraries.sweetalert,
+ global.config.libraries.awesomplete,
+ ];
+
+ const purifyOptions = {
+ whitelist: ['*sweet*', '*animate*', '*rotate*'],
+ };
+
+ return gulp.src(bundler)
+ .pipe(sass({
+ includePaths: resetCSS.includePath,
+ }))
+ .pipe(combineMQ({
+ beautify: false,
+ }))
+ .pipe(concatCSS(
+ global.config.const_settings.concatCSSName,
+ {
+ rebaseUrls: false,
+ },
+ ))
+ .pipe(purify([
+ global.config.const_settings.allJsFiles,
+ global.config.const_settings.indexLocation,
+ ], purifyOptions))
+ .pipe(cleanCSS({
+ level: {
+ 2: {
+ mergeAdjacentRules: true,
+ mergeIntoShorthands: true,
+ mergeMedia: true,
+ mergeNonAdjacentRules: true,
+ mergeSemantically: false,
+ overrideProperties: true,
+ removeEmpty: true,
+ reduceNonAdjacentRules: true,
+ removeDuplicateFontRules: true,
+ removeDuplicateMediaBlocks: true,
+ removeDuplicateRules: true,
+ removeUnusedAtRules: true,
+ restructureRules: false,
+ },
+ },
+ 'skip-rebase': true,
+ 'version': true,
+ 'compatibility': 'ie9',
+ }))
+ .pipe(rename({
+ suffix: '.min',
+ extname: '.css',
+ }))
+ .pipe(gulp.dest(global.config.const_settings.distLocation + '/css'));
+
+});
diff --git a/frontend/gulp-tasks/production/BrowserSync.js b/frontend/gulp-tasks/production/BrowserSync.js
new file mode 100644
index 0000000..00e345e
--- /dev/null
+++ b/frontend/gulp-tasks/production/BrowserSync.js
@@ -0,0 +1,17 @@
+'use strict';
+
+import gulp from 'gulp';
+import browserSync from 'browser-sync';
+
+gulp.task('browserSync', () => {
+
+ return browserSync.init({
+ server: {
+ baseDir: global.config.const_settings.distLocation,
+ index: 'index.html',
+ },
+ ui: false,
+ open: false,
+ });
+
+});
diff --git a/frontend/gulp-tasks/production/EsLint.js b/frontend/gulp-tasks/production/EsLint.js
new file mode 100644
index 0000000..39c1e2a
--- /dev/null
+++ b/frontend/gulp-tasks/production/EsLint.js
@@ -0,0 +1,15 @@
+'use strict';
+
+import gulp from 'gulp';
+import eslint from 'gulp-eslint';
+
+gulp.task('eslint', () => {
+
+ return gulp.src(global.config.const_settings.allLintableJsFiles)
+ .pipe(eslint({
+ configFile: global.config.const_settings.eslint_config,
+ }))
+ .pipe(eslint.format())
+ .pipe(eslint.failAfterError());
+
+});
diff --git a/frontend/gulp-tasks/production/Html.js b/frontend/gulp-tasks/production/Html.js
new file mode 100644
index 0000000..06cead9
--- /dev/null
+++ b/frontend/gulp-tasks/production/Html.js
@@ -0,0 +1,18 @@
+'use strict';
+
+import gulp from 'gulp';
+import browserSync from 'browser-sync';
+import plumber from 'gulp-plumber';
+
+const reload = browserSync.reload;
+
+gulp.task('html-dev', () => {
+
+ return gulp.src(global.config.const_settings.indexLocation)
+ .pipe(plumber())
+ .pipe(gulp.dest(global.config.const_settings.distLocation))
+ .pipe(reload({
+ stream: true,
+ }));
+
+});
diff --git a/frontend/gulp-tasks/production/HtmlValidator.js b/frontend/gulp-tasks/production/HtmlValidator.js
new file mode 100644
index 0000000..d4ec82c
--- /dev/null
+++ b/frontend/gulp-tasks/production/HtmlValidator.js
@@ -0,0 +1,12 @@
+'use strict';
+
+import gulp from 'gulp';
+import w3cjs from 'gulp-w3cjs';
+
+gulp.task('html-validate', function() {
+
+ gulp.src(global.config.const_settings.indexLocation)
+ .pipe(w3cjs())
+ .pipe(w3cjs.reporter());
+
+});
diff --git a/frontend/gulp-tasks/production/Images.js b/frontend/gulp-tasks/production/Images.js
new file mode 100644
index 0000000..c1bed5d
--- /dev/null
+++ b/frontend/gulp-tasks/production/Images.js
@@ -0,0 +1,16 @@
+'use strict';
+
+import gulp from 'gulp';
+import browserSync from 'browser-sync';
+
+const reload = browserSync.reload;
+
+gulp.task('images-dev', () => {
+
+ return gulp.src(global.config.const_settings.allImgFiles)
+ .pipe(gulp.dest(global.config.const_settings.distLocation + '/images'))
+ .pipe(reload({
+ stream: true,
+ }));
+
+});
diff --git a/frontend/gulp-tasks/production/Karma.js b/frontend/gulp-tasks/production/Karma.js
new file mode 100644
index 0000000..5d76f01
--- /dev/null
+++ b/frontend/gulp-tasks/production/Karma.js
@@ -0,0 +1,14 @@
+'use strict';
+
+import gulp from 'gulp';
+import karma from 'karma';
+
+const Server = karma.Server;
+
+gulp.task('karma', (done) => {
+
+ new Server({
+ configFile: require('path').resolve(global.config.const_settings.karma_config),
+ }, done).start();
+
+});
diff --git a/frontend/gulp-tasks/production/SassLint.js b/frontend/gulp-tasks/production/SassLint.js
new file mode 100644
index 0000000..6f1c869
--- /dev/null
+++ b/frontend/gulp-tasks/production/SassLint.js
@@ -0,0 +1,15 @@
+'use strict';
+
+import gulp from 'gulp';
+import sassLint from 'gulp-sass-lint';
+
+gulp.task('sass-lint', () => {
+
+ return gulp.src(global.config.const_settings.allScssFiles)
+ .pipe(sassLint({
+ configFile: global.config.const_settings.sass_lint,
+ }))
+ .pipe(sassLint.format())
+ .pipe(sassLint.failOnError());
+
+});
diff --git a/frontend/gulp-tasks/production/Scripts.js b/frontend/gulp-tasks/production/Scripts.js
new file mode 100644
index 0000000..352b58f
--- /dev/null
+++ b/frontend/gulp-tasks/production/Scripts.js
@@ -0,0 +1,60 @@
+'use strict';
+
+import gulp from 'gulp';
+import hbsfy from 'hbsfy';
+import browserify from 'browserify';
+import rename from 'gulp-rename';
+import browserSync from 'browser-sync';
+import source from 'vinyl-source-stream';
+import buffer from 'vinyl-buffer';
+import browserifyIncr from 'browserify-incremental';
+
+const reload = browserSync.reload;
+
+gulp.task('scripts-dev', () => {
+
+ let bundler = browserify(global.config.const_settings.jsInitializerLocation, {
+ transform: [
+ ['babelify', {
+ 'presets': ['es2015'],
+ }],
+ ['hbsfy', {
+ extensions: ['hbs'],
+ }],
+ ],
+ paths: global.browserify_paths,
+ debug: true,
+ cache: {},
+ packageCache: {},
+ fullPaths: true,
+ watch: true,
+ });
+
+ let bundle = () => {
+ return bundler
+ .bundle()
+ .on('error', function(error) {
+ console.log(error.toString());
+ this.emit('end');
+ })
+ .pipe(source('bundle.js'))
+ .pipe(buffer())
+ .pipe(rename({
+ suffix: '.min',
+ extname: '.js',
+ })
+ )
+ .pipe(gulp.dest(global.config.const_settings.distLocation + '/js'))
+ .pipe(reload({
+ stream: true,
+ }));
+ };
+
+ if (global.isWatching) {
+ bundler = browserifyIncr(bundler);
+ bundler.on('update', bundle);
+ }
+
+ return bundle();
+
+});
diff --git a/frontend/gulp-tasks/production/Styles.js b/frontend/gulp-tasks/production/Styles.js
new file mode 100644
index 0000000..3f23d1f
--- /dev/null
+++ b/frontend/gulp-tasks/production/Styles.js
@@ -0,0 +1,45 @@
+'use strict';
+
+import gulp from 'gulp';
+import sass from 'gulp-sass';
+import resetCSS from 'node-reset-scss';
+import concatCSS from 'gulp-concat-css';
+import rename from 'gulp-rename';
+import browserSync from 'browser-sync';
+import plumber from 'gulp-plumber';
+
+const reload = browserSync.reload;
+
+gulp.task('styles-dev', () => {
+
+ const bundler = [
+ global.config.libraries.normalize,
+ global.config.const_settings.scssManifestLocation,
+ global.config.libraries.blaze,
+ global.config.libraries.font_awesome,
+ global.config.libraries.popups,
+ global.config.libraries.sweetalert,
+ global.config.libraries.awesomplete,
+ ];
+
+ return gulp.src(bundler)
+ .pipe(plumber())
+ .pipe(sass({
+ includePaths: resetCSS.includePath,
+ }))
+ .pipe(concatCSS(
+ global.config.const_settings.concatCSSName,
+ {
+ rebaseUrls: false,
+ }
+ ))
+ .pipe(rename({
+ suffix: '.min',
+ extname: '.css',
+ }))
+ .pipe(gulp.dest(global.config.const_settings.distLocation + '/css'))
+ .pipe(reload({
+ stream: true,
+ }));
+
+});
diff --git a/frontend/gulpfile.babel.js b/frontend/gulpfile.babel.js
new file mode 100644
index 0000000..d9c3afb
--- /dev/null
+++ b/frontend/gulpfile.babel.js
@@ -0,0 +1,53 @@
+'use strict';
+
+import gulp from 'gulp';
+import requireDir from 'require-dir';
+import browserSync from 'browser-sync';
+import runSequence from 'run-sequence';
+import gulp_config from './config/gulp/gulp_config.json';
+import browserify_scanner from './config/browserify/browserify_scanner.json'
+
+global.config = gulp_config;
+global.browserify_paths = browserify_scanner.paths;
+
+const reload = browserSync.reload;
+requireDir('./gulp-tasks', {
+ recurse: true,
+});
+
+//Watch files for changes and refresh browser
+gulp.task('watch', () => {
+
+ gulp.watch(global.config.const_settings.indexLocation, ['html-dev'], reload);
+ gulp.watch(global.config.const_settings.allScssFiles, ['styles-dev'], reload);
+ gulp.watch(global.config.const_settings.allJsFiles, ['scripts-dev'], reload);
+ gulp.watch(global.config.const_settings.allImgFiles, ['images-dev'], reload);
+
+});
+
+//Build the distributable project structure
+gulp.task('build', () => {
+
+ return runSequence('clean', ['fonts', 'styles', 'html', 'scripts', 'shiv', 'images'], () => {
+ console.info('Project has been build using minification.');
+ });
+
+});
+
+//Build the development project structure
+gulp.task('build-dev', () => {
+
+ return runSequence('clean', ['fonts', 'styles-dev', 'html-dev', 'scripts-dev', 'images-dev'], () => {
+ console.info('Project has been build for development. Sourcemaps are in use.');
+ });
+
+});
+
+//Default (runnable via 'gulp')
+gulp.task('default', () => {
+
+ return runSequence('build-dev', ['watch', 'browserSync'], () => {
+ console.info('Development environment has been created. Watching for changes...');
+ });
+
+});
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..131ff38
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,73 @@
+{
+ "name": "ecommerce-front-end-project",
+ "version": "1.0.0",
+ "description": "Front end website/application that cooperates with server side application written in Java for university purposes.",
+ "main": "web/app/js/Initializer.js",
+ "scripts": {
+ "start": "gulp",
+ "test": "gulp karma"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com:Bartosz-D3V/E-commerce-full-stack-website.git"
+ },
+ "author": "Bartosz-D3V",
+ "license": "MIT",
+ "homepage": "https://https://github.com/Bartosz-D3V/E-commerce-full-stack-website#readme",
+ "dependencies": {
+ "awesomplete": "^1.1.1",
+ "backbone": "^1.3.3",
+ "backbone.localstorage": "^2.0.0-alpha.3",
+ "backbone.marionette": "^3.2.0",
+ "backbone.radio": "^2.0.0",
+ "blaze": "^3.2.2",
+ "font-awesome": "^4.7.0",
+ "google-maps": "^3.2.1",
+ "handlebars": "^4.0.6",
+ "html5shiv": "^3.7.3",
+ "jquery": "^3.2.0",
+ "normalize.css": "^6.0.0",
+ "paypal-checkout": "^4.0.54",
+ "popups": "^1.1.3",
+ "sweetalert": "^1.1.3",
+ "underscore": "^1.8.3"
+ },
+ "devDependencies": {
+ "babel-preset-es2015": "^6.24.0",
+ "babelify": "^7.3.0",
+ "browser-sync": "^2.18.8",
+ "browserify": "^14.1.0",
+ "browserify-incremental": "^3.1.1",
+ "eslint-config-google": "^0.7.1",
+ "gulp": "^3.9.1",
+ "gulp-babel": "^6.1.2",
+ "gulp-clean": "^0.3.2",
+ "gulp-clean-css": "^3.0.4",
+ "gulp-combine-mq": "^0.4.0",
+ "gulp-concat-css": "^2.3.0",
+ "gulp-eslint": "^3.0.1",
+ "gulp-htmlmin": "^3.0.0",
+ "gulp-imagemin": "^3.1.1",
+ "gulp-plumber": "^1.1.0",
+ "gulp-purifycss": "^0.2.0",
+ "gulp-rename": "^1.2.2",
+ "gulp-sass": "^3.1.0",
+ "gulp-sass-lint": "^1.3.2",
+ "gulp-serve": "^1.4.0",
+ "gulp-uglify": "^2.1.0",
+ "gulp-util": "^3.0.8",
+ "gulp-w3cjs": "^1.3.0",
+ "hbsfy": "^2.7.0",
+ "jasmine-core": "^2.5.2",
+ "karma": "^1.6.0",
+ "karma-browserify": "^5.1.1",
+ "karma-chrome-launcher": "^2.0.0",
+ "karma-jasmine": "^1.1.0",
+ "node-reset-scss": "^1.0.1",
+ "require-dir": "^0.3.1",
+ "run-sequence": "^1.2.2",
+ "vinyl-buffer": "^1.0.0",
+ "vinyl-source-stream": "^1.1.0",
+ "watchify": "^3.9.0"
+ }
+}
diff --git a/frontend/web/app/assets/images/background.jpg b/frontend/web/app/assets/images/background.jpg
new file mode 100644
index 0000000..c7f102e
Binary files /dev/null and b/frontend/web/app/assets/images/background.jpg differ
diff --git a/frontend/web/app/assets/images/favicon-book.ico b/frontend/web/app/assets/images/favicon-book.ico
new file mode 100644
index 0000000..d101b0d
Binary files /dev/null and b/frontend/web/app/assets/images/favicon-book.ico differ
diff --git a/frontend/web/app/assets/images/pay_pal/pay-pal-logo.png b/frontend/web/app/assets/images/pay_pal/pay-pal-logo.png
new file mode 100644
index 0000000..aab877a
Binary files /dev/null and b/frontend/web/app/assets/images/pay_pal/pay-pal-logo.png differ
diff --git a/frontend/web/app/assets/images/products/.gitkeep b/frontend/web/app/assets/images/products/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/web/app/assets/styles/base/_mixins.scss b/frontend/web/app/assets/styles/base/_mixins.scss
new file mode 100644
index 0000000..0f6d4e2
--- /dev/null
+++ b/frontend/web/app/assets/styles/base/_mixins.scss
@@ -0,0 +1,47 @@
+@mixin hr-color($color) {
+ border-color: $color;
+ background-color: $color;
+ color: $color;
+}
+
+@mixin fade-in($time) {
+ margin-top: 25px;
+ font-size: 21px;
+ text-align: center;
+ animation: fadein $time;
+ -moz-animation: fadein $time; /* Firefox */
+ -webkit-animation: fadein $time; /* Safari and Chrome */
+ -o-animation: fadein $time; /* Opera */
+ @keyframes fadein {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ @-moz-keyframes fadein { /* Firefox */
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ @-webkit-keyframes fadein { /* Safari and Chrome */
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ @-o-keyframes fadein { /* Opera */
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+}
diff --git a/frontend/web/app/assets/styles/base/_typography.scss b/frontend/web/app/assets/styles/base/_typography.scss
new file mode 100644
index 0000000..d760116
--- /dev/null
+++ b/frontend/web/app/assets/styles/base/_typography.scss
@@ -0,0 +1,50 @@
+h1 {
+ margin: $box-margin-none;
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: 3em;
+}
+
+h2 {
+ margin: $box-margin-none;
+ font-size: 2em;
+}
+
+h3 {
+ font-size: 1.5em;
+ white-space: nowrap;
+}
+
+h4 {
+ font-weight: 500;
+ font-size: 1em;
+ margin-top: 0;
+}
+
+p {
+ font-size: 1.5em;
+}
+
+a {
+ color: $light-grey;
+
+ &:hover {
+ color: white;
+ }
+}
+
+.c-text {
+ &.no-extras {
+ line-height: 15px;
+ }
+}
+
+.line {
+ width: 20%;
+ height: 4px;
+ border-radius: 6px;
+
+ &--orange {
+ @include hr-color($juicy-orange);
+ }
+}
diff --git a/frontend/web/app/assets/styles/base/_variables.scss b/frontend/web/app/assets/styles/base/_variables.scss
new file mode 100644
index 0000000..e7b207c
--- /dev/null
+++ b/frontend/web/app/assets/styles/base/_variables.scss
@@ -0,0 +1,14 @@
+$fontGlobal: "EB Garamond";
+
+$backgroundURL: url('../images/background.jpg');
+
+$orange: #D56505;
+$bright-orange: #FFB677;
+$dark-yellow: #FFA04E;
+$dark-white: #E6E6E6;
+$juicy-orange: #FF4500;
+$light-black: #222;
+$light-grey: #9D9D9D;
+
+$sectionHeight: 100vh;
+$box-margin-none: 0 0 0 0;
diff --git a/frontend/web/app/assets/styles/layout/_footer.scss b/frontend/web/app/assets/styles/layout/_footer.scss
new file mode 100644
index 0000000..eae5133
--- /dev/null
+++ b/frontend/web/app/assets/styles/layout/_footer.scss
@@ -0,0 +1,19 @@
+.c-footer {
+ position: absolute;
+ background-color: $light-black;
+ text-align: center;
+ height: 100px;
+ width: 100%;
+
+ &__anchor {
+ color: black;
+
+ &:hover {
+ color: $juicy-orange;
+ }
+ }
+ &__li {
+ text-align: center;
+ list-style-type: none;
+ }
+}
diff --git a/frontend/web/app/assets/styles/layout/_grid.scss b/frontend/web/app/assets/styles/layout/_grid.scss
new file mode 100644
index 0000000..fb1b1f9
--- /dev/null
+++ b/frontend/web/app/assets/styles/layout/_grid.scss
@@ -0,0 +1,16 @@
+.o-grid {
+
+ &__cell--height-70 {
+ min-height: 450px;
+ }
+ &__cell--place-around {
+ margin-left: 6%;
+ margin-right: 6%;
+ }
+ &--margin-top-5 {
+ margin-top: 5%;
+ }
+ &--margin-top-15 {
+ margin-top: 15%;
+ }
+}
diff --git a/frontend/web/app/assets/styles/layout/_header.scss b/frontend/web/app/assets/styles/layout/_header.scss
new file mode 100644
index 0000000..1954443
--- /dev/null
+++ b/frontend/web/app/assets/styles/layout/_header.scss
@@ -0,0 +1,17 @@
+header {
+ position: relative;
+ width: 100%;
+ height: 100vh;
+ background-size: cover;
+ background: $backgroundURL center;
+ text-align: center;
+ color: white;
+}
+
+.t-header {
+ font-weight: 500;
+
+ &__text--white {
+ color: white;
+ }
+}
diff --git a/frontend/web/app/assets/styles/layout/_section.scss b/frontend/web/app/assets/styles/layout/_section.scss
new file mode 100644
index 0000000..be1f9d7
--- /dev/null
+++ b/frontend/web/app/assets/styles/layout/_section.scss
@@ -0,0 +1,26 @@
+.s-wide {
+ font-family: $fontGlobal;
+ text-align: center;
+ width: 100%;
+ text-justify: inter-word;
+ padding-bottom: 150px;
+
+ &--full-height {
+ height: 100vh;
+ }
+ &--half-vertical{
+ width: 50%;
+ }
+ &--front {
+ @extend .s-wide--full-height;
+ background-image: $backgroundURL;
+ background-repeat: no-repeat;
+ background-size: cover;
+ }
+ &--orange {
+ background: $orange;
+ }
+ &--bright-orange {
+ background: $bright-orange;
+ }
+}
diff --git a/frontend/web/app/assets/styles/manifest.scss b/frontend/web/app/assets/styles/manifest.scss
new file mode 100644
index 0000000..ac9dae0
--- /dev/null
+++ b/frontend/web/app/assets/styles/manifest.scss
@@ -0,0 +1,22 @@
+@import url('https://fonts.googleapis.com/css?family=EB+Garamond');
+@import "base/variables";
+@import "base/mixins";
+@import "base/typography";
+@import "state/modifiers";
+@import "module/navbar";
+@import "module/google_maps";
+@import "module/table";
+@import "module/button";
+@import "module/card";
+@import "module/container";
+@import "module/panel";
+@import "module/icon";
+@import "module/img";
+@import "module/ul";
+@import "layout/section";
+@import "layout/header";
+@import "layout/footer";
+@import "layout/grid";
+@import "mobile/phablet/rwd_phablet";
+@import "mobile/smartphone/rwd_smartphone";
+@import "mobile/tablet/rwd_tablet";
diff --git a/frontend/web/app/assets/styles/mobile/phablet/_rwd_phablet.scss b/frontend/web/app/assets/styles/mobile/phablet/_rwd_phablet.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/web/app/assets/styles/mobile/smartphone/_rwd_smartphone.scss b/frontend/web/app/assets/styles/mobile/smartphone/_rwd_smartphone.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/web/app/assets/styles/mobile/tablet/_rwd_tablet.scss b/frontend/web/app/assets/styles/mobile/tablet/_rwd_tablet.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/web/app/assets/styles/module/_button.scss b/frontend/web/app/assets/styles/module/_button.scss
new file mode 100644
index 0000000..85f17e0
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_button.scss
@@ -0,0 +1,20 @@
+.c-button {
+ a {
+ color: white;
+ &:hover {
+ color: white;
+ }
+ }
+ i {
+ font-size: 1em;
+ }
+ &.no-extras {
+ margin-top: +4%;
+ }
+
+ &--to-right {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_card.scss b/frontend/web/app/assets/styles/module/_card.scss
new file mode 100644
index 0000000..0bedef9
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_card.scss
@@ -0,0 +1,22 @@
+.c-card {
+
+ &.no-extras {
+ position: absolute;
+ right: 140px;
+ width: 200px;
+ z-index: 99;
+ }
+
+ &__item--link {
+ display: block;
+ padding: $box-margin-none;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ text-align: center;
+
+ &:hover {
+ color: black;
+ }
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_container.scss b/frontend/web/app/assets/styles/module/_container.scss
new file mode 100644
index 0000000..c4ca8dd
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_container.scss
@@ -0,0 +1,20 @@
+.o-container {
+
+ &--vertical-center {
+ position: relative;
+ top: +27vh;
+ }
+ &--vertical-top {
+ position: relative;
+ top: +10vh;
+ }
+ &--full-width {
+ width: 100vw;
+ }
+ &--dark-white-text {
+ color: $dark-white;
+ }
+ &--black-text {
+ color: black;
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_google_maps.scss b/frontend/web/app/assets/styles/module/_google_maps.scss
new file mode 100644
index 0000000..5826e62
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_google_maps.scss
@@ -0,0 +1,4 @@
+.c-map {
+ width: 100%;
+ height: 100%;
+}
diff --git a/frontend/web/app/assets/styles/module/_icon.scss b/frontend/web/app/assets/styles/module/_icon.scss
new file mode 100644
index 0000000..6539289
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_icon.scss
@@ -0,0 +1,9 @@
+.c-ico {
+
+ &--big, &.no-extras {
+ font-size: 10em;
+ }
+ &--white {
+ color: white;
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_img.scss b/frontend/web/app/assets/styles/module/_img.scss
new file mode 100644
index 0000000..0cc6a9a
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_img.scss
@@ -0,0 +1,5 @@
+.img {
+ &--fade-in {
+ @include fade-in(3s);
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_navbar.scss b/frontend/web/app/assets/styles/module/_navbar.scss
new file mode 100644
index 0000000..18ef97b
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_navbar.scss
@@ -0,0 +1,25 @@
+.c-nav {
+
+ &.no-extras {
+ color: inherit;
+ background-color: $light-black;
+ }
+ &__item, &.no-extras {
+ &:not(:disabled) &:not(.custom-background){
+ &:hover {
+ background-color: transparent;
+ }
+ }
+ }
+ &__item--no-bullet {
+ list-style-type: none;
+ }
+ &__item--link {
+ display: block;
+ padding: $box-margin-none;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ font-size: 1.25em;
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_panel.scss b/frontend/web/app/assets/styles/module/_panel.scss
new file mode 100644
index 0000000..df521fb
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_panel.scss
@@ -0,0 +1,13 @@
+.o-panel-container {
+ img {
+ display: inline;
+ width: 150px;
+ height: 300px;
+ }
+ &--lowered {
+ margin-top: 60px;
+ }
+ &--to-right{
+ text-align: center;
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_table.scss b/frontend/web/app/assets/styles/module/_table.scss
new file mode 100644
index 0000000..54ea64a
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_table.scss
@@ -0,0 +1,8 @@
+.c-table {
+
+ &__cell {
+ font-size: larger;
+ text-align: center;
+ vertical-align: middle;
+ }
+}
diff --git a/frontend/web/app/assets/styles/module/_ul.scss b/frontend/web/app/assets/styles/module/_ul.scss
new file mode 100644
index 0000000..2850010
--- /dev/null
+++ b/frontend/web/app/assets/styles/module/_ul.scss
@@ -0,0 +1,7 @@
+.c-ul{
+
+ &--vertical{
+ vertical-align: middle;
+ margin-top: 14%;
+ }
+}
diff --git a/frontend/web/app/assets/styles/state/_modifiers.scss b/frontend/web/app/assets/styles/state/_modifiers.scss
new file mode 100644
index 0000000..ddff524
--- /dev/null
+++ b/frontend/web/app/assets/styles/state/_modifiers.scss
@@ -0,0 +1,7 @@
+.italic {
+ font-style: italic;
+}
+
+.bold {
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/frontend/web/app/index.html b/frontend/web/app/index.html
new file mode 100644
index 0000000..06ed7a6
--- /dev/null
+++ b/frontend/web/app/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+ Book store that matches your needs!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/web/app/js/Initializer.js b/frontend/web/app/js/Initializer.js
new file mode 100644
index 0000000..3f189ee
--- /dev/null
+++ b/frontend/web/app/js/Initializer.js
@@ -0,0 +1,9 @@
+'use strict';
+
+import {$} from 'Vendor';
+import App from 'App';
+
+$(document).ready(() => {
+ const app = new App();
+ app.start();
+});
diff --git a/frontend/web/app/js/components/App.js b/frontend/web/app/js/components/App.js
new file mode 100644
index 0000000..2afca3a
--- /dev/null
+++ b/frontend/web/app/js/components/App.js
@@ -0,0 +1,14 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+import {Marionette} from 'Vendor';
+import MainRouter from 'MainRouter';
+
+export default Marionette.Application.extend({
+
+ onStart() {
+ new MainRouter();
+ Backbone.history.start();
+ },
+
+});
diff --git a/frontend/web/app/js/components/collection/domain/products/AudiobooksCollection.js b/frontend/web/app/js/components/collection/domain/products/AudiobooksCollection.js
new file mode 100644
index 0000000..d677627
--- /dev/null
+++ b/frontend/web/app/js/components/collection/domain/products/AudiobooksCollection.js
@@ -0,0 +1,26 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+import {Radio} from 'Vendor';
+import AudiobookModel from 'AudiobookModel';
+
+export default Backbone.Collection.extend({
+
+ url: 'http://localhost:8080/audiobooks/',
+ model: AudiobookModel,
+
+ initialize: function() {
+ this.fetch({
+ url: this.url + 'all',
+ });
+
+ const bookChannel = Radio.channel('booksCollection');
+
+ bookChannel.reply({'getModelById': this.getModelById}, this);
+ },
+
+ getModelById: function(id) {
+ return this.findWhere({'id': id});
+ },
+
+});
diff --git a/frontend/web/app/js/components/collection/domain/products/BooksCollection.js b/frontend/web/app/js/components/collection/domain/products/BooksCollection.js
new file mode 100644
index 0000000..244b8fe
--- /dev/null
+++ b/frontend/web/app/js/components/collection/domain/products/BooksCollection.js
@@ -0,0 +1,26 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+import {Radio} from 'Vendor';
+import BookModel from 'BookModel';
+
+export default Backbone.Collection.extend({
+
+ url: 'http://localhost:8080/books/',
+ model: BookModel,
+
+ initialize: function() {
+ this.fetch({
+ url: this.url + 'all',
+ });
+
+ const bookChannel = Radio.channel('booksCollection');
+
+ bookChannel.reply({'getModelById': this.getModelById}, this);
+ },
+
+ getModelById: function(id) {
+ return this.findWhere({'id': id});
+ },
+
+});
diff --git a/frontend/web/app/js/components/collection/domain/products/EbooksCollection.js b/frontend/web/app/js/components/collection/domain/products/EbooksCollection.js
new file mode 100644
index 0000000..2a0ff75
--- /dev/null
+++ b/frontend/web/app/js/components/collection/domain/products/EbooksCollection.js
@@ -0,0 +1,26 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+import {Radio} from 'Vendor';
+import EbookModel from 'EbookModel';
+
+export default Backbone.Collection.extend({
+
+ url: 'http://localhost:8080/ebooks/',
+ model: EbookModel,
+
+ initialize: function() {
+ this.fetch({
+ url: this.url + 'all',
+ });
+
+ const bookChannel = Radio.channel('booksCollection');
+
+ bookChannel.reply({'getModelById': this.getModelById}, this);
+ },
+
+ getModelById: function(id) {
+ return this.findWhere({'id': id});
+ },
+
+});
diff --git a/frontend/web/app/js/components/collection/domain/transaction/OrderItemsCollection.js b/frontend/web/app/js/components/collection/domain/transaction/OrderItemsCollection.js
new file mode 100644
index 0000000..e0c5603
--- /dev/null
+++ b/frontend/web/app/js/components/collection/domain/transaction/OrderItemsCollection.js
@@ -0,0 +1,90 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {_} from 'Vendor';
+import {Backbone} from 'Vendor';
+import {LocalStorage} from 'Vendor';
+import OrderItem from 'OrderItemModel';
+
+export default Backbone.Collection.extend({
+
+ url: 'http://localhost:8080/basket/',
+ localStorage: new LocalStorage('OrderItemsCollection'),
+ model: OrderItem,
+
+ initialize: function() {
+ this.fetch();
+ },
+
+ addToBasket: function(product) {
+ const tempOrderItem = new OrderItem({
+ _id: product.get('id'),
+ productId: product.get('id'),
+ title: product.get('title'),
+ price: product.get('price'),
+ quantity: 1,
+ });
+ const orderedItemExist = this.get(tempOrderItem);
+ if (orderedItemExist) {
+ orderedItemExist.increaseQuantity();
+ orderedItemExist.save();
+ } else {
+ this.add(tempOrderItem);
+ tempOrderItem.save();
+ }
+ },
+
+ clearBasket: function() {
+ this.each((model) => {
+ this.localStorage.destroy(model);
+ });
+ this.reset();
+ },
+
+ removeFromBasket: function(orderItemId) {
+ const orderedItemExist = this.get(orderItemId);
+ if (orderedItemExist) {
+ this.remove(orderItemId);
+ this.localStorage.destroy(orderedItemExist);
+ } else {
+ throw new Error('This product is not in your basket');
+ }
+ },
+
+ updateQuantityOrderItem: function(orderItemId, newQuantity) {
+ const updatedProduct = this.findWhere({
+ _id: orderItemId,
+ });
+ updatedProduct.setQuantity(newQuantity);
+ updatedProduct.save();
+ },
+
+ makeAnOrder: function(customerEmail) {
+ let orders = this.map(function(model) {
+ return _.pick(model.toJSON(), ['productId', 'quantity']);
+ });
+ let basket = {'basket': orders, 'customerEmail': customerEmail};
+ basket.customerEmail = customerEmail;
+ $.ajax({
+ url: this.url + 'checkout',
+ type: 'POST',
+ async: true,
+ data: JSON.stringify(basket),
+ contentType: 'application/json; charset=utf-8',
+ dataType: 'JSON',
+ });
+ },
+
+ getTotal: function() {
+ let total = 0;
+ this.each((model) => {
+ total += model.getTotalPrice();
+ });
+ return total.toFixed(2);
+ },
+
+ getQuantity: function() {
+ return this.pluck('quantity').reduce((a, b) => a + b, 0);
+ },
+
+});
diff --git a/frontend/web/app/js/components/controller/ProductController.js b/frontend/web/app/js/components/controller/ProductController.js
new file mode 100644
index 0000000..5736599
--- /dev/null
+++ b/frontend/web/app/js/components/controller/ProductController.js
@@ -0,0 +1,24 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+
+export default Marionette.Object.extend({
+
+ initialize: function() {
+ this.productChannel = Radio.channel('productChannel');
+ this.bookChannel = Radio.channel('booksCollection');
+
+ this.productChannel.reply({'getTitleList': this.getTitleList}, this);
+ this.productChannel.reply({'getModelByURL': this.getModelByUrl}, this);
+ },
+
+ getTitleList: function() {
+ return this.bookChannel.request('getTitleList');
+ },
+
+ getModelByUrl: function(option) {
+ return this.bookChannel.request('getModelByURL', option);
+ },
+
+});
diff --git a/frontend/web/app/js/components/controller/PurchaseController.js b/frontend/web/app/js/components/controller/PurchaseController.js
new file mode 100644
index 0000000..ebfb449
--- /dev/null
+++ b/frontend/web/app/js/components/controller/PurchaseController.js
@@ -0,0 +1,74 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+
+export default Marionette.Object.extend({
+
+ initialize: function(options) {
+ this.collection = options.collection;
+ this.itemChannel = Radio.channel('orderItemsCollection');
+ this.bookChannel = Radio.channel('booksCollection');
+ this.routerChannel = Radio.channel('routerChannel');
+ this.transactionChannel = Radio.channel('transactionChannel');
+
+ this.listenTo(this.itemChannel, 'addToBasket', this.addToBasket);
+ this.listenTo(this.itemChannel, 'clearBasket', this.clearBasket);
+ this.listenTo(this.itemChannel, 'removeOrderItem', this.removeOrderItem);
+ this.listenTo(this.itemChannel, 'updateQuantityOrderItem', this.updateQuantityOrderItem);
+ this.listenTo(this.itemChannel, 'transactionCompleted', this.transactionCompleted);
+
+ this.itemChannel.reply({'getBasketSize': this.getBasketSize}, this);
+ this.itemChannel.reply({'getTotalPrice': this.getTotalPrice}, this);
+ this.itemChannel.reply({'getProductById': this.getProductById}, this);
+ },
+
+ addToBasket: function(product) {
+ this.collection.addToBasket(product);
+
+ this.itemChannel.trigger('updateBasketBadgeCounter');
+ },
+
+ clearBasket: function() {
+ this.collection.clearBasket();
+
+ this._updateBasketStats();
+ },
+
+ removeOrderItem: function(orderId) {
+ this.collection.removeFromBasket(orderId);
+
+ this._updateBasketStats();
+ },
+
+ updateQuantityOrderItem: function(orderItemId, newQuantity) {
+ this.collection.updateQuantityOrderItem(orderItemId, newQuantity);
+
+ this._updateBasketStats();
+ },
+
+ getBasketSize: function() {
+ return this.collection.getQuantity();
+ },
+
+ getTotalPrice: function() {
+ return this.collection.getTotal();
+ },
+
+ getProductById: function(id) {
+ return this.bookChannel.request('getModelById', id);
+ },
+
+ transactionCompleted: function() {
+ const customerEmail = this.transactionChannel.request('getCustomerEmail');
+ this.collection.makeAnOrder(customerEmail);
+ this.clearBasket();
+ this.routerChannel.trigger('navigateToSuccessPage');
+ },
+
+ _updateBasketStats: function() {
+ this.itemChannel.trigger('updateBasketBadgeCounter');
+ this.itemChannel.trigger('recalculateTotal', this.collection);
+ },
+
+});
diff --git a/frontend/web/app/js/components/controller/RouterController.js b/frontend/web/app/js/components/controller/RouterController.js
new file mode 100644
index 0000000..cb0b610
--- /dev/null
+++ b/frontend/web/app/js/components/controller/RouterController.js
@@ -0,0 +1,125 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import MainScreenView from 'MainScreenView';
+import BookShopWrapperView from 'BookShopWrapperView';
+import OrderItemsCollection from 'OrderItemsCollection';
+import BasketWrapperView from 'BasketWrapperView';
+import PurchaseController from 'PurchaseController';
+import RegisterView from 'RegisterView';
+import LoginView from 'LoginView';
+import DeliveryPageView from 'DeliveryPageView';
+import ReturnsPageView from 'ReturnsPageView';
+import ForDevelopersPageView from 'ForDevelopersPageView';
+import ContactUsPageView from 'ContactUsPageView';
+import CareerPageView from 'CareerPageView';
+import DetailsCheckoutView from 'DetailsCheckoutView';
+import CheckoutSuccessfulView from 'CheckoutSuccessfulView';
+import RightNavBarPartView from 'RightNavBarPartView';
+import ProductPageView from 'ProductPageView';
+import TransactionController from 'TransactionController';
+import ProductController from 'ProductController';
+
+export default Marionette.Object.extend({
+
+ initialize: function() {
+ this._orderItemsCollection = new OrderItemsCollection();
+
+ new PurchaseController({
+ collection: this._orderItemsCollection,
+ });
+ new TransactionController();
+ new ProductController();
+
+ const rightNavBarPartView = new RightNavBarPartView();
+ rightNavBarPartView.render();
+ },
+
+ showMainPage: function() {
+ const mainScreenView = new MainScreenView();
+ mainScreenView.render();
+ },
+
+ showBookStore: function() {
+ const bookShopWrapperView = new BookShopWrapperView({
+ productType: 'book',
+ });
+ bookShopWrapperView.render();
+ },
+
+ showEbookStore: function() {
+ const bookShopWrapperView = new BookShopWrapperView({
+ productType: 'ebook',
+ });
+ bookShopWrapperView.render();
+ },
+
+ showAudiobookStore: function() {
+ const bookShopWrapperView = new BookShopWrapperView({
+ productType: 'audiobook',
+ });
+ bookShopWrapperView.render();
+ },
+
+ showProductPage: function(titleOfProduct) {
+ const productPageView = new ProductPageView({
+ modelUrl: titleOfProduct,
+ });
+ productPageView.render();
+ },
+
+ showBasket: function() {
+ const basketWrapperView = new BasketWrapperView({
+ collection: this._orderItemsCollection,
+ });
+ basketWrapperView.render();
+ },
+
+ showRegister: function() {
+ const registerView = new RegisterView();
+ registerView.render();
+ },
+
+ showLogin: function() {
+ const loginView = new LoginView();
+ loginView.render();
+ },
+
+ showDelivery: function() {
+ const deliveryPageView = new DeliveryPageView();
+ deliveryPageView.render();
+ },
+
+ showReturns: function() {
+ const returnsPageView = new ReturnsPageView();
+ returnsPageView.render();
+ },
+
+ showForDevelopers: function() {
+ const forDevelopersPageView = new ForDevelopersPageView();
+ forDevelopersPageView.render();
+ },
+
+ showContactUs: function() {
+ const contactUsPageView = new ContactUsPageView();
+ contactUsPageView.render();
+ },
+
+ showCareer: function() {
+ const careerPageView = new CareerPageView();
+ careerPageView.render();
+ },
+
+ showCheckoutDetails: function() {
+ const detailsCheckoutView = new DetailsCheckoutView({
+ options: this._orderItemsCollection.getTotal(),
+ });
+ detailsCheckoutView.render();
+ },
+
+ showSuccessPage: function() {
+ const checkoutSuccessfulView = new CheckoutSuccessfulView();
+ checkoutSuccessfulView.render();
+ },
+
+});
diff --git a/frontend/web/app/js/components/controller/TransactionController.js b/frontend/web/app/js/components/controller/TransactionController.js
new file mode 100644
index 0000000..4133ff2
--- /dev/null
+++ b/frontend/web/app/js/components/controller/TransactionController.js
@@ -0,0 +1,61 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import AccountCredentials from 'AccountCredentialsModel';
+import Customer from 'CustomerModel';
+
+export default Marionette.Object.extend({
+
+ initialize: function() {
+ this.userChannel = Radio.channel('userChannel');
+ this.routerChannel = Radio.channel('routerChannel');
+ this.transactionChannel = Radio.channel('transactionChannel');
+
+ this.listenTo(this.transactionChannel, 'login', this.login);
+ this.listenTo(this.userChannel, 'signOut', this.logout);
+ this.listenTo(this.transactionChannel, 'register', this.register);
+ this.listenTo(this.transactionChannel, 'checkout', this.checkout);
+
+ this.userChannel.reply({'isLogged': this._isLogged}, this);
+ },
+
+ login: function(email, password) {
+ if (!this._isLogged()) {
+ const accountCredentials = new AccountCredentials({
+ email: email,
+ password: password,
+ });
+ accountCredentials.login();
+ }
+ },
+
+ logout: function() {
+ localStorage.removeItem('token');
+ },
+
+ register: function(email, password, firstName, lastName, address) {
+ const customer = new Customer({
+ email: email,
+ password: password,
+ firstName: firstName,
+ lastName: lastName,
+ address: address,
+ });
+ customer.register();
+ },
+
+ checkout: function() {
+ let isLogged = this.userChannel.request('isLogged');
+ if (isLogged) {
+ this.routerChannel.trigger('navigateToDetailsPage');
+ } else {
+ this.routerChannel.trigger('register');
+ }
+ },
+
+ _isLogged: function() {
+ return localStorage.getItem('token') !== (undefined || null);
+ },
+
+});
diff --git a/frontend/web/app/js/components/model/domain/products/AudiobookModel.js b/frontend/web/app/js/components/model/domain/products/AudiobookModel.js
new file mode 100644
index 0000000..e0f1543
--- /dev/null
+++ b/frontend/web/app/js/components/model/domain/products/AudiobookModel.js
@@ -0,0 +1,9 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+
+export default Backbone.Model.extend({
+
+ idAttribute: '_id',
+
+});
diff --git a/frontend/web/app/js/components/model/domain/products/BookModel.js b/frontend/web/app/js/components/model/domain/products/BookModel.js
new file mode 100644
index 0000000..e0f1543
--- /dev/null
+++ b/frontend/web/app/js/components/model/domain/products/BookModel.js
@@ -0,0 +1,9 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+
+export default Backbone.Model.extend({
+
+ idAttribute: '_id',
+
+});
diff --git a/frontend/web/app/js/components/model/domain/products/EbookModel.js b/frontend/web/app/js/components/model/domain/products/EbookModel.js
new file mode 100644
index 0000000..e0f1543
--- /dev/null
+++ b/frontend/web/app/js/components/model/domain/products/EbookModel.js
@@ -0,0 +1,9 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+
+export default Backbone.Model.extend({
+
+ idAttribute: '_id',
+
+});
diff --git a/frontend/web/app/js/components/model/domain/products/ProductModel.js b/frontend/web/app/js/components/model/domain/products/ProductModel.js
new file mode 100644
index 0000000..e0f1543
--- /dev/null
+++ b/frontend/web/app/js/components/model/domain/products/ProductModel.js
@@ -0,0 +1,9 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+
+export default Backbone.Model.extend({
+
+ idAttribute: '_id',
+
+});
diff --git a/frontend/web/app/js/components/model/domain/transaction/BasketModel.js b/frontend/web/app/js/components/model/domain/transaction/BasketModel.js
new file mode 100644
index 0000000..17f640c
--- /dev/null
+++ b/frontend/web/app/js/components/model/domain/transaction/BasketModel.js
@@ -0,0 +1,37 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Backbone} from 'Vendor';
+import {Radio} from 'Vendor';
+import OrderItemsCollection from 'OrderItemsCollection';
+
+export default Backbone.Model.extend({
+
+ url: 'http://localhost:8080/',
+ collection: OrderItemsCollection,
+ customerId: null,
+
+ initialize: function() {
+ this.routerChannel = Radio.channel('routerChannel');
+ },
+
+ checkout: function() {
+ $.ajax({
+ url: this.url + 'checkout',
+ type: 'POST',
+ async: true,
+ data: JSON.stringify(this),
+ contentType: 'application/json; charset=utf-8',
+ dataType: 'JSON',
+ statusCode: {
+ 200: (response) => {
+ /*
+ * Navigate to success page
+ * Empty the basket
+ * */
+ },
+ },
+ });
+ },
+
+});
diff --git a/frontend/web/app/js/components/model/domain/transaction/OrderItemModel.js b/frontend/web/app/js/components/model/domain/transaction/OrderItemModel.js
new file mode 100644
index 0000000..a36f9bf
--- /dev/null
+++ b/frontend/web/app/js/components/model/domain/transaction/OrderItemModel.js
@@ -0,0 +1,38 @@
+'use strict';
+
+import {Backbone} from 'Vendor';
+import {LocalStorage} from 'Vendor';
+
+export default Backbone.Model.extend({
+
+ idAttribute: '_id',
+ localStorage: new LocalStorage('OrderItemsCollection'),
+ defaults: {
+ quantity: 0,
+ },
+
+ increaseQuantity: function() {
+ this.set('quantity', this.getQuantity() + 1);
+ },
+
+ decreaseQuantity: function() {
+ this.set('quantity', this.getQuantity() - 1);
+ },
+
+ getQuantity: function() {
+ return this.get('quantity');
+ },
+
+ setQuantity: function(newQuantity) {
+ this.set('quantity', newQuantity);
+ },
+
+ getPrice: function() {
+ return this.get('price');
+ },
+
+ getTotalPrice: function() {
+ return this.getQuantity() * this.getPrice();
+ },
+
+});
diff --git a/frontend/web/app/js/components/model/users/AccountCredentialsModel.js b/frontend/web/app/js/components/model/users/AccountCredentialsModel.js
new file mode 100644
index 0000000..767c2b1
--- /dev/null
+++ b/frontend/web/app/js/components/model/users/AccountCredentialsModel.js
@@ -0,0 +1,54 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Backbone} from 'Vendor';
+import {Radio} from 'Vendor';
+
+export default Backbone.Model.extend({
+
+ url: 'http://localhost:8080/',
+ default: {
+ email: '',
+ password: '',
+ },
+
+ initialize: function() {
+ this.userChannel = Radio.channel('userChannel');
+ this.transactionChannel = Radio.channel('transactionChannel');
+
+ this.transactionChannel.reply({'getCustomerEmail': this._getEmail}, this);
+ },
+
+ login: function(accountCredentials) {
+ $.ajax({
+ url: this.url + 'auth',
+ type: 'POST',
+ async: false,
+ data: JSON.stringify(accountCredentials || this),
+ contentType: 'application/json; charset=utf-8',
+ dataType: 'JSON',
+ statusCode: {
+ 200: (response) => {
+ this._setToken(response.token);
+ this.userChannel.trigger('userLogged');
+ },
+ 500: () => {
+ $('#warning-area').show();
+ },
+ },
+ });
+ },
+
+ logout: function() {
+ localStorage.removeItem('token');
+ },
+
+ _getEmail: function() {
+ return this.get('email');
+ },
+
+ _setToken: function(token) {
+ localStorage.setItem('token', token);
+ },
+
+});
diff --git a/frontend/web/app/js/components/model/users/CustomerModel.js b/frontend/web/app/js/components/model/users/CustomerModel.js
new file mode 100644
index 0000000..2fb4bf0
--- /dev/null
+++ b/frontend/web/app/js/components/model/users/CustomerModel.js
@@ -0,0 +1,56 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Backbone} from 'Vendor';
+import {Radio} from 'Vendor';
+
+export default Backbone.Model.extend({
+
+ url: 'http://localhost:8080/',
+
+ initialize: function() {
+ this.transactionChannel = Radio.channel('transactionChannel');
+ },
+
+ register: function() {
+ $.ajax({
+ url: this.url + 'register',
+ type: 'POST',
+ async: false,
+ data: JSON.stringify(this),
+ contentType: 'application/json; charset=utf-8',
+ dataType: 'JSON',
+ statusCode: {
+ 200: (customerId) => {
+ this.setId(customerId);
+ this.loginUser();
+ },
+ },
+ });
+ },
+
+ loginUser: function() {
+ const email = this._getEmail();
+ const password = this._getPassword();
+
+ this.transactionChannel.trigger('login', email, password);
+ },
+
+ setId: function(id) {
+ this.set('id', id);
+ localStorage.setItem('customerId', id);
+ },
+
+ getId: function() {
+ return this.get('id') || localStorage.getItem('customerId');
+ },
+
+ _getEmail: function() {
+ return this.get('email');
+ },
+
+ _getPassword: function() {
+ return this.get('password');
+ },
+
+});
diff --git a/frontend/web/app/js/components/object/key_vendor/KeyVendor.js b/frontend/web/app/js/components/object/key_vendor/KeyVendor.js
new file mode 100644
index 0000000..98d425a
--- /dev/null
+++ b/frontend/web/app/js/components/object/key_vendor/KeyVendor.js
@@ -0,0 +1,43 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+
+export default Marionette.Object.extend({
+
+ url: 'http://localhost:8080/system/',
+
+ getGoogleMapsAPIKey: function() {
+ let key;
+
+ $.ajax({
+ url: this.url + 'google-maps',
+ type: 'GET',
+ async: false,
+ data: false,
+ dataType: 'JSON',
+ }).done((response) => {
+ key = response.token;
+ });
+
+ return key;
+ },
+
+ getPayPalAPIKey: function() {
+ let key;
+
+ $.ajax({
+ url: this.url + 'paypal',
+ type: 'GET',
+ async: false,
+ data: false,
+ dataType: 'JSON',
+ }).done((response) => {
+ key = response.token;
+ });
+
+ return key;
+
+ },
+
+});
diff --git a/frontend/web/app/js/components/object/view_helpers/SearchBarHelper.js b/frontend/web/app/js/components/object/view_helpers/SearchBarHelper.js
new file mode 100644
index 0000000..3c7a3a0
--- /dev/null
+++ b/frontend/web/app/js/components/object/view_helpers/SearchBarHelper.js
@@ -0,0 +1,33 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+
+export default Marionette.Object.extend({
+
+ url: 'http://localhost:8080/products/available-titles',
+
+ initialize: function(options) {
+ this.options = options;
+ },
+
+ getAvailableTitles: function() {
+ let titleList;
+
+ $.ajax({
+ url: this.url,
+ type: 'GET',
+ async: false,
+ data: false,
+ dataType: 'JSON',
+ }).done((response) => {
+ titleList = response;
+ }).fail(() => {
+ titleList = [];
+ titleList.push('Unfortunately, an error occurred.');
+ });
+
+ return titleList;
+ },
+
+});
diff --git a/frontend/web/app/js/components/object/view_helpers/SearchBarModelManager.js b/frontend/web/app/js/components/object/view_helpers/SearchBarModelManager.js
new file mode 100644
index 0000000..96f916e
--- /dev/null
+++ b/frontend/web/app/js/components/object/view_helpers/SearchBarModelManager.js
@@ -0,0 +1,57 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+import ProductModel from 'ProductModel';
+
+export default Marionette.Object.extend({
+
+ url: 'http://localhost:8080/products/get-by-title',
+
+ initialize: function(options) {
+ this.options = options;
+ },
+
+ getModelByTitle: function(tempTitle) {
+ const title = this.toTitleCase(tempTitle);
+ const productObj = this.fetchModel(title);
+
+ return new ProductModel(productObj);
+ },
+
+ toTitleCase: function(phrase) {
+ let tempPhrase;
+
+ if (phrase) {
+ tempPhrase = phrase.replace(/\w\S*/g, (txt) => {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ });
+ tempPhrase = tempPhrase.replace(/-/g, ' ');
+ }
+ return tempPhrase;
+ },
+
+ fetchModel: function(title) {
+ let responseModel;
+
+ $.ajax({
+ url: this.url,
+ type: 'GET',
+ async: false,
+ data: {
+ title: title,
+ },
+ dataType: 'JSON',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }).done((response) => {
+ responseModel = response;
+ }).fail(() => {
+ responseModel = '';
+ });
+
+ return responseModel;
+ },
+
+});
diff --git a/frontend/web/app/js/components/router/MainRouter.js b/frontend/web/app/js/components/router/MainRouter.js
new file mode 100644
index 0000000..e969687
--- /dev/null
+++ b/frontend/web/app/js/components/router/MainRouter.js
@@ -0,0 +1,73 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import RouterController from 'RouterController';
+
+export default Marionette.AppRouter.extend({
+
+ controller: new RouterController(),
+
+ appRoutes: {
+ '': 'showMainPage',
+ 'books': 'showBookStore',
+ 'ebooks': 'showEbookStore',
+ 'audiobooks': 'showAudiobookStore',
+ 'product/:titleOfProduct': 'showProductPage',
+ 'basket': 'showBasket',
+ 'register': 'showRegister',
+ 'login': 'showLogin',
+ 'delivery': 'showDelivery',
+ 'returns': 'showReturns',
+ 'for-developers': 'showForDevelopers',
+ 'contact-us': 'showContactUs',
+ 'career': 'showCareer',
+ },
+
+ initialize: function() {
+ this.routerChannel = Radio.channel('routerChannel');
+
+ this.listenTo(this.routerChannel, 'navigateToDetailsPage', this.showDetailsPage);
+ this.listenTo(this.routerChannel, 'navigateToSuccessPage', this.showSuccessPage);
+ this.listenTo(this.routerChannel, 'navigateToMainPage', this.showMainPage);
+ this.listenTo(this.routerChannel, 'register', this.showRegister);
+
+ this._setRuntimeRoutes();
+ },
+
+ showDetailsPage: function() {
+ this.navigate('checkout/details', {
+ trigger: true,
+ replace: true,
+ });
+ },
+
+ showSuccessPage: function() {
+ this.navigate('checkout/success', {
+ trigger: true,
+ replace: true,
+ });
+ },
+
+ showMainPage: function() {
+ this.navigate('', {
+ trigger: true,
+ replace: true,
+ });
+ },
+
+ showRegister: function() {
+ this.navigate('register', {
+ trigger: true,
+ replace: true,
+ });
+ },
+
+ _setRuntimeRoutes: function() {
+ this.processAppRoutes(this.controller, {
+ 'checkout/details': 'showCheckoutDetails',
+ 'checkout/success': 'showSuccessPage',
+ });
+ },
+
+});
diff --git a/frontend/web/app/js/components/vendor/Vendor.js b/frontend/web/app/js/components/vendor/Vendor.js
new file mode 100644
index 0000000..cda47b8
--- /dev/null
+++ b/frontend/web/app/js/components/vendor/Vendor.js
@@ -0,0 +1,18 @@
+'use strict';
+
+import _ from 'underscore';
+import $ from 'jquery';
+import Backbone from 'backbone';
+import Marionette from 'backbone.marionette';
+import Radio from 'backbone.radio';
+import {LocalStorage} from 'backbone.localstorage';
+import PayPal from 'paypal-checkout';
+import GoogleMapsLoader from 'google-maps';
+import popups from 'popups';
+import sweetalert from 'sweetalert';
+import Awesomplete from 'awesomplete';
+
+window.jQuery = $;
+Backbone.$ = $;
+
+export {_, $, Backbone, Marionette, Radio, LocalStorage, PayPal, GoogleMapsLoader, popups, sweetalert, Awesomplete};
diff --git a/frontend/web/app/js/components/view/collection_view/OrderItemsCollectionView.js b/frontend/web/app/js/components/view/collection_view/OrderItemsCollectionView.js
new file mode 100644
index 0000000..d6b4c1d
--- /dev/null
+++ b/frontend/web/app/js/components/view/collection_view/OrderItemsCollectionView.js
@@ -0,0 +1,12 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import OrderItemView from 'OrderItemView';
+
+export default Marionette.CollectionView.extend({
+
+ tagName: 'tbody',
+ className: 'c-table__body',
+ childView: OrderItemView,
+
+});
diff --git a/frontend/web/app/js/components/view/collection_view/ProductsCollectionView.js b/frontend/web/app/js/components/view/collection_view/ProductsCollectionView.js
new file mode 100644
index 0000000..86ba0fd
--- /dev/null
+++ b/frontend/web/app/js/components/view/collection_view/ProductsCollectionView.js
@@ -0,0 +1,22 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import ProductView from 'ProductView';
+
+export default Marionette.CollectionView.extend({
+
+ tagName: 'div',
+ className: 'o-grid o-grid--wrap',
+ childView: ProductView,
+
+ initialize: function(options) {
+ this.options = options;
+ },
+
+ childViewOptions: function() {
+ return {
+ category: this.options.category,
+ };
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/components_view/BasketCheckoutView.js b/frontend/web/app/js/components/view/components_view/BasketCheckoutView.js
new file mode 100644
index 0000000..437d59b
--- /dev/null
+++ b/frontend/web/app/js/components/view/components_view/BasketCheckoutView.js
@@ -0,0 +1,54 @@
+'use strict';
+
+import {_} from 'Vendor';
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import {sweetalert} from 'Vendor';
+import template from 'BasketCheckoutTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ template: template,
+ events: {
+ 'click #basket-proceed-button': 'navigateToDetailsPage',
+ 'click #basket-reset-button': 'clearBasket',
+ },
+
+ initialize: function(totalPrice) {
+ this.options = totalPrice;
+ this.transactionChannel = Radio.channel('transactionChannel');
+ this.itemChannel = Radio.channel('orderItemsCollection');
+ },
+
+ serializeData: function() {
+ return _.extend({
+ totalPrice: this.options.totalPrice,
+ });
+ },
+
+ navigateToDetailsPage: function(event) {
+ event.preventDefault();
+
+ this.transactionChannel.trigger('checkout');
+ },
+
+ clearBasket: function() {
+ let self = this;
+ sweetalert({
+ title: 'Are you sure?',
+ text: 'Do you really want to empty your basket?',
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: 'Yes, delete it!',
+ closeOnConfirm: false,
+ html: false,
+ }, () => {
+ sweetalert('Done!',
+ 'Your basket has been reset',
+ 'success');
+ self.itemChannel.trigger('clearBasket');
+ });
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/components_view/CheckoutSuccessfulView.js b/frontend/web/app/js/components/view/components_view/CheckoutSuccessfulView.js
new file mode 100644
index 0000000..261c60b
--- /dev/null
+++ b/frontend/web/app/js/components/view/components_view/CheckoutSuccessfulView.js
@@ -0,0 +1,17 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {sweetalert} from 'Vendor';
+import template from 'CheckoutSuccessfulTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ template: template,
+ el: '#basket-area',
+
+ onRender: function() {
+ const successMsg = 'Payment has been proceeded and your order is on its way';
+ sweetalert('Great!', successMsg, 'success');
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/components_view/DetailsCheckoutView.js b/frontend/web/app/js/components/view/components_view/DetailsCheckoutView.js
new file mode 100644
index 0000000..a513aff
--- /dev/null
+++ b/frontend/web/app/js/components/view/components_view/DetailsCheckoutView.js
@@ -0,0 +1,28 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import PayPalPaymentView from 'PayPalPaymentView';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: false,
+ regions: {
+ main: '#basket-area',
+ checkout: '#checkout',
+ },
+
+ initialize: function(options) {
+ this.options = options;
+ },
+
+ onRender: function() {
+ const payPalPaymentView = new PayPalPaymentView({
+ options: this.options,
+ });
+ this.showChildView('main', payPalPaymentView);
+ this.removeRegion('checkout');
+ payPalPaymentView.renderBtn();
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/components_view/DetailsView.js b/frontend/web/app/js/components/view/components_view/DetailsView.js
new file mode 100644
index 0000000..05b33a3
--- /dev/null
+++ b/frontend/web/app/js/components/view/components_view/DetailsView.js
@@ -0,0 +1,41 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {popups} from 'Vendor';
+import bookTemplate from 'BookDetailsViewTemplate.hbs';
+import ebookTemplate from 'EbookDetailsViewTemplate.hbs';
+import audiobookTemplate from 'AudiobookDetailsViewTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ initialize: function(options) {
+ this.options = options.category.category;
+ },
+
+ onRender: function() {
+ this.showDetails();
+ },
+
+ showDetails: function() {
+ popups.alert({
+ content: this.$el.html(),
+ additionalButtonOkClass: 'c-button c-button--success c-button--to-right no-extras',
+ flagCloseByOverlay: true,
+ flagCloseByEsc: true,
+ });
+ },
+
+ getTemplate: function() {
+ switch (this.options) {
+ case 'Books':
+ return bookTemplate;
+ case 'Ebooks':
+ return ebookTemplate;
+ case 'Audiobooks':
+ return audiobookTemplate;
+ default:
+ return bookTemplate;
+ }
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/components_view/EmptyBasketView.js b/frontend/web/app/js/components/view/components_view/EmptyBasketView.js
new file mode 100644
index 0000000..08be878
--- /dev/null
+++ b/frontend/web/app/js/components/view/components_view/EmptyBasketView.js
@@ -0,0 +1,10 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'EmptyBasketTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ template: template,
+
+});
diff --git a/frontend/web/app/js/components/view/components_view/OrderItemView.js b/frontend/web/app/js/components/view/components_view/OrderItemView.js
new file mode 100644
index 0000000..cb47e14
--- /dev/null
+++ b/frontend/web/app/js/components/view/components_view/OrderItemView.js
@@ -0,0 +1,51 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import {sweetalert} from 'Vendor';
+import template from 'OrderBoxRowTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ tagName: 'tr',
+ className: 'c-table__row',
+ template: template,
+ events: {
+ 'click #delete-order-button': 'deleteSingleOrderItem',
+ 'change #quantity-order-input': 'updateQuantityOrderItem',
+ },
+
+ initialize: function() {
+ this.itemChannel = Radio.channel('orderItemsCollection');
+ },
+
+ deleteSingleOrderItem: function(event) {
+ let self = this;
+
+ const orderId = parseInt($(event.target).data('target'));
+ sweetalert({
+ title: 'Are you sure?',
+ text: 'Do you really want to delete this product?',
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: 'Yes, delete it!',
+ closeOnConfirm: false,
+ html: false,
+ }, () => {
+ sweetalert('Deleted!',
+ 'Product has been removed from basket',
+ 'success');
+ self.itemChannel.trigger('removeOrderItem', orderId);
+ });
+ },
+
+ updateQuantityOrderItem: function(event) {
+ const orderId = parseInt($(event.target).data('target'));
+ const newQuantity = parseInt($(event.target).prop('value'));
+
+ this.itemChannel.trigger('updateQuantityOrderItem', orderId, newQuantity);
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/components_view/ProductView.js b/frontend/web/app/js/components/view/components_view/ProductView.js
new file mode 100644
index 0000000..83834ce
--- /dev/null
+++ b/frontend/web/app/js/components/view/components_view/ProductView.js
@@ -0,0 +1,38 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import template from 'ProductBoxTemplate.hbs';
+import DetailsView from 'DetailsView';
+
+export default Marionette.View.extend({
+
+ tagName: 'article',
+ className: 'o-grid__cell o-grid__cell--width-25',
+ template: template,
+ events: {
+ 'click button.c-button--brand': 'addToBasket',
+ 'click button.c-button--info': 'showDetails',
+ },
+
+ initialize: function(category) {
+ this.category = category;
+ this.itemChannel = Radio.channel('orderItemsCollection');
+ },
+
+ addToBasket: function(event) {
+ const productId = parseInt($(event.target).attr('id'));
+ const productModel = this.itemChannel.request('getProductById', productId);
+
+ this.itemChannel.trigger('addToBasket', productModel);
+ },
+
+ showDetails: function() {
+ new DetailsView({
+ model: this.model,
+ category: this.category,
+ }).render();
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/BasketTableView.js b/frontend/web/app/js/components/view/item_view/BasketTableView.js
new file mode 100644
index 0000000..dd67be5
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/BasketTableView.js
@@ -0,0 +1,30 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'BasketTableTemplate.hbs';
+import OrderItemsCollectionView from 'OrderItemsCollectionView';
+
+export default Marionette.View.extend({
+
+ tagName: 'table',
+ className: 'c-table',
+ template: template,
+ regions: {
+ body: {
+ el: 'tbody',
+ replaceElement: true,
+ },
+ },
+
+ initialize: function(options) {
+ this.options = options;
+ },
+
+ onRender: function() {
+ let orderItemsCollectionView = new OrderItemsCollectionView({
+ collection: this.options.collection,
+ });
+ this.showChildView('body', orderItemsCollectionView);
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/BasketWrapperView.js b/frontend/web/app/js/components/view/item_view/BasketWrapperView.js
new file mode 100644
index 0000000..612feee
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/BasketWrapperView.js
@@ -0,0 +1,49 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import template from 'BasketWrapperTemplate.hbs';
+import BasketTableView from 'BasketTableView';
+import EmptyBasketView from 'EmptyBasketView';
+import BasketCheckoutView from 'BasketCheckoutView';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: template,
+ replaceElement: true,
+ regions: {
+ main: '#basket-area',
+ checkout: '#checkout',
+ },
+
+ initialize: function(options) {
+ this.options = options;
+ this.itemChannel = Radio.channel('orderItemsCollection');
+
+ this.listenTo(this.itemChannel, 'recalculateTotal', this._updateOptions);
+ },
+
+ onRender: function() {
+ let basketView;
+ let ordersCollection = this.options.collection;
+ if (ordersCollection.length) {
+ basketView = new BasketTableView({
+ collection: ordersCollection,
+ });
+ const basketCheckoutView = new BasketCheckoutView({
+ totalPrice: ordersCollection.getTotal(),
+ });
+ this.showChildView('checkout', basketCheckoutView);
+ } else {
+ basketView = new EmptyBasketView();
+ }
+ this.showChildView('main', basketView);
+ },
+
+ _updateOptions: function(options) {
+ this.options.collection = options;
+ this.render();
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/BookShopWrapperView.js b/frontend/web/app/js/components/view/item_view/BookShopWrapperView.js
new file mode 100644
index 0000000..56f17ee
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/BookShopWrapperView.js
@@ -0,0 +1,55 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'BookShopWrapperTemplate.hbs';
+import ProductsCollectionView from 'ProductsCollectionView';
+import BooksCollection from 'BooksCollection';
+import EbooksCollection from 'EbooksCollection';
+import AudiobooksCollection from 'AudiobooksCollection';
+
+export default Marionette.View.extend({
+
+ template: template,
+ el: '#mainApp',
+ replaceElement: true,
+ regions: {
+ main: '#store-area',
+ },
+
+ initialize: function(options) {
+ this.options = options;
+ },
+
+ onRender: function() {
+ let booksCollection;
+ let ebooksCollection;
+ let audiobooksCollection;
+ let collection;
+ let category;
+
+ switch (this.options.productType) {
+ case 'book':
+ category = 'Books';
+ booksCollection = new BooksCollection();
+ collection = booksCollection;
+ break;
+ case 'ebook':
+ category = 'Ebooks';
+ ebooksCollection = new EbooksCollection();
+ collection = ebooksCollection;
+ break;
+ case 'audiobook':
+ category = 'Audiobooks';
+ audiobooksCollection = new AudiobooksCollection();
+ collection = audiobooksCollection;
+ break;
+ }
+
+ const productsCollectionView = new ProductsCollectionView({
+ collection: collection,
+ category: category,
+ });
+ this.showChildView('main', productsCollectionView);
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/ContactUsView.js b/frontend/web/app/js/components/view/item_view/ContactUsView.js
new file mode 100644
index 0000000..4204d11
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/ContactUsView.js
@@ -0,0 +1,75 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+import {GoogleMapsLoader} from 'Vendor';
+import {sweetalert} from 'Vendor';
+import template from 'ContactUsTemplate.hbs';
+import KeyVendor from 'KeyVendor';
+
+export default Marionette.View.extend({
+
+ template: template,
+ events: {
+ 'click #geolocation-btn': 'showWay',
+ },
+
+ initialize: function() {
+ const keyVendor = new KeyVendor();
+ this.key = keyVendor.getGoogleMapsAPIKey();
+ },
+
+ renderMap: function() {
+ const mapSelector = $('#map');
+ GoogleMapsLoader.KEY = this.key;
+
+ GoogleMapsLoader.load(function(google) {
+ let map = new google.maps.Map(mapSelector[0], {
+ zoom: 17,
+ center: new google.maps.LatLng(51.500729, -0.124625),
+ mapTypeId: google.maps.MapTypeId.ROADMAP,
+ });
+ let point = new google.maps.Marker({
+ position: new google.maps.LatLng(51.500729, -0.124625),
+ map: map[0],
+ title: 'Bookify!',
+ });
+
+ point.setMap(map);
+ });
+ },
+
+ showWay: function() {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(function(position) {
+ const mapSelector = $('#map');
+ GoogleMapsLoader.KEY = 'AIzaSyC0eTW8VQQQUVl_e2rEzfwAMhYGlWmQ9zU';
+
+ GoogleMapsLoader.load(function(google) {
+ const latitude = position.coords.latitude;
+ const longitude = position.coords.longitude;
+ const coords = new google.maps.LatLng(latitude, longitude);
+ const directionsService = new google.maps.DirectionsService();
+ const directionsDisplay = new google.maps.DirectionsRenderer();
+ const map = new google.maps.Map(mapSelector[0]);
+ const request = {
+ origin: coords,
+ destination: new google.maps.LatLng(51.500729, -0.124625),
+ travelMode: google.maps.DirectionsTravelMode.DRIVING,
+ };
+
+ directionsDisplay.setMap(map);
+ directionsDisplay.setPanel(document.getElementById('panel'));
+ directionsService.route(request, function(response, status) {
+ if (status == google.maps.DirectionsStatus.OK) {
+ directionsDisplay.setDirections(response);
+ }
+ });
+ });
+ });
+ } else {
+ sweetalert('Oops...', 'Geolocation is not supported by this browser.', 'error');
+ }
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/LoginView.js b/frontend/web/app/js/components/view/item_view/LoginView.js
new file mode 100644
index 0000000..d789b13
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/LoginView.js
@@ -0,0 +1,28 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import template from 'LoginTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ template: template,
+ el: '#mainApp',
+ events: {
+ 'click #login': 'login',
+ },
+
+ initialize: function() {
+ this.transactionChannel = Radio.channel('transactionChannel');
+ },
+
+ login: function(event) {
+ event.preventDefault();
+ const email = $('#email').val();
+ const password = $('#password').val();
+
+ this.transactionChannel.trigger('login', email, password);
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/MainScreenView.js b/frontend/web/app/js/components/view/item_view/MainScreenView.js
new file mode 100644
index 0000000..20f6aa4
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/MainScreenView.js
@@ -0,0 +1,21 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import ContactUsView from 'ContactUsView';
+import template from 'MainScreenTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ template: template,
+ el: '#mainApp',
+ regions: {
+ contact: '#contact-us',
+ },
+
+ onRender: function() {
+ const contactUsView = new ContactUsView();
+ this.showChildView('contact', contactUsView);
+ contactUsView.renderMap();
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/PayPalPaymentView.js b/frontend/web/app/js/components/view/item_view/PayPalPaymentView.js
new file mode 100644
index 0000000..ec25191
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/PayPalPaymentView.js
@@ -0,0 +1,62 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import {PayPal} from 'Vendor';
+import template from 'PayPalPaymentTemplate.hbs';
+import KeyVendor from 'KeyVendor';
+
+export default Marionette.View.extend({
+
+ template: template,
+
+ initialize: function(options) {
+ this.itemChannel = Radio.channel('orderItemsCollection');
+
+ this.totalPrice = options.options.options;
+
+ const keyVendor = new KeyVendor();
+ this.key = keyVendor.getPayPalAPIKey();
+ },
+
+ renderBtn: function() {
+ const self = this;
+
+ PayPal.Button.render({
+ env: 'sandbox',
+ commit: true,
+
+ client: {
+ sandbox: self.key,
+ },
+
+ payment: function() {
+ const env = this.props.env;
+ const client = this.props.client;
+
+ return PayPal.rest.payment.create(env, client, {
+ transactions: [
+ {
+ amount: {total: self.totalPrice, currency: 'GBP'},
+ },
+ ],
+ });
+ },
+
+ onAuthorize: function(data, actions) {
+ return actions.payment.execute().then(function() {
+ self.itemChannel.trigger('transactionCompleted');
+ });
+ },
+
+ style: {
+ size: 'small',
+ color: 'blue',
+ shape: 'pill',
+ label: 'checkout',
+ },
+
+ }, '#paypal-button');
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/ProductPageView.js b/frontend/web/app/js/components/view/item_view/ProductPageView.js
new file mode 100644
index 0000000..d2b6641
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/ProductPageView.js
@@ -0,0 +1,33 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import template from 'ProductPageViewTemplate.hbs';
+import SearchBarModelManager from 'SearchBarModelManager';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: template,
+ replaceElement: true,
+ events: {
+ 'click button.c-button--brand.nw': 'addToBasket',
+ },
+
+ initialize: function(options) {
+ this.itemChannel = Radio.channel('orderItemsCollection');
+
+ this.assignModel(options);
+ },
+
+ addToBasket: function() {
+ const productModel = this.model;
+
+ this.itemChannel.trigger('addToBasket', productModel);
+ },
+
+ assignModel: function(options) {
+ this.model = new SearchBarModelManager().getModelByTitle(options.modelUrl);
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/RegisterView.js b/frontend/web/app/js/components/view/item_view/RegisterView.js
new file mode 100644
index 0000000..7694366
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/RegisterView.js
@@ -0,0 +1,31 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import template from 'RegisterTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ template: template,
+ el: '#mainApp',
+ events: {
+ 'click #register': 'register',
+ },
+
+ initialize: function() {
+ this.transactionChannel = Radio.channel('transactionChannel');
+ },
+
+ register: function(event) {
+ event.preventDefault();
+ const email = $('#email').val();
+ const password = $('#password').val();
+ const firstName = $('#firstName').val();
+ const lastName = $('#lastName').val();
+ const address = $('#address').val();
+
+ this.transactionChannel.trigger('register', email, password, firstName, lastName, address);
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/RightNavBarPartView.js b/frontend/web/app/js/components/view/item_view/RightNavBarPartView.js
new file mode 100644
index 0000000..8950308
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/RightNavBarPartView.js
@@ -0,0 +1,126 @@
+'use strict';
+
+import {$} from 'Vendor';
+import {Backbone} from 'Vendor';
+import {Marionette} from 'Vendor';
+import {Radio} from 'Vendor';
+import {Awesomplete} from 'Vendor';
+import template from 'RightNavBarPartTemplate.hbs';
+import SearchBarHelper from 'SearchBarHelper';
+
+export default Marionette.View.extend({
+
+ template: template,
+ el: '#right-navbar-li',
+ events: {
+ 'click #account': '_toggleDropdown',
+ 'click #login-btn': '_toggleDropdown',
+ 'click #register-btn': '_toggleDropdown',
+ 'click #look-up-anchor': '_displayLookupBtn',
+ 'click #sign-out-btn': '_signOut',
+ 'focus #awesomplete': 'listenToSearchBtn',
+ },
+
+ initialize: function() {
+ this.userChannel = Radio.channel('userChannel');
+ this.routerChannel = Radio.channel('routerChannel');
+ this.productChannel = Radio.channel('productChannel');
+ this.itemChannel = Radio.channel('orderItemsCollection');
+
+ this.listenTo(this.itemChannel, 'updateBasketBadgeCounter', this._updateBasketBadgeCounter);
+ this.listenTo(this.userChannel, 'userLogged', this._showSignOutBtn);
+ },
+
+ onRender: function() {
+ this._numberOfItems = localStorage.getItem('numberOfItems');
+ this.selectorsMap = this._getSelectorObjects();
+
+ this._refreshBadgeCounter();
+ this._setAccountBtn();
+ },
+
+ _updateBasketBadgeCounter: function() {
+ this._numberOfItems = this.itemChannel.request('getBasketSize');
+
+ localStorage.setItem('numberOfItems', JSON.stringify(this._numberOfItems));
+ this._refreshBadgeCounter();
+ },
+
+ _refreshBadgeCounter: function() {
+ const badgeSelector = $('.c-badge');
+ if (badgeSelector) {
+ badgeSelector.text(this._numberOfItems);
+ }
+ },
+
+ _getSelectorObjects: function() {
+ return new Map()
+ .set('account_btn', $('#account'))
+ .set('account_drop_down', $('#account-dropdown'))
+ .set('sign_out_btn', $('#sign-out-btn'))
+ .set('contact_btn', $('#contact-btn'))
+ .set('basket_anchor', $('#basket-anchor'))
+ .set('awesomplete', $('#awesomplete'));
+ },
+
+ _toggleDropdown: function() {
+ this.selectorsMap.get('account_drop_down').toggle();
+ },
+
+ _signOut: function(event) {
+ event.preventDefault();
+ this.selectorsMap.get('account_btn').show();
+ this.selectorsMap.get('sign_out_btn').hide();
+
+ this.userChannel.trigger('signOut');
+ this.routerChannel.trigger('navigateToMainPage');
+ },
+
+ _setAccountBtn: function() {
+ if (this.userChannel.request('isLogged')) {
+ this._showSignOutBtn();
+ }
+ },
+
+ _showSignOutBtn: function() {
+ this.selectorsMap.get('account_drop_down').hide();
+ this.selectorsMap.get('account_btn').hide();
+ this.selectorsMap.get('sign_out_btn').show();
+
+ this.routerChannel.trigger('navigateToMainPage');
+ },
+
+ _displayLookupBtn: function() {
+ this.selectorsMap.get('account_btn').toggle();
+ this.selectorsMap.get('account_drop_down').hide();
+ this.selectorsMap.get('contact_btn').toggle();
+ this.selectorsMap.get('basket_anchor').toggle();
+ this.selectorsMap.get('awesomplete').toggle();
+
+ this._showSearchInput();
+ },
+
+ _showSearchInput: function() {
+ const titleList = new SearchBarHelper().getAvailableTitles();
+ let awesompleteSelector = this.selectorsMap.get('awesomplete');
+ let selectedProductTitle;
+
+ if (!this.isCreated) {
+ new Awesomplete(awesompleteSelector[0], {
+ list: titleList,
+ minChars: 1,
+ });
+ this.isCreated = true;
+ }
+
+ if (this.isCreated) {
+ awesompleteSelector.on('awesomplete-selectcomplete', function() {
+ selectedProductTitle = this.value.replace(/\s+/g, '-').toLowerCase();
+ Backbone.history.navigate('product/' + selectedProductTitle, {
+ trigger: true,
+ });
+ });
+ }
+ },
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/static/CareerPageView.js b/frontend/web/app/js/components/view/item_view/static/CareerPageView.js
new file mode 100644
index 0000000..a5660a4
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/static/CareerPageView.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'CareerPageViewTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: template,
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/static/ContactUsPageView.js b/frontend/web/app/js/components/view/item_view/static/ContactUsPageView.js
new file mode 100644
index 0000000..1a17eee
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/static/ContactUsPageView.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'ContactUsPageTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: template,
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/static/DeliveryPageView.js b/frontend/web/app/js/components/view/item_view/static/DeliveryPageView.js
new file mode 100644
index 0000000..d9dfda5
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/static/DeliveryPageView.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'DeliveryPageViewTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: template,
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/static/ForDevelopersPageView.js b/frontend/web/app/js/components/view/item_view/static/ForDevelopersPageView.js
new file mode 100644
index 0000000..fb9d29a
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/static/ForDevelopersPageView.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'ForDevelopersPageViewTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: template,
+
+});
diff --git a/frontend/web/app/js/components/view/item_view/static/ReturnsPageView.js b/frontend/web/app/js/components/view/item_view/static/ReturnsPageView.js
new file mode 100644
index 0000000..0c9701e
--- /dev/null
+++ b/frontend/web/app/js/components/view/item_view/static/ReturnsPageView.js
@@ -0,0 +1,11 @@
+'use strict';
+
+import {Marionette} from 'Vendor';
+import template from 'ReturnsPageViewTemplate.hbs';
+
+export default Marionette.View.extend({
+
+ el: '#mainApp',
+ template: template,
+
+});
diff --git a/frontend/web/app/templates/components/checkout/BasketCheckoutTemplate.hbs b/frontend/web/app/templates/components/checkout/BasketCheckoutTemplate.hbs
new file mode 100644
index 0000000..9baa989
--- /dev/null
+++ b/frontend/web/app/templates/components/checkout/BasketCheckoutTemplate.hbs
@@ -0,0 +1,14 @@
+
+
Checkout
+
+
Total: £{{totalPrice}}
+
+
+
+
+
+
diff --git a/frontend/web/app/templates/components/checkout/CheckoutSuccessfulTemplate.hbs b/frontend/web/app/templates/components/checkout/CheckoutSuccessfulTemplate.hbs
new file mode 100644
index 0000000..3d6ec61
--- /dev/null
+++ b/frontend/web/app/templates/components/checkout/CheckoutSuccessfulTemplate.hbs
@@ -0,0 +1,3 @@
+
Thank you
+
Your order has been placed
+
We also sent you an email with confirmation.
\ No newline at end of file
diff --git a/frontend/web/app/templates/components/checkout/EmptyBasketTemplate.hbs b/frontend/web/app/templates/components/checkout/EmptyBasketTemplate.hbs
new file mode 100644
index 0000000..68b25f0
--- /dev/null
+++ b/frontend/web/app/templates/components/checkout/EmptyBasketTemplate.hbs
@@ -0,0 +1,7 @@
+
+
+
Your basket is empty
+
Why not add something into it?
+
+
diff --git a/frontend/web/app/templates/components/checkout/PayPalPaymentTemplate.hbs b/frontend/web/app/templates/components/checkout/PayPalPaymentTemplate.hbs
new file mode 100644
index 0000000..e8aa3e8
--- /dev/null
+++ b/frontend/web/app/templates/components/checkout/PayPalPaymentTemplate.hbs
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
Please complete the payment process by clicking the button below
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/web/app/templates/components/checkout/basket/BasketTableTemplate.hbs b/frontend/web/app/templates/components/checkout/basket/BasketTableTemplate.hbs
new file mode 100644
index 0000000..42a469f
--- /dev/null
+++ b/frontend/web/app/templates/components/checkout/basket/BasketTableTemplate.hbs
@@ -0,0 +1,9 @@
+
+