Compare commits
6 Commits
a108d90a3d
...
3f8e8479d0
Author | SHA1 | Date | |
---|---|---|---|
|
3f8e8479d0 | ||
ed33f010d5 | |||
|
2ac6cc2ff2 | ||
|
1a2a77beca | ||
|
9e90002113 | ||
8b89ec32f1 |
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
footer.html
|
||||
table.html
|
43
api/articles.js
Normal file
43
api/articles.js
Normal 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
22
api/buildPage.js
Normal 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>Ⓐ anarchy planet</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
`;
|
20
api/index.js
Executable file
20
api/index.js
Executable 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
24
api/items.js
Executable 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
13
api/robots.js
Normal 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
26
ascii.js
Normal 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
26
db/db.js
Executable 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
1248
db/desert.txt
Normal file
File diff suppressed because it is too large
Load Diff
6
db/index.js
Executable file
6
db/index.js
Executable file
@ -0,0 +1,6 @@
|
||||
const db = require('./db');
|
||||
|
||||
// register models
|
||||
require('./models');
|
||||
|
||||
module.exports = db;
|
14
db/models/article.js
Normal file
14
db/models/article.js
Normal 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
4
db/models/index.js
Executable file
@ -0,0 +1,4 @@
|
||||
const Item = require('./item');
|
||||
const Article = require('./article');
|
||||
|
||||
module.exports = { Item, Article };
|
11
db/models/item.js
Executable file
11
db/models/item.js
Executable 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
27
db/seed.js
Executable 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();
|
@ -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
47
hn-client.js
Normal 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
71
index.js
Normal file → Executable 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
6501
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@ -3,16 +3,54 @@
|
||||
"version": "1.0.0",
|
||||
"description": "TODO",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha ."
|
||||
"author": "anarchyplanet",
|
||||
"license": "Ⓐ",
|
||||
"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
16
public/add.html
Normal 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>Ⓐ anarchy planet</p>
|
||||
</body>
|
||||
</html>
|
16
public/index.html
Normal file
16
public/index.html
Normal 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>Ⓐ anarchy planet</footer>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user