Skip to content

Commit f890ef2

Browse files
committed
feat: add tegg hacknews
1 parent 3ae61c2 commit f890ef2

File tree

22 files changed

+657
-0
lines changed

22 files changed

+657
-0
lines changed

hackernews-tegg/.eslintrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"root": true,
3+
"extends": "eslint-config-egg/typescript"
4+
}
5+

hackernews-tegg/.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
logs/
2+
npm-debug.log
3+
node_modules/
4+
coverage/
5+
.idea/
6+
run/
7+
logs/
8+
.DS_Store
9+
.vscode
10+
*.swp
11+
*.lock
12+
*.js
13+
14+
app/**/*.js
15+
test/**/*.js
16+
config/**/*.js
17+
app/**/*.map
18+
test/**/*.map
19+
config/**/*.map

hackernews-tegg/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# hackernews-async-ts
2+
3+
[Hacker News](https://news.ycombinator.com/) showcase using typescript && egg
4+
5+
## QuickStart
6+
7+
### Development
8+
9+
```bash
10+
$ npm i
11+
$ npm run dev
12+
$ open http://localhost:7001/
13+
```
14+
15+
Don't tsc compile at development mode, if you had run `tsc` then you need to `npm run clean` before `npm run dev`.
16+
17+
### Deploy
18+
19+
```bash
20+
$ npm run tsc
21+
$ npm start
22+
```
23+
24+
### Npm Scripts
25+
26+
- Use `npm run lint` to check code style
27+
- Use `npm test` to run unit test
28+
- se `npm run clean` to clean compiled js at development mode once
29+
30+
### Requirement
31+
32+
- Node.js 16.x
33+
- Typescript 4.x
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Controller } from 'egg';
2+
3+
export default class NewsController extends Controller {
4+
public async list() {
5+
const { ctx, app } = this;
6+
const pageSize = app.config.news.pageSize;
7+
const page = parseInt(ctx.query.page, 10) || 1;
8+
9+
const idList = await ctx.service.news.getTopStories(page);
10+
11+
// get itemInfo parallel
12+
const newsList = await Promise.all(idList.map(id => ctx.service.news.getItem(id)));
13+
await ctx.render('news/list.tpl', { list: newsList, page, pageSize });
14+
}
15+
16+
public async detail() {
17+
const { ctx } = this;
18+
const id = ctx.params.id;
19+
const newsInfo = await ctx.service.news.getItem(id);
20+
// get comment parallel
21+
const commentList = await Promise.all(newsInfo.kids.map(_id => ctx.service.news.getItem(_id)));
22+
await ctx.render('news/detail.tpl', { item: newsInfo, comments: commentList });
23+
}
24+
25+
public async user() {
26+
const { ctx } = this;
27+
const id = ctx.params.id;
28+
const userInfo = await ctx.service.news.getUser(id);
29+
await ctx.render('news/user.tpl', { user: userInfo });
30+
}
31+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import moment from 'moment';
2+
3+
export function relativeTime(time) {
4+
return moment(new Date(time * 1000)).fromNow();
5+
}
6+
7+
export function domain(url) {
8+
return url && url.split('/')[2];
9+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
body,
2+
html {
3+
font-family: Verdana;
4+
font-size: 13px;
5+
height: 100%
6+
}
7+
ul {
8+
list-style-type: none;
9+
padding: 0;
10+
margin: 0
11+
}
12+
a {
13+
color: #000;
14+
cursor: pointer;
15+
text-decoration: none
16+
}
17+
#wrapper {
18+
background-color: #f6f6ef;
19+
width: 85%;
20+
min-height: 80px;
21+
margin: 0 auto
22+
}
23+
#header,
24+
#wrapper {
25+
position: relative
26+
}
27+
#header {
28+
background-color: #f60;
29+
height: 24px
30+
}
31+
#header h1 {
32+
font-weight: 700;
33+
font-size: 13px;
34+
display: inline-block;
35+
vertical-align: middle;
36+
margin: 0
37+
}
38+
#header .source {
39+
color: #fff;
40+
font-size: 11px;
41+
position: absolute;
42+
top: 4px;
43+
right: 4px
44+
}
45+
#header .source a {
46+
color: #fff
47+
}
48+
#header .source a:hover {
49+
text-decoration: underline
50+
}
51+
#yc {
52+
border: 1px solid #fff;
53+
margin: 2px;
54+
display: inline-block
55+
}
56+
#yc,
57+
#yc img {
58+
vertical-align: middle
59+
}
60+
.view {
61+
position: absolute;
62+
background-color: #f6f6ef;
63+
width: 100%;
64+
-webkit-transition: opacity .2s ease;
65+
transition: opacity .2s ease;
66+
box-sizing: border-box;
67+
padding: 8px 20px
68+
}
69+
.view.v-enter,
70+
.view.v-leave {
71+
opacity: 0
72+
}
73+
@media screen and (max-width: 700px) {
74+
body,
75+
html {
76+
margin: 0
77+
}
78+
#wrapper {
79+
width: 100%
80+
}
81+
}
82+
.news-view {
83+
padding-left: 5px;
84+
padding-right: 15px
85+
}
86+
.news-view.loading:before {
87+
content: "Loading...";
88+
position: absolute;
89+
top: 16px;
90+
left: 20px
91+
}
92+
.news-view .nav {
93+
padding: 10px 10px 10px 40px;
94+
margin-top: 10px;
95+
border-top: 2px solid #f60
96+
}
97+
.news-view .nav a {
98+
margin-right: 10px
99+
}
100+
.news-view .nav a:hover {
101+
text-decoration: underline
102+
}
103+
.item {
104+
padding: 2px 0 2px 40px;
105+
position: relative;
106+
-webkit-transition: background-color .2s ease;
107+
transition: background-color .2s ease
108+
}
109+
.item p {
110+
margin: 2px 0
111+
}
112+
.item .index,
113+
.item .title:visited {
114+
color: #828282
115+
}
116+
.item .index {
117+
position: absolute;
118+
width: 30px;
119+
text-align: right;
120+
left: 0;
121+
top: 4px
122+
}
123+
.item .domain,
124+
.item .subtext {
125+
font-size: 11px;
126+
color: #828282
127+
}
128+
.item .domain a,
129+
.item .subtext a {
130+
color: #828282
131+
}
132+
.item .subtext a:hover {
133+
text-decoration: underline
134+
}
135+
.item-view .item {
136+
padding-left: 0;
137+
margin-bottom: 30px
138+
}
139+
.item-view .item .index {
140+
display: none
141+
}
142+
.item-view .poll-options {
143+
margin-left: 30px;
144+
margin-bottom: 40px
145+
}
146+
.item-view .poll-options li {
147+
margin: 12px 0
148+
}
149+
.item-view .poll-options p {
150+
margin: 8px 0
151+
}
152+
.item-view .poll-options .subtext {
153+
color: #828282;
154+
font-size: 11px
155+
}
156+
.item-view .itemtext {
157+
color: #828282;
158+
margin-top: 0;
159+
margin-bottom: 30px
160+
}
161+
.item-view .itemtext p {
162+
margin: 10px 0
163+
}
164+
.comhead {
165+
font-size: 11px;
166+
margin-bottom: 8px
167+
}
168+
.comhead,
169+
.comhead a {
170+
color: #828282
171+
}
172+
.comhead a:hover {
173+
text-decoration: underline
174+
}
175+
.comhead .toggle {
176+
margin-right: 4px
177+
}
178+
.comment-content {
179+
margin: 0 0 16px 24px;
180+
word-wrap: break-word
181+
}
182+
.comment-content code {
183+
white-space: pre-wrap
184+
}
185+
.child-comments {
186+
margin: 8px 0 8px 22px
187+
}
188+
.user-view {
189+
color: #828282
190+
}
191+
.user-view li {
192+
margin: 5px 0
193+
}
194+
.user-view .label {
195+
display: inline-block;
196+
min-width: 60px
197+
}
198+
.user-view .about {
199+
margin-top: 1em
200+
}
201+
.user-view .links a {
202+
text-decoration: underline
203+
}
3.04 KB
Loading

