1
0
Fork 0

Initial commit

master
Ambrose Chua 2016-12-01 01:19:07 +08:00
commit ecce295d7f
13 changed files with 442 additions and 0 deletions

7
.eslintrc Normal file
View File

@ -0,0 +1,7 @@
{
"rules": {
"indent": ["error", "tab"],
"no-tabs": "off"
},
"extends": "airbnb"
}

69
assets/bid.js Normal file
View File

@ -0,0 +1,69 @@
$('#placeBid').click(function () {
var name = $('#name').val();
var phone = $('#phone').val();
var amount = $('#amount').val();
var success = function success(data) {
var message = data.message;
$('#bidModal').modal('hide');
$('#success').html(
'<strong>Success! </strong>Your bid has been counted. ' + (message ? 'Message: ' + message : '')
).fadeIn(100);
update();
};
var error = function error(xhr) {
var message = JSON.parse(xhr.responseText).message;
$('#bidModal').modal('hide');
$('#error').html(
'<strong>Error! </strong>Your bid was rejected. ' + (message ? 'Reason: ' + message : '')
).fadeIn(100);
};
$('#error').fadeOut();
$('#success').fadeOut();
$.ajax({
method: 'PUT',
url: window.location.pathname.replace(/\/$/g, '') + '/bids/',
dataType: 'json',
data: {
name: name,
phone: phone,
amount: amount
},
success: success,
error: error
});
});
var update = function update() {
var success = function success(data) {
$('#highest').text('$' + (data.bid.highest || data.bid.starting));
};
var error = function error(xhr) {
var message = xhr.responseText;
console.warn('error', message);
};
$.ajax({
method: 'GET',
url: window.location.pathname.replace(/\/$/g, ''),
dataType: 'json',
success: success,
error: error
});
};
setInterval(update, 1000);
//$('#amount').change(function () {
setInterval(function () {
var amount = $('#amount').val();
$('#bidModal').find('.modal-title').text('Place bid of $' + amount);
}, 500);
//});

22
assets/item.css Normal file
View File

@ -0,0 +1,22 @@
.crop-wide {
display: none;
}
.crop-narrow {
display: block;
}
.images {
margin: -1em;
}
.image {
margin: 1em;
}
@media (min-width: 768px) {
.crop-wide {
display: block;
}
.crop-narrow {
display: none;
}
}

17
assets/items.css Normal file
View File

@ -0,0 +1,17 @@
.items {
margin: -1em;
}
.item {
margin: 1em;
}
@media (min-width: 768px) {
.items {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 1 0 auto;
width: calc(50% - 4em);
}
}

3
assets/main.css Normal file
View File

@ -0,0 +1,3 @@
main {
padding-top: 6vw;
}

9
index.js Normal file
View File

@ -0,0 +1,9 @@
/* eslint-env node, es6 */
const express = require('express');
const routes = require('./routes');
const app = express();
app.use(routes);
app.listen(process.env.PORT || 8080, process.env.IP || "127.0.0.1");

76
logic.js Normal file
View File

