Initial commit
commit
ecce295d7f
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"rules": {
|
||||
"indent": ["error", "tab"],
|
||||
"no-tabs": "off"
|
||||
},
|
||||
"extends": "airbnb"
|
||||
}
|
|
@ -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);
|
||||
//});
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
main {
|
||||
padding-top: 6vw;
|
||||
}
|
|
@ -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");
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
|
@ -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">×</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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue