├── .gitignore ├── README.md ├── index.js ├── lib └── Counters.js ├── package.json └── static ├── app.css ├── app.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SITEPOINT FRONTEND TEST 2 | 3 | You need to create a simple counter application that can do the following: 4 | * Add a named counter to a list of counters 5 | * Increment any of the counters 6 | * Decrement any of the counters 7 | * Delete a counter 8 | * Show a sum of all the counter values 9 | * It must persist data back to the server 10 | 11 | We have provided: 12 | * Compiled Directory: of `/static/` 13 | * `/static/index.html` that will be served at `localhost:3000` when the server is running 14 | * `/static/app.js` and `/static/app.css` will be used automatically by `/static/index.html` 15 | 16 | > If you need other publicly available files, other than `index.html`, `app.js`, `app.css` you will have to modify the server code in `/index.js` 17 | 18 | Some other notes: 19 | * The design, layout, ux, is all up to you. 20 | * You can change anything you want (server stuff included) as long as the above list is completed. 21 | * This isn't a backend test, don't make it require any databases. 22 | * If you decide to use a precompiler of any kind (js/css/etc..) we need to be able to run it with `npm run build`. 23 | * We don't want to run any `npm install -g whatever` commands. **NO GLOBAL DEPENDENCIES** 24 | * Tests are good. 25 | 26 | A possible layout could be: 27 | ``` 28 | Counter App 29 | +-----------------------------+ 30 | | Input [+] | 31 | +-----------------------------+ 32 | +-----------------------------+ 33 | | [x] Bob [-] 5 [+] | 34 | | [x] Steve [-] 1 [+] | 35 | | [x] Pat [-] 4 [+] | 36 | +-----------------------------+ 37 | +-----------------------------+ 38 | | Total 10 | 39 | +-----------------------------+ 40 | ``` 41 | 42 | ## Install and start the server 43 | 44 | ``` 45 | $ npm install 46 | $ npm start 47 | $ npm run build #[optional] use for any precompilers you choose 48 | ``` 49 | 50 | ## API endpoints / examples 51 | 52 | > The following endpoints are expecting a `Content-Type: application/json` 53 | 54 | ``` 55 | GET /api/v1/counters 56 | # [] 57 | 58 | POST {title: "bob"} /api/v1/counter 59 | # [ 60 | # {id: "asdf", title: "bob", count: 0} 61 | # ] 62 | 63 | POST {title: "steve"} /api/v1/counter 64 | # [ 65 | # {id: "asdf", title: "bob", count: 0}, 66 | # {id: "qwer", title: "steve", count: 0} 67 | # ] 68 | 69 | POST {id: "asdf"} /api/v1/counter/inc 70 | # [ 71 | # {id: "asdf", title: "bob", count: 1}, 72 | # {id: "qwer", title: "steve", count: 0} 73 | # ] 74 | 75 | POST {id: "qwer"} /api/v1/counter/dec 76 | # [ 77 | # {id: "asdf", title: "bob", count: 1}, 78 | # {id: "qwer", title: "steve", count: -1} 79 | # ] 80 | 81 | DELETE {id: "qwer"} /api/v1/counter 82 | # [ 83 | # {id: "asdf", title: "bob", count: 1} 84 | # ] 85 | 86 | GET /api/v1/counters 87 | # [ 88 | # {id: "asdf", title: "bob", count: 1}, 89 | # ] 90 | ``` 91 | 92 | > **NOTE:* Each request returns the current state of all counters. 93 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var app = express(); 3 | var bodyParser = require("body-parser"); 4 | var compression = require("compression"); 5 | var morgan = require("morgan"); 6 | var PORT = Number( process.env.PORT || 3000 ); 7 | var Counters = require("./lib/Counters"); 8 | 9 | app.use(morgan("combined")); 10 | app.use(bodyParser.urlencoded({extended: false})); 11 | app.use(bodyParser.json()); 12 | app.use(compression()); 13 | 14 | function sendFile(name) { 15 | return function(req, res) { 16 | res.sendFile(__dirname + "/static/" + name); 17 | }; 18 | } 19 | 20 | app.get("/", sendFile("index.html")); 21 | app.get("/app.js", sendFile("app.js")); 22 | app.get("/app.css", sendFile("app.css")); 23 | 24 | // [json] GET /api/v1/counters 25 | // => [ 26 | // => {id: "asdf", title: "boop", count: 4}, 27 | // => {id: "zxcv", title: "steve", count: 3} 28 | // => ] 29 | app.get("/api/v1/counters", function(req, res) { 30 | res.json(Counters.all()) 31 | }); 32 | 33 | // [json] POST {title: "bob"} /api/v1/counters 34 | // => [ 35 | // => {id: "asdf", title: "boop", count: 4}, 36 | // => {id: "zxcv", title: "steve", count: 3}, 37 | // => {id: "qwer", title: "bob", count: 0} 38 | // => ] 39 | app.post("/api/v1/counter", function(req, res) { 40 | res.json(Counters.create(req.body.title)); 41 | }) 42 | 43 | // [json] DELETE {id: "asdf"} /api/v1/counter 44 | // => [ 45 | // => {id: "zxcv", title: "steve", count: 3}, 46 | // => {id: "qwer", title: "bob", count: 0} 47 | // => ] 48 | app.delete("/api/v1/counter", function(req, res) { 49 | res.json(Counters.delete(req.body.id)); 50 | }); 51 | 52 | // [json] POST {id: "qwer"} /api/v1/counter/inc 53 | // => [ 54 | // => {id: "zxcv", title: "steve", count: 3}, 55 | // => {id: "qwer", title: "bob", count: 1} 56 | // => ] 57 | app.post("/api/v1/counter/inc", function(req, res) { 58 | res.json(Counters.inc(req.body.id)); 59 | }); 60 | 61 | // [json] POST {id: "zxcv"} /api/v1/counter/dec 62 | // => [ 63 | // => {id: "zxcv", title: "steve", count: 2}, 64 | // => {id: "qwer", title: "bob", count: 1} 65 | // => ] 66 | app.post("/api/v1/counter/dec", function(req, res) { 67 | res.json(Counters.dec(req.body.id)); 68 | }); 69 | 70 | app.get("*", sendFile("index.html")); 71 | app.head("*", sendFile("index.html")); 72 | 73 | app.listen(PORT, console.log.bind(null, "PORT: " + PORT)); 74 | -------------------------------------------------------------------------------- /lib/Counters.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var __Counters = {}; 3 | 4 | module.exports = { 5 | all : getAll, 6 | create : create, 7 | inc : applyTo("count", inc), 8 | dec : applyTo("count", dec), 9 | delete : del 10 | }; 11 | 12 | function genId() { 13 | return (+new Date() + ~~(Math.random * 999999)).toString(36); 14 | } 15 | 16 | function getAll() { return _.map(__Counters, _.identity); } 17 | 18 | function create(title) { 19 | var id = genId(); 20 | __Counters[id] = {id: id, title: title, count: 0}; 21 | return getAll(); 22 | } 23 | 24 | function del(id) { 25 | delete __Counters[id]; 26 | return getAll(); 27 | } 28 | 29 | function applyTo(key, fn) { 30 | return function(id) { 31 | __Counters[id][key] = fn(__Counters[id][key]); 32 | return getAll(); 33 | } 34 | } 35 | 36 | function inc(n) { return n + 1; } 37 | function dec(n) { return n - 1; } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sitepoint-test", 3 | "version": "1.0.0", 4 | "description": "sitepoint front end test", 5 | "main": "index.js", 6 | "scripts": { 7 | "start" : "node index.js" 8 | }, 9 | "keywords": [ 10 | "sitepoint" 11 | ], 12 | "author": "orodio", 13 | "license": "ISC", 14 | "dependencies": { 15 | "body-parser": "^1.10.1", 16 | "compression": "^1.3.0", 17 | "express": "^4.11.0", 18 | "lodash": "^2.4.1", 19 | "morgan": "^1.5.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /static/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitepoint/frontend-test/ec40bf1f63b9e605cc63fe7662f2a506e5babd83/static/app.css -------------------------------------------------------------------------------- /static/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitepoint/frontend-test/ec40bf1f63b9e605cc63fe7662f2a506e5babd83/static/app.js -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Counter Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------