hackernews-tegg/app/router.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Application } from 'egg';
2+
3+
export default (app: Application) => {
4+
const { controller, router } = app;
5+
6+
router.redirect('/', '/news');
7+
router.get('/news', controller.news.list);
8+
router.get('/news/item/:id', controller.news.detail);
9+
router.get('/news/user/:id', controller.news.user);
10+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Service } from 'egg';
2+
3+
export interface NewsItem {
4+
id: number;
5+
score: number;
6+
time: number;
7+
title: string;
8+
type: string;
9+
url: string;
10+
descendants: number;
11+
kids: number[];
12+
by: string;
13+
}
14+
15+
/**
16+
* HackerNews Api Service
17+
*/
18+
export class HackerNews extends Service {
19+
/**
20+
* request hacker-news api
21+
* @param api - Api name
22+
* @param opts - urllib options
23+
*/
24+
public async request(api: string, opts?: any) {
25+
const options = {
26+
dataType: 'json',
27+
timeout: '30s',
28+
...opts,
29+
};
30+
31+
const result = await this.ctx.curl(`${this.config.news.serverUrl}/${api}`, options);
32+
return result.data;
33+
}
34+
35+
/**
36+
* get top story ids
37+
* @param page - page number, 1-ase
38+
* @param pageSize - page count
39+
*/
40+
public async getTopStories(page?: number, pageSize?: number): Promise<number[]> {
41+
page = page || 1;
42+
const requestPageSize = pageSize ?? this.config.news.pageSize;
43+
44+
try {
45+
const result = await this.request('topstories.json', {
46+
data: {
47+
orderBy: '"$key"',
48+
startAt: `"${requestPageSize * (page - 1)}"`,
49+
endAt: `"${requestPageSize * page - 1}"`,
50+
},
51+
});
52+
return Object.keys(result).map(key => result[key]);
53+
} catch (e) {
54+
this.ctx.logger.error(e);
55+
return [];
56+
}
57+
}
58+
59+
/**
60+
* query item
61+
* @param id - itemId
62+
*/
63+
public async getItem(id: number): Promise<NewsItem> {
64+
return await this.request(`item/${id}.json`);
65+
}
66+
67+
/**
68+
* get user info
69+
* @param id - userId
70+
*/
71+
public async getUser(id: number) {
72+
return await this.request(`user/${id}.json`);
73+
}
74+
}
75+
76+
export default HackerNews;

0 commit comments

Comments
 (0)