Commit 100a3ae9 authored by Khalid Ali's avatar Khalid Ali

Merge branch 'feature/10-add-listing' into 'master'

Feature/10-Add-Listing

Closes #11 and #10

See merge request !7
parents 3ee05698 5a6b6ec9
Pipeline #4412 passed with stages
in 3 minutes
.DS_Store
node_modules
/dist
cockroach-data/*
*.pem
/tests/e2e/reports/
selenium-debug.log
......
This diff is collapsed.
......@@ -14,38 +14,48 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"axios": "^0.18.0",
"bootstrap": "^4.2.1",
"bootstrap-vue": "^2.0.0-rc.11",
"eslint-plugin-import": "^2.14.0",
"@types/vuelidate": "^0.7.5",
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.0-rc.24",
"css-loader": "^2.1.1",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"jquery": "^1.12.4",
"popper.js": "^1.14.6",
"register-service-worker": "^1.5.2",
"vue": "^2.5.22",
"https": "^1.0.0",
"install": "^0.12.2",
"jquery": "^3.4.1",
"popper.js": "^1.15.0",
"register-service-worker": "^1.6.2",
"serve": "^10.1.2",
"style-loader": "^0.23.1",
"vue": "^2.6.10",
"vue-class-component": "^6.0.0",
"vue-loader": "^15.7.0",
"vue-property-decorator": "^7.3.0",
"vue-router": "^3.0.1",
"vue-router": "^3.0.6",
"vue-style-loader": "^4.1.2",
"vue-svg-loader": "^0.11.0",
"vue-svgicon": "^3.2.2"
"vue-svgicon": "^3.2.6",
"vuelidate": "^0.7.4"
},
"devDependencies": {
"@types/jest": "^23.3.13",
"@vue/cli-plugin-babel": "^3.3.0",
"@vue/cli-plugin-e2e-nightwatch": "^3.3.0",
"@vue/cli-plugin-pwa": "^3.3.0",
"@vue/cli-plugin-typescript": "^3.3.0",
"@vue/cli-plugin-unit-jest": "^3.3.0",
"@vue/cli-service": "^3.3.0",
"@vue/test-utils": "^1.0.0-beta.28",
"acorn": "^6.0.5",
"@types/jest": "^23.3.14",
"@types/node": "^11.13.15",
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-plugin-e2e-nightwatch": "^3.8.0",
"@vue/cli-plugin-pwa": "^3.8.0",
"@vue/cli-plugin-typescript": "^3.8.1",
"@vue/cli-plugin-unit-jest": "^3.8.0",
"@vue/cli-service": "^3.8.4",
"@vue/test-utils": "^1.0.0-beta.29",
"acorn": "^6.1.1",
"babel-core": "7.0.0-bridge.0",
"eslint": "^5.12.1",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"ts-jest": "^23.0.0",
"typescript": "^3.2.4",
"vue-template-compiler": "^2.5.22"
"typescript": "^3.5.2",
"vue-template-compiler": "^2.6.10"
}
}
<template>
<div id="app">
<NavigationBar/>
<router-view/>
<div id="contents">
<router-view/>
</div>
<Footer/>
</div>
</template>
......@@ -26,9 +29,12 @@ export default class App extends Vue {}
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#contents {
margin-bottom: 90px;
}
@import '~bootstrap/dist/css/bootstrap.css'
@import '~bootstrap/dist/css/bootstrap.css';
@import '~bootstrap-vue/dist/bootstrap-vue.css';
</style>
import axios from 'axios';
const axiosInstance = axios.create({
// baseURL: `/api/`,
// baseURL: `https://localhost:9090/bs/api`,
headers: {
// 'X-CSRF-TOKEN': selector.getAttribute('content')
// 'Access-Control-Allow-Origin': '*'
},
withCredentials: true,
maxRedirects: 10,
});
export default axiosInstance;
......@@ -30,7 +30,7 @@ export default class BeerList extends Vue {
public beers: Beer[] = [];
private async created() {
const response = await axios.get('http://localhost:8080/good-beers');
const response = await axios.get('http://localhost:9090/good-beers');
this.beers = await response.data;
}
}
......
<template>
<footer class="footer">
<footer class="fixed-bottom">
<div class="container">
<div class="row">
<div class="col-md-6">
......@@ -24,17 +24,9 @@ export default class Footer extends Vue {}
</script>
<style scoped>
.footer {
position: absolute;
bottom: 0;
footer {
width: 100%;
background-color: #f5f5f5;
}
.container {
width: auto;
max-width: 1200px;
padding: 0px 15px;
padding-top: 23px;
padding-top: 18px;
}
</style>
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import BootstrapVue from 'bootstrap-vue';
import SvgIcon from 'vue-svgicon';
import App from './App.vue';
import router from './router';
......@@ -7,6 +9,8 @@ import 'bootstrap';
Vue.config.productionTip = false;
Vue.use(SvgIcon);
Vue.use(BootstrapVue);
Vue.use(Vuelidate);
router.beforeEach((to, from, next) => {
document.title = to.meta.title;
......
......@@ -27,5 +27,13 @@ export default new Router({
title: 'SRCT Bookshare - About',
},
},
{
path: '/create_listing',
name: 'create_listing',
component: () => import ('./views/CreateListing.vue'),
meta: {
title: 'SRCT Bookshare - Create Listing',
},
},
],
});
<template>
<b-container class="b-form-container">
<h1 class="text-center"><strong>SRCT</strong> BOOKSHARE</h1>
<h3 class="text-center">Add Textbook</h3>
<b-form @submit="onSubmit" @reset="onReset" v-if="show">
<b-form-row>
<b-col>
<b-form-group
id="fieldset1"
label="ISBN:"
label-for="isbn"
>
<b-form-input
id="isbn"
type="number"
v-model="form.isbn"
required
:state="!$v.form.isbn.$invalid"
aria-describedby="inputLiveHelp inputLiveFeedback"
placeholder="Enter ISBN (8120321219)"
/>
<b-form-invalid-feedback id="inputLiveFeedback">
This is a required field and must be have between 9 and 13 numbers.
</b-form-invalid-feedback>
<b-form-text id="inputLiveHelp">
The ISBN is usually located on the back of the textbook.
</b-form-text>
</b-form-group>
</b-col>
</b-form-row>
<b-form-group id="fieldset2" label="Course:" label-for="courses" label-text-align="left">
<b-form-input
id="courses"
type="text"
v-model="form.course"
required
:state="!$v.form.course.$invalid"
aria-describedby="inputLiveHelp2 inputLiveFeedback2"
placeholder="Enter Course Name (MATH 125)"
>
</b-form-input>
<b-form-invalid-feedback id="inputLiveFeedback2">
This is a required field and must be have between 7 and 8 characters.
</b-form-invalid-feedback>
<b-form-text id="inputLiveHelp2">
The course number can be found Patriotweb.
</b-form-text>
</b-form-group>
<b-form-group id="fieldset3" label="Condition:" label-for="condition">
<b-form-select id="condition" :options="condition" required v-model="form.condition">
</b-form-select>
</b-form-group>
<b-form-group id="fieldset4" label="Access Code:" label-for="accessCode">
<b-form-select id="accessCode" :options="accessCode" required v-model="form.accessCode">
</b-form-select>
</b-form-group>
<b-form-group
id="fieldset5"
label="Price:"
label-for="price"
>
<b-form-input
id="price"
type="number"
v-model="form.price"
required
:state="!$v.form.price.$invalid"
aria-describedby="inputLiveHelp3 inputLiveFeedback3"
placeholder="0"
/>
<b-form-invalid-feedback id="inputLiveFeedback3">
This is a required field and must be greater than 0.
</b-form-invalid-feedback>
<b-form-text id="inputLiveHelp3">
The price should be the amount you intend to sell for.
</b-form-text>
</b-form-group>
<b-form-group id="fieldset6" label="Images:" label-for="photo">
<b-form-file
id="files"
ref="files"
v-model="files"
accept="image/*"
multiple
placeholder="Choose some photos..."
drop-placeholder="Drop photos here..."
/>
</b-form-group>
<b-form-group id="fieldset7" label="Other Notes:" label-for="description">
<b-form-textarea
id="description"
v-model="form.description"
placeholder="Enter some useful information"
rows="3"
max-rows="6"
/>
</b-form-group>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<div class="text-center">
<b-button class="mx-sm-1" type="submit" variant="primary" :disabled="$v.form.$invalid">Submit</b-button>
<b-button class="mx-sm-1" type="reset" variant="danger">Reset</b-button>
</div>
</b-form>
</b-container>
</template>
<script lang="ts">
import API from '../api';
import { Component, Vue } from 'vue-property-decorator';
import { Validation } from 'vuelidate';
import { required, minLength, maxLength, between, numeric, helpers } from 'vuelidate/lib/validators';
interface Options {
value: number;
text: string;
}
@Component({
validations: {
form: {
isbn: {
required,
numeric,
minLength: minLength(9),
maxLength: maxLength(13),
},
course: {
required,
minLength: minLength(7),
maxLength: maxLength(8),
},
price: {
required,
numeric,
between: between(1, 1000),
},
},
},
})
export default class CreateListing extends Vue {
// Private Form Variables
private form: {isbn: string, course: string, condition: number,
accessCode: number, price: number, description: string} = {
isbn: '', course: '', condition: 0, accessCode: 2, price: 0, description: '',
};
private show: boolean = true;
private files: File[] = [];
// List of options
private condition: Options[] = [
{value: 5, text: 'New'}, {value: 4, text: 'Like New'},
{value: 3, text: 'Very Good'}, {value: 2, text: 'Good'},
{value: 1, text: 'Acceptable'}, {value: 0, text: 'Unacceptable'}];
private accessCode: Options[] = [
{value: 2, text: 'Not Applicable'},
{value: 1, text: 'Access Code Included'},
{value: 0, text: 'Access Code NOT Included'},
];
private created() {
// Must be logged in
API.get('/bs/api/').catch((error) => {
// Redirect to backend login
if (error.response === undefined || error.response.status === 403) {
location.replace('https://localhost:9090/bs/api/login');
}
});
// Get CSRF Token
API.get('/').then((response) => {
const token = response.headers['x-csrf-token'];
if (response.headers['x-csrf-token'] != null) {
localStorage.setItem('csrf_token', token);
}
}).catch((error) => {
// console.log(error.response);
});
}
private onSubmit(evt: Event) {
evt.preventDefault();
const formData = new FormData();
for ( const file of this.files ) {
formData.append('images', file, file.name);
}
formData.append('data', JSON.stringify(this.form));
API.post('bs/api/listing',
formData,
{
headers: {
'X-CSRF-TOKEN': localStorage.getItem('csrf_token'),
'Content-Type': 'multipart/form-data',
},
}).then((response) => {
// console.log(response);
})
.catch(function(this: CreateListing, error) {
// Redirect to backend login
if (error.response === undefined || error.response.status === 403) {
location.replace('https://localhost:9090/bs/api/login');
}
});
}
private onReset(evt: Event) {
evt.preventDefault();
/* Reset our form values */
this.form.isbn = '';
this.form.course = '';
this.form.condition = 0;
this.form.accessCode = 2;
this.form.price = 0;
this.form.description = '';
/* Trick to reset/clear native browser form validation state */
this.show = false;
this.$nextTick(() => {
this.show = true;
});
}
}
</script>
<style>
.b-form-container {
margin: 20px auto;
}
</style>
\ No newline at end of file
<template>
<div class="container">
<div class="page-header" id="banner">
<div class="row">
<div class="col-lg-12 text-center">
<h1><strong>SRCT</strong>&#8203;BOOKSHARE</h1>
<p class="lead text-center"><strong>Create Listing</strong></p>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 col-lg-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title text-center"><strong><i class="fa fa-plus-circle"></i> Add Your Textbook</strong></h1>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<form class="form" method="post" @submit.prevent="postNow">
ISBN<input class="textinput textInput form-control" id="id_isbn" maxlength="20" v-model="isbn" placeholder="0801884039" type="text" />
Title<input class="textinput textInput form-control" id="id_title" maxlength="200" v-model="title" placeholder="Squirrels: The Animal Answer Guide" type="text" />
Course<input class="textinput textInput form-control" id="id_course_abbr" maxlength="10" v-model="course_abbr" placeholder="ENGH 302" type="text" />
Condition<select class="select form-control" id="id_condition" v-model="condition">
<option value="New">New</option>
<option value="Like New">Like New</option>
<option value="Very Good">Very Good</option>
<option value="Good" selected="selected">Good</option>
<option value="Acceptable">Acceptable</option>
<option value="Unacceptable">Unacceptable</option>
</select>
Access Code<select class="select form-control" id="id_access_code" v-model="access_code">
<option value="Not Applicable" selected="selected">Not Applicable</option>
<option value="Access Code Included">Access Code Included</option>
<option value="Access Code NOT Included">Access Code NOT Included</option>
</select>
<div id="div_id_price">
Price
<div class="controls col-md-9 bottom-padding">
<div class="input-group">
<span class="input-group-addon">$</span>
<input class="numberinput form-control" id="id_price" min="0" v-model="price" placeholder="whole numbers" type="number" value="0"/>
<span class="input-group-addon">.00</span>
</div>
</div>
</div>
Other Notes<textarea class="textarea form-control" cols="40" id="id_description" maxlength="2000" v-model="description" placeholder="I would be willing to exchange this textbook for one that I need next semester. /// This is for Professor Smith&#39;s section ONLY. /// I can give you the workbook as well." rows="10"></textarea>
<input type="submit" value="Create" class= "btn btn-primary btn-primary">
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from 'axios';
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class CreateListing extends Vue {
public errors: object[] = [];
private async postNow() {
axios.post('https://localhost:9090/bs/api/listing', {
isbn: this.$data.isbn,
title: this.$data.title,
course: this.$data.course,
condition: this.$data.condition,
accessCode: this.$data.access_code,
price: this.$data.price,
description: this.$data.description,
}).catch(e => {
this.errors.push(e);
});
}
}
</script>
......@@ -48,6 +48,7 @@ import '@/components/icons/book';
import '@/components/icons/gift';
import '@/components/icons/eye';
import { Component, Vue } from 'vue-property-decorator';
import API from '../api';
@Component({
components: {
......
module.exports = {
devServer: {
port: 8081,
proxy: {
'/good-beers': {
target: 'http://localhost:8080',
secure: false
}
}
https: true,
port: 8081
// proxy: {
// '/bs/api/': {
// target: 'https://localhost:9090',
// ws: true,
// changeOrigin: true
// }
// }
},
configureWebpack: {
module: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment