Compare commits

...

6 Commits

Author SHA1 Message Date
Robert Webb
3f8e8479d0 Added linting to the project 2019-02-10 16:07:09 -08:00
ed33f010d5 added /api/robots 2019-02-08 06:58:09 -08:00
notnull
2ac6cc2ff2 added seed, and a template for serving the html 2019-02-07 20:59:24 -08:00
notnull
1a2a77beca updated readme 2019-02-07 18:55:15 -08:00
notnull
9e90002113 added database and express server 2019-02-07 18:47:35 -08:00
8b89ec32f1 Merge branch 'master' of totally_not_fb/hacker-news-cli into master 2019-02-06 19:39:03 -08:00
22 changed files with 8162 additions and 62 deletions

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
footer.html
table.html

43
api/articles.js Normal file
View File

@ -0,0 +1,43 @@
const router = require('express').Router();
const { Article } = require('../db/models');
const buildPage = require('./buildPage');
module.exports = router;
router.get('/', async (req, res, next) => {
try {
const articles = await Article.findAll();
const tbl = articles
.map(
article => `<tr><td>${article.title}</td><td>${article.link}</td></tr>`
)
.join();
const page = buildPage(tbl);
console.log(page);
res.status(201).send(page);
} catch (err) {
next(err);
}
});
router.get('/:id', async (req, res, next) => {
try {
const article = await Article.findById(req.params.id);
console.log(article.title);
console.log(`by: ${article.author}`);
console.log(article.text);
res.status(201).send(article);
} catch (err) {
next(err);
}
});
router.post('/', async (req, res, next) => {
const body = req.body;
try {
const article = await Article.create(body);
res.redirect(article.id);
} catch (err) {
next(err);
}
});

22
api/buildPage.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = listString =>
`
<!DOCTYPE html>
<html>
<body>
<h2>Haxor Newz</h2>
<h3>"uber l337"</h3>
<table>
<thead>
<th>Title</th>
<th>Link</th>
</thead>
${listString}
<tfoot>
ATTACK!
</tfoot>
</table>
<footer>&#9398; anarchy planet</footer>
</body>
</html>
`;

20
api/index.js Executable file
View File

@ -0,0 +1,20 @@
const router = require('express').Router();
module.exports = router;
router.use('/items', require('./items'));
router.use('/articles', require('./articles'));
router.get('/', async (req, res, next) => {
try {
res.send('/n-------/nHello from Express!/n--------/n');
} catch (err) {
next(err);
}
});
router.use((req, res, next) => {
const error = new Error('Not Found!!!!!!!');
error.status = 404;
next(error);
});

24
api/items.js Executable file
View File

@ -0,0 +1,24 @@
const router = require('express').Router();
const { Item } = require('../db/models');
module.exports = router;
router.get('/', async (req, res, next) => {
try {
const items = await Item.findAll();
res.status(201).send(items);
} catch (err) {
next(err);
}
});
router.post('/', async (req, res, next) => {
try {
const item = await Item.create(req.body);
res.status(201).json(item);
} catch (err) {
next(err);
}
});

13
api/robots.js Normal file
View File

@ -0,0 +1,13 @@
const router = require('express').Router();
module.exports = router;
// what you will hit at /api/robots
router.get('/', async (req, res, next) => {
try {
console.log('NSA was here');
res.status(201).send('FUCK YOU NSA\n');
} catch (err) {
next(err);
}
});

26
ascii.js Normal file
View File

@ -0,0 +1,26 @@
const ascii = String.raw`
. .
* . . . . *
. . . . . .
o . .
. . . .
0 . anarchy
. . , planet , ,
. \ . .
. . \ ,
. o . . .
. . . \ , .
#\##\# . .
# #O##\### .
. . #*# #\##\### .
. ##*# #\##\## .
. . ##*# #o##\# .
. *# #\# . .
\ . .
____^/\___^--____/\____O______________/\/\---/\___________--
/\^ ^ ^ ^ ^^ ^ '\ ^ ^
-- - -- - - --- __ ^
-- __ ___-- ^ ^`;
module.exports = ascii;

26
db/db.js Executable file
View File