@ -0,0 +1,76 @@
/* eslint-env node, es6 */
const fs = require('fs');
const path = require('path');
const low = require('lowdb');
const fileAsync = require('lowdb/lib/file-async');
class Logic {
constructor() {
const BASE_DIR = path.join(__dirname, 'data');
const DB_FILE = path.join(BASE_DIR, 'data.json');
const CONFIG_FILE = path.join(BASE_DIR, 'config.json');
this.config = require(CONFIG_FILE); // TODO: Replace with readFileSync
this.db = low(DB_FILE, {
storage: fileAsync,
});
this.db.defaults({ items: [] }).value();
this.baseDir = BASE_DIR;
}
getConfig() {
return this.config;
}
async getIndex() {
return await new Promise((resolve, reject) => {
fs.readFile(path.join(this.baseDir, 'index.html'), 'utf8', (err, content) => {
if (err) return reject(err);
return resolve(content);
});
});
}
async getItems() {
return this.db.get('items').value();
}
async getItem(id) {
const dbitem = this.db.get('items').find({ id });
const item = Object.assign({}, dbitem.value());
item.bid.next = (item.bid.highest || item.bid.starting) + item.bid.increment;
return item;
}
async putItemBid(id, bid) {
const dbitem = this.db.get('items').find({ id });
const item = Object.assign({}, dbitem.value());
item.bid.next = (item.bid.highest || item.bid.starting) + item.bid.increment;
const amount = parseInt(bid.amount, 10);
const name = bid.name;
const phone = bid.phone;
if (!item.bid.bids) {
dbitem.get('bid').set('bids', []).value();
}
if (amount < item.bid.next) {
throw new Error(`Please bid $${item.bid.next} or higher`);
}
if (!name) {
throw new Error('Please give us your name');
}
if (!phone || phone.length < 8) {
throw new Error('Please give us a valid phone number');
}
dbitem.get('bid').get('bids').push({
amount,
name,
phone,
}).value();
dbitem.get('bid').set('highest', amount).value();
}
}
module.exports = Logic;

15
package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "simple-auction",
"scripts": {
"start": "node --harmony index.js"
},
"dependencies": {
"body-parser": "^1.15.2",
"bootstrap": "^4.0.0-alpha.5",
"express": "^4.14.0",
"express-handlebars": "^3.0.0",
"imgix.js": "^3.0.4",
"jquery": "^3.1.1",
"lowdb": "^0.14.0"
}
}

93
routes.js Normal file
View File

@ -0,0 +1,93 @@
/* eslint-env node, es6 */
const path = require('path');
const express = require('express');
const bodyparser = require('body-parser');
const hbs = require('express-handlebars');
const Logic = require('./logic');
const routes = express();
routes.engine('hbs', hbs({
extname: '.hbs',
defaultLayout: 'default',
helpers: {
ifincludes: (a, b, options) => {
if (a && a.includes && a.includes(b)) {
return options.fn(this);
}
return options.inverse(this);
},
},
}));
routes.set('view engine', 'hbs');
routes.use('/jquery', express.static(path.join(__dirname, 'node_modules/jquery/dist')));
routes.use('/bootstrap', express.static(path.join(__dirname, 'node_modules/bootstrap/dist')));
routes.use('/imgix.js', express.static(path.join(__dirname, 'node_modules/imgix.js/dist')));
routes.use('/assets', express.static(path.join(__dirname, 'assets')));
routes.use(bodyparser.urlencoded({
extended: true,
}));
const logic = new Logic();
const config = logic.getConfig();
routes.get('/', (req, res, next) => {
logic.getIndex().then((content) => {
res.render('index', {
content,
config,
});
}).catch((err) => {
next(err);
});
});
routes.get('/items/', (req, res, next) => {
logic.getItems().then((items) => {
switch (req.accepts(['json', 'html'])) {
case 'json':
return res.json(items);
case 'html':
default:
return res.render('items', {
items,
config,
});
}
}).catch((err) => {
next(err);
});
});
routes.get('/items/:id', (req, res, next) => {
logic.getItem(parseInt(req.params.id, 10)).then((item) => {
switch (req.accepts(['json', 'html'])) {
case 'json':
return res.json(item);
case 'html':
default:
return res.render('item', {
item,
config,
});
}
}).catch((err) => {
next(err);
});
});
routes.put('/items/:id/bids/', (req, res) => {
logic.putItemBid(parseInt(req.params.id, 10), req.body).then(() => {
res.json({
success: true,
});
}).catch((err) => {
res.status(400).json({
error: true,
message: err.message,
});
});
});
module.exports = routes;

11
views/index.hbs Normal file
View File

@ -0,0 +1,11 @@
<main class="container-fluid">
<div class="row">
<div class="col-sm-10 offset-sm-1 col-md-8 offset-md-2 col-lg-6 offset-lg-3 col-xl-4 offset-xl-4">
{{{content}}}
<div>
<a href="/items/" class="btn btn-primary">Go to auction</a>
</div>
</div>
</div>
</main>

