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 * 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). The MVP of this project is a clone of https://news.ycombinator.com but in CLI.
** TODOs
** notes A user should be able to:
*** features - read a list of articles in the database from the command line
**** voting - create a new story by posting a link and a title
***** registration - vote up or down on a story
** voting
*** registration
required to comment (?) required to comment (?)
***** search *** search
by popularity, date. I find it effective. 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. - All threads threaded and collapsible. maxed by default. each can be minimized individually, collapsing all contained comments.
- main threads sorted by quality, replies chronological (?), - main threads sorted by quality, replies chronological (?),
- poor quality comments remain visible but dimmed according to how downvoted - poor quality comments remain visible but dimmed according to how downvoted
***** user karma *** user karma
- users have karma - users have karma
- user post/comment history can be seen by clicking on their profile. - user post/comment history can be seen by clicking on their profile.
***** voting *** voting
regiistration required regiistration required
min karma required for downvoting (comments only?) 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 - 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, (?) - 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. 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 - [[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/"; db.authenticate()
.then(() => {
const TOP_STORIES = "topstories"; console.log('Connection has been established successfully.');
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 => { .catch(err => {
if (!isStatusOk(res.status)) { console.error('Unable to connect to the database:', err);
throw res; });
}
return res.json();
})
.then(res => res)
.catch(error => console.error(error));
}
function isStatusOk(statusCode) { app.use(morgan('tiny'));
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; })); // 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", "version": "1.0.0",
"description": "TODO", "description": "TODO",
"main": "index.js", "main": "index.js",
"scripts": { "author": "anarchyplanet",
"test": "mocha ." "license": "&#9398;",
"bugs": {
"url": "https://irc.anarchyplanet.org/git/notnull/server/issues"
}, },
"homepage": "irc.anarchyplanet.org",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "ssh://git@irc.anarchyplanet.org:2222/notnull/hacker-news-cli.git" "url": "ssh://git@irc.anarchyplanet.org:2222/notnull/hacker-news-cli.git"
}, },
"author": "", "scripts": {
"license": "MIT", "test": "mocha .",
"seed": "node db/seed.js",
"start": "nodemon index"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
},
"dependencies": { "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>