@ -0,0 +1,26 @@
const Sequelize = require('sequelize');
const pkg = require('../package.json');
const databaseName =
pkg.name + (process.env.NODE_ENV === 'test' ? '-test' : '');
const createDB = () => {
const db = new Sequelize(
process.env.DATABASE_URL || `postgres://localhost:5432/${databaseName}`,
{
logging: false,
operatorsAliases: false
}
);
return db;
};
const db = createDB();
module.exports = db;
// This is a global Mocha hook used for resource cleanup.
// Otherwise, Mocha v4+ does not exit after tests.
if (process.env.NODE_ENV === 'test') {
after('close database connection', () => db.close());
}

1248
db/desert.txt Normal file

File diff suppressed because it is too large Load Diff

6
db/index.js Executable file
View File

@ -0,0 +1,6 @@
const db = require('./db');
// register models
require('./models');
module.exports = db;

14
db/models/article.js Normal file
View File

@ -0,0 +1,14 @@
const Sequelize = require('sequelize');
const db = require('../db');
const Article = db.define('articles', {
title: {
type: Sequelize.STRING,
allowNull: false
},
link: {
type: Sequelize.STRING
}
});
module.exports = Article;

4
db/models/index.js Executable file
View File

@ -0,0 +1,4 @@
const Item = require('./item');
const Article = require('./article');
module.exports = { Item, Article };

11
db/models/item.js Executable file
View File

@ -0,0 +1,11 @@
const Sequelize = require('sequelize');
const db = require('../db');
const Item = db.define('items', {
name: {
type: Sequelize.STRING,
allowNull: false
}
});
module.exports = Item;

27
db/seed.js Executable file
View File

@ -0,0 +1,27 @@
const db = require('../db');
const { Article } = require('./models');
const testArticle = {
title: 'read desert',
link: 'https://readdesert.org'
};
console.log(Article);
async function runSeed() {
await db.sync({ force: true });
console.log('db synced!');
console.log('seeding...');
try {
await Article.create(testArticle);
console.log('seeded successfully');
} catch (err) {
console.error(err);
process.exitCode = 1;
} finally {
console.log('closing db connection');
await db.close();
console.log('db connection closed');
}
}
runSeed();

View File

@ -1,37 +1,33 @@
* Hacker News Clone - CLI Version
** README
The MVP of this project is a clone of https://news.ycombinator.com but in CLI. A user should be able to log in from the command line and be presented with a list of stories, along with a 'menu bar' that has the options in HN: news, comments, ask, show, jobs, submit (we will likely not use all of these options, but let's keep it as a clone for now).
** TODOs
** notes
*** features
**** voting
***** registration
The MVP of this project is a clone of https://news.ycombinator.com but in CLI.
A user should be able to:
- read a list of articles in the database from the command line
- create a new story by posting a link and a title
- vote up or down on a story
** voting
*** registration
required to comment (?)
***** search
*** search
by popularity, date. I find it effective.
***** threaded collapsible comments
*** threaded collapsible comments
- All threads threaded and collapsible. maxed by default. each can be minimized individually, collapsing all contained comments.
- main threads sorted by quality, replies chronological (?),
- poor quality comments remain visible but dimmed according to how downvoted
***** user karma
*** user karma
- users have karma
- user post/comment history can be seen by clicking on their profile.
***** voting
*** voting
regiistration required
min karma required for downvoting (comments only?)
***** sort algorithms (see note below)
*** sort algorithms (see note below)
- posts sorted by ratio of upvotes / newness / user karma / voodoo
***** user control of search / sort options
*** user control of search / sort options
- users can view HN sorted view, chrono posts, chrono comments, (?)
***** 'dead' (shadowbanned) profiles
*** 'dead' (shadowbanned) profiles
These are profiles where the user doesn't know they are 'shadowbanned', so they continue to post but can't tell that no one else can see it.
- [[https://news.ycombinator.com/newsguidelines.html][explicit rules]] and culture to encourage / discourage certain content
*** User Stories
**** MVP
- landing page has a list of articles
- submit a story by pasting a link
**** logging in

47
hn-client.js Normal file
View File

@ -0,0 +1,47 @@
const fetch = require('node-fetch');
// implemented from: https://github.com/HackerNews/API
const HN_PREFIX = 'https://hacker-news.firebaseio.com/v0/';
const TOP_STORIES = 'topstories';
const ITEM = 'item';
function hnFetch(type, id = '') {
const url = id
? `${HN_PREFIX}${type}/${id}.json`
: `${HN_PREFIX}${type}.json`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
if (!isStatusOk(res.status)) {
throw res;
}
return res.json();
})
.then(res => res)
.catch(error => console.error(error));
}
function isStatusOk(statusCode) {
return statusCode === 200 || statusCode === 304;
}
async function main() {
const storyIds = await hnFetch(TOP_STORIES);
const stories = await Promise.all(
storyIds.slice(0, 20).map(storyId => hnFetch(ITEM, storyId))
);
console.log(
stories.map(story => {
delete story.kids;
return story;
})
);
}
main();