85
views/item.hbs Normal file
View File

@ -0,0 +1,85 @@
{{#if item}}
{{#with item}}
<main class="container-fluid">
<div class="row">
<div class="col-sm-10 offset-sm-1 col-md-8 offset-md-2 col-lg-6 offset-lg-3 col-xl-4 offset-xl-4">
<div class="alert alert-success" role="alert" id="success" style="display: none;"></div>
<div class="alert alert-danger" role="alert" id="error" style="display: none;"></div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-8 offset-sm-2 col-md-6 offset-md-0 col-lg-4 offset-lg-2 mb-1">
<div class="images">
{{#each images}}
<div class="image">
<a href="https://{{@root.config.imgix}}/{{src}}?w=2048">
<img class="crop-wide img-fluid rounded" ix-src="https://{{@root.config.imgix}}/{{src}}?w=480&h=640&fit=crop&crop=edges" />
<img class="crop-narrow img-fluid rounded" ix-src="https://{{@root.config.imgix}}/{{src}}?w=768&h=480&fit=crop&crop=edges" />
</a>
</div>
{{/each}}
</div>
</div>
<div class="col-xs-12 col-sm-8 offset-sm-2 col-md-6 offset-md-0 col-lg-4 offset-lg-0">
<h6 class="text-muted">Description</h6>
<p>{{description}}</p>
<h6 class="text-muted">{{#if bid.highest}}Highest{{else}}Starting{{/if}} bid</h6>
<h1 class="display-4" id="highest">${{#if bid.highest}}{{bid.highest}}{{else}}{{bid.starting}}{{/if}}</h1>
<form action="#" method="get">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">$</span>
<input class="form-control" type="number" id="amount" min="1" max="10000" placeholder="Amount" value="{{bid.next}}" />
<span class="input-group-btn">
<input class="btn btn-primary" type="button" value="Place bid" data-toggle="modal" data-target="#bidModal" />
</span>
</div>
</div>
</form>
</div>
</div>
</main>
{{/with}}
{{else}}
<main class="container-fluid">
<div class="row">
<div class="col-sm-8 offset-sm-2 col-md-6 offset-md-3 col-lg-4 offset-lg-4">
<div class="alert alert-danger">
<strong>Error! </strong>Item not found.
</div>
</div>
</div>
</main>
{{/if}}
<div class="modal fade" id="bidModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Place bid</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="phoneNumber">Phone number</label>
<input type="tel" class="form-control" id="phone" placeholder="Enter your phone number">
<small class="form-text text-muted" title="TODO: add longer PDPA consent">Your number will be used solely for the purposes of contact and will be kept privately. </small>
</div>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" placeholder="Enter your name">
</div>
<p class="text-muted">
{{@root.config.bidinfo}}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="placeBid">Done</button>
</div>
</div>
</div>
</div>

16
views/items.hbs Normal file
View File

@ -0,0 +1,16 @@
<main class="container-fluid">
<div class="row">
<div class="col-sm-8 offset-sm-2 col-md-12 offset-md-0 col-lg-8 offset-lg-2">
<div class="items">
{{#each items}}
<div class="item">
<a href="/items/{{id}}">
<img class="crop-wide img-fluid rounded" ix-src="https://{{@root.config.imgix}}/{{images.0.src}}?w=480&h=640&fit=crop&crop=edges" />
<img class="crop-narrow img-fluid rounded" ix-src="https://{{@root.config.imgix}}/{{images.0.src}}?w=768&h=480&fit=crop&crop=edges" />
</a>
</div>
{{/each}}
</div>
</div>
</div>
</main>

19
views/layouts/default.hbs Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{{#if title}}{{title}}{{else}}{{@root.config.title}}{{/if}}</title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="/assets/main.css" />
<link rel="stylesheet" href="/assets/items.css" />
<link rel="stylesheet" href="/assets/item.css" />
</head>
<body>
{{{body}}}
<script src="/jquery/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
<script src="/imgix.js/imgix.min.js"></script>
<script src="/assets/bid.js"></script>
</body>
</html>