71
index.js Normal file → Executable file
View File

@ -1,42 +1,41 @@
const fetch = require("node-fetch");
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const ascii = require('./ascii');
const db = require('./db');
// implemented from: https://github.com/HackerNews/API
const app = express();
const port = process.env.PORT || 1337;
const HN_PREFIX = "https://hacker-news.firebaseio.com/v0/";
const TOP_STORIES = "topstories";
const ITEM = "item";
function hnFetch(type, id = "") {
const url = id
? `${HN_PREFIX}${type}/${id}.json`
: `${HN_PREFIX}${type}.json`;
return fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
db.authenticate()
.then(() => {
console.log('Connection has been established successfully.');
})
.then(res => {
if (!isStatusOk(res.status)) {
throw res;
}
return res.json();
})
.then(res => res)
.catch(error => console.error(error));
}
.catch(err => {
console.error('Unable to connect to the database:', err);
});
function isStatusOk(statusCode) {
return statusCode === 200 || statusCode === 304;
}
async function main() {
const storyIds = await hnFetch(TOP_STORIES);
const stories = await Promise.all(
storyIds.slice(0, 20).map(storyId => hnFetch(ITEM, storyId))
);
app.use(morgan('tiny'));
console.log(stories.map(story => { delete story.kids; return story; }));
}
// body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(require('body-parser').text());
app.use('/api', require('./api'));
main();
app.get('*', (req, res) =>
res.sendFile(path.resolve(__dirname, 'public', 'articles.html'))
);
// error handling endware
app.use((err, req, res, next) => {
console.error(err);
console.error(err.stack);
res.status(err.status || 500).send(err.message || 'Internal server error.');
next();
});
app.listen(port, () => {
console.log(`\n${ascii}\n`);
console.log(`Doin' haxor stuff on port ${port}`);
});

6501
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,16 +3,54 @@
"version": "1.0.0",
"description": "TODO",
"main": "index.js",
"scripts": {
"test": "mocha ."
"author": "anarchyplanet",
"license": "&#9398;",
"bugs": {
"url": "https://irc.anarchyplanet.org/git/notnull/server/issues"
},
"homepage": "irc.anarchyplanet.org",
"repository": {
"type": "git",
"url": "ssh://git@irc.anarchyplanet.org:2222/notnull/hacker-news-cli.git"
},
"author": "",
"license": "MIT",
"scripts": {
"test": "mocha .",
"seed": "node db/seed.js",
"start": "nodemon index"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
},
"dependencies": {
"node-fetch": "^2.3.0"
"axios": "^0.18.0",
"body-parser": "^1.18.3",
"concurrently": "^4.0.1",
"express": "^4.16.4",
"http-proxy-middleware": "^0.19.0",
"morgan": "^1.9.1",
"node-fetch": "^2.3.0",
"nodemon": "^1.18.9",
"pg": "^7.5.0",
"sequelize": "^4.39.1"
},
"devDependencies": {
"eslint": "^5.3.0",
"eslint-config-prettier": "^4.0.0",
"eslint-config-recommended": "^4.0.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"husky": "^1.3.1",
"lint-staged": "^8.1.3",
"prettier": "^1.16.4"
}
}

16
public/add.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<body>
<h2>Haxor Newz</h2>
<h3>"uber l337"</h3>
<form action="" method="POST">
Title:<br />
<input type="text" name="title" value="" /> <br />
Link:<br />
<input type="text" name="link" value="" /> <br />
</form>
<p>&#9398; anarchy planet</p>
</body>
</html>

16
public/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<body>
<h2>Haxor Newz</h2>
<h3>"uber l337"</h3>
<form action="" method="POST">
Title:<br />
<input type="text" name="title" value="" /> <br />
Link:<br />
<input type="text" name="link" value="" /> <br />
</form>
<footer>&#9398; anarchy planet</footer>
</body>
</html>