├── README.md
├── public
├── 192x192.png
├── 384x384.png
├── 512x512.png
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.test.js
├── Default.js
├── reducers
│ ├── index.js
│ ├── story.js
│ └── comment.js
├── const
│ └── index.js
├── components
│ ├── Spinner.js
│ ├── CommentItem.js
│ ├── Spinner.css
│ ├── Comment.js
│ └── Item.js
├── actions
│ └── comment.js
├── Ask.js
├── Jobs.js
├── Show.js
├── Newest.js
├── App.js
├── Story.js
├── Container.js
├── registerServiceWorker.js
├── index.css
└── index.js
├── .gitignore
├── package.json
└── firebase.json
/README.md:
--------------------------------------------------------------------------------
1 | # hackerNews-pwa
2 |
3 | hacker news pwa
4 |
--------------------------------------------------------------------------------
/public/192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stereobooster/hackerNews-pwa/master/public/192x192.png
--------------------------------------------------------------------------------
/public/384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stereobooster/hackerNews-pwa/master/public/384x384.png
--------------------------------------------------------------------------------
/public/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stereobooster/hackerNews-pwa/master/public/512x512.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stereobooster/hackerNews-pwa/master/public/favicon.ico
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | it("renders without crashing", () => {
6 | const div = document.createElement("div");
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/Default.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Redirect } from "react-router-dom";
3 |
4 | class Default extends Component {
5 | render() {
6 | return ;
7 | }
8 | }
9 |
10 | export default Default;
11 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import comment from "./comment";
3 | import story from "./story";
4 |
5 | const rootReducer = combineReducers({
6 | comment,
7 | story
8 | });
9 |
10 | export default rootReducer;
11 |
--------------------------------------------------------------------------------
/src/const/index.js:
--------------------------------------------------------------------------------
1 | export const NEWS_LIST = "NEWS_LIST";
2 | export const SHOW_LIST = "SHOW_LIST";
3 | export const ASK_LIST = "ASK_LIST";
4 | export const NEWEST_LIST = "NEWEST_LIST";
5 | export const JOBS_LIST = "JOBS_LIST";
6 |
7 | export const STORY = "STORY";
8 |
--------------------------------------------------------------------------------
/src/components/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Spinner.css";
3 |
4 | export default () => (
5 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/reducers/story.js:
--------------------------------------------------------------------------------
1 | import { STORY } from "../const";
2 |
3 | const initState = {};
4 |
5 | export default (state = initState, action) => {
6 | switch (action.type) {
7 | case STORY:
8 | return {
9 | state: action.data
10 | };
11 | default:
12 | return state;
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | .firebaserc
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | HNPWA
10 |
11 |
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "HN PWA",
3 | "name": "Hacker News PWA",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "384x384.png",
12 | "sizes": "384x384",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "512x512.png",
17 | "sizes": "512x512",
18 | "type": "image/png"
19 | }
20 | ],
21 | "start_url": "./index.html",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/actions/comment.js:
--------------------------------------------------------------------------------
1 | export const getData = (type, page) => dispatch => {
2 | fetch(`https://node-hnapi.herokuapp.com/${type}?page=${page}`)
3 | .then(res => res.json())
4 | .then(data => {
5 | dispatch(receiveData(`${type.toUpperCase()}_LIST`, data));
6 | });
7 | };
8 |
9 | export const getItem = id => dispatch => {
10 | fetch(`https://node-hnapi.herokuapp.com/item/${id}`)
11 | .then(res => res.json())
12 | .then(data => {
13 | dispatch(receiveData("STORY", data));
14 | });
15 | };
16 |
17 | const receiveData = (type, data) => {
18 | return {
19 | type,
20 | data
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/CommentItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default props => {
4 | return (
5 | reply(props.id)}
11 | >
12 |
{props.user}
13 |
14 |
{props.timeAgo}
15 |
16 |
17 | );
18 | };
19 |
20 | const reply = id => {
21 | window.location.href = `https://news.ycombinator.com/reply?id=${id}`;
22 | };
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hacker-news-pwa",
3 | "version": "0.1.3",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.2.0",
7 | "react-dom": "^16.2.0",
8 | "react-redux": "^5.0.5",
9 | "react-router": "^4.1.1",
10 | "react-router-dom": "^4.1.1",
11 | "redux": "^3.6.0",
12 | "redux-thunk": "^2.2.0"
13 | },
14 | "devDependencies": {
15 | "prettier": "^1.10.2",
16 | "react-scripts": "1.0.7"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "deploy": "firebase deploy --only hosting",
22 | "push": "yarn build && yarn deploy",
23 | "test": "react-scripts test --env=jsdom",
24 | "eject": "react-scripts eject",
25 | "prettier": "prettier --write 'src/**/*.{js,css}'"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Ask.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { getData } from "./actions/comment";
4 | import Container from "./Container";
5 |
6 | class Ask extends Container {
7 | componentWillMount() {
8 | const page = this.props.match.params.page;
9 | this.props.dispatch(getData("ask", page));
10 | }
11 |
12 | componentWillReceiveProps(nextProps) {
13 | const newPage = nextProps.match.params.page;
14 | const page = this.props.match.params.page;
15 | if (newPage !== page) {
16 | this.props.dispatch(getData("ask", newPage));
17 | }
18 | }
19 |
20 | render() {
21 | const { ask } = this.props;
22 | return (
23 |
24 | {this.renderPage("ask")}
25 | {this.renderList(ask)}
26 |
27 | );
28 | }
29 | }
30 |
31 | const mapStateToProps = state => state.comment;
32 |
33 | export default connect(mapStateToProps)(Ask);
34 |
--------------------------------------------------------------------------------
/src/Jobs.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { getData } from "./actions/comment";
4 | import Container from "./Container";
5 |
6 | class Jobs extends Container {
7 | componentWillMount() {
8 | const page = this.props.match.params.page;
9 | this.props.dispatch(getData("jobs", page));
10 | }
11 |
12 | componentWillReceiveProps(nextProps) {
13 | const newPage = nextProps.match.params.page;
14 | const page = this.props.match.params.page;
15 | if (newPage !== page) {
16 | this.props.dispatch(getData("jobs", newPage));
17 | }
18 | }
19 |
20 | render() {
21 | const { jobs } = this.props;
22 | return (
23 |
24 | {this.renderPage("jobs")}
25 | {this.renderList(jobs)}
26 |
27 | );
28 | }
29 | }
30 |
31 | const mapStateToProps = state => state.comment;
32 |
33 | export default connect(mapStateToProps)(Jobs);
34 |
--------------------------------------------------------------------------------
/src/Show.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { getData } from "./actions/comment";
4 | import Container from "./Container";
5 |
6 | class Show extends Container {
7 | componentWillMount() {
8 | const page = this.props.match.params.page;
9 | this.props.dispatch(getData("show", page));
10 | }
11 |
12 | componentWillReceiveProps(nextProps) {
13 | const newPage = nextProps.match.params.page;
14 | const page = this.props.match.params.page;
15 | if (newPage !== page) {
16 | this.props.dispatch(getData("show", newPage));
17 | }
18 | }
19 |
20 | render() {
21 | const { show } = this.props;
22 | return (
23 |
24 | {this.renderPage("show")}
25 | {this.renderList(show)}
26 |
27 | );
28 | }
29 | }
30 |
31 | const mapStateToProps = state => state.comment;
32 |
33 | export default connect(mapStateToProps)(Show);
34 |
--------------------------------------------------------------------------------
/src/Newest.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { getData } from "./actions/comment";
4 | import Container from "./Container";
5 |
6 | class Newest extends Container {
7 | componentWillMount() {
8 | const page = this.props.match.params.page;
9 | this.props.dispatch(getData("newest", page));
10 | }
11 |
12 | componentWillReceiveProps(nextProps) {
13 | const newPage = nextProps.match.params.page;
14 | const page = this.props.match.params.page;
15 | if (newPage !== page) {
16 | this.props.dispatch(getData("newest", newPage));
17 | }
18 | }
19 |
20 | render() {
21 | const { newest } = this.props;
22 | return (
23 |
24 | {this.renderPage("newest")}
25 | {this.renderList(newest)}
26 |
27 | );
28 | }
29 | }
30 |
31 | const mapStateToProps = state => state.comment;
32 |
33 | export default connect(mapStateToProps)(Newest);
34 |
--------------------------------------------------------------------------------
/src/reducers/comment.js:
--------------------------------------------------------------------------------
1 | import {
2 | NEWS_LIST,
3 | SHOW_LIST,
4 | ASK_LIST,
5 | NEWEST_LIST,
6 | JOBS_LIST
7 | } from "../const";
8 |
9 | const initState = {
10 | news: [],
11 | show: [],
12 | ask: [],
13 | newest: [],
14 | jobs: []
15 | };
16 |
17 | export default (state = initState, action) => {
18 | switch (action.type) {
19 | case NEWS_LIST:
20 | return {
21 | ...state,
22 | news: action.data
23 | };
24 | case SHOW_LIST:
25 | return {
26 | ...state,
27 | show: action.data
28 | };
29 | case ASK_LIST:
30 | return {
31 | ...state,
32 | ask: action.data
33 | };
34 | case NEWEST_LIST:
35 | return {
36 | ...state,
37 | newest: action.data
38 | };
39 | case JOBS_LIST:
40 | return {
41 | ...state,
42 | jobs: action.data
43 | };
44 | default:
45 | return state;
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { getData } from "./actions/comment";
4 | import Container from "./Container";
5 |
6 | class App extends Container {
7 | componentWillMount() {
8 | const page = this.props.match.params.page;
9 | this.props.dispatch(getData("news", page));
10 | }
11 |
12 | componentWillReceiveProps(nextProps, nextState, context) {
13 | const newPage = nextProps.match.params.page;
14 | const page = this.props.match.params.page;
15 | if (newPage !== page) {
16 | this.props.dispatch(getData("news", newPage));
17 | }
18 | }
19 |
20 | render() {
21 | const { news } = this.props;
22 | return (
23 |
24 | {this.renderPage()}
25 | {this.renderList(news)}
26 |
27 | );
28 | }
29 | }
30 |
31 | const mapStateToProps = state => {
32 | return state.comment;
33 | };
34 |
35 | export default connect(mapStateToProps)(App);
36 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "rewrites": [
5 | {
6 | "source": "**",
7 | "destination": "/index.html"
8 | }
9 | ],
10 | "ignore": [
11 | "firebase.json",
12 | "**/.*",
13 | "**/node_modules/**"
14 | ],
15 | "headers": [
16 | {
17 | "source": "service-worker.js",
18 | "headers": [
19 | {
20 | "key": "Cache-Control",
21 | "value": "max-age=0"
22 | }
23 | ]
24 | },
25 | {
26 | "source": "static/**/*.@(css|js|map)",
27 | "headers": [
28 | {
29 | "key": "Cache-Control",
30 | "value": "max-age=31536000"
31 | }
32 | ]
33 | },
34 | {
35 | "source": "**",
36 | "headers": [
37 | {
38 | "key": "Link",
39 | "value": ";rel=preconnect;crossorigin"
40 | }
41 | ]
42 | }
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Spinner.css:
--------------------------------------------------------------------------------
1 | .spinner {
2 | margin: 100px auto 0;
3 | width: 70px;
4 | text-align: center;
5 | }
6 |
7 | .spinner > div {
8 | width: 18px;
9 | height: 18px;
10 | background-color: #fb651e;
11 |
12 | border-radius: 100%;
13 | display: inline-block;
14 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
15 | animation: sk-bouncedelay 1.4s infinite ease-in-out both;
16 | }
17 |
18 | .spinner .bounce1 {
19 | -webkit-animation-delay: -0.32s;
20 | animation-delay: -0.32s;
21 | }
22 |
23 | .spinner .bounce2 {
24 | -webkit-animation-delay: -0.16s;
25 | animation-delay: -0.16s;
26 | }
27 |
28 | @-webkit-keyframes sk-bouncedelay {
29 | 0%,
30 | 80%,
31 | 100% {
32 | -webkit-transform: scale(0);
33 | }
34 | 40% {
35 | -webkit-transform: scale(1);
36 | }
37 | }
38 |
39 | @keyframes sk-bouncedelay {
40 | 0%,
41 | 80%,
42 | 100% {
43 | -webkit-transform: scale(0);
44 | transform: scale(0);
45 | }
46 | 40% {
47 | -webkit-transform: scale(1);
48 | transform: scale(1);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Comment.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import CommentItem from "./CommentItem";
3 |
4 | export default class Comment extends Component {
5 | constructor() {
6 | super();
7 | this.element = [];
8 | }
9 |
10 | renderChildren(data, type = 1) {
11 | if (data.comments) {
12 | data.comments.forEach((ele, key) => {
13 | this.element.push(
14 |
22 | );
23 | this.renderChildren(ele, key + 2);
24 | });
25 | }
26 | }
27 |
28 | renderComponent(data) {
29 | this.element = [
30 |
37 | ];
38 | this.renderChildren(data);
39 | return this.element;
40 | }
41 |
42 | render() {
43 | return {this.renderComponent(this.props.data)}
;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Item.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | export default class Item extends Component {
5 | jumpUrl(event, url) {
6 | event.preventDefault();
7 | try {
8 | window.open(url);
9 | } catch (e) {
10 | // const id = url.match(/id=([0-9]*)/, 'g');
11 | }
12 | }
13 |
14 | render() {
15 | const { data, index, page } = this.props;
16 | return (
17 |
18 |
27 |
28 |
29 |
30 | {data.points ? `${data.points} by ${data.user} |` : " "}
31 | {" " + data.time_ago} |
32 | {data.comments_count > 0
33 | ? ` ${data.comments_count} comments`
34 | : " discuss"}
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Story.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { getItem } from "./actions/comment";
4 | import Comment from "./components/Comment";
5 |
6 | class Story extends Component {
7 | componentWillMount() {
8 | const id = this.props.match.params.id;
9 | this.props.dispatch(getItem(id));
10 | }
11 |
12 | reply(id) {
13 | window.location.href = `https://news.ycombinator.com/item?id=${id}`;
14 | }
15 |
16 | renderDescription(data) {
17 | if (data && Object.hasOwnProperty.call(data, "title")) {
18 | return (
19 |
20 |
25 |
this.reply(data.id)}>
26 | - points: {data.points}
27 | - comments_count: {data.comments_count}
28 | - time_ago: {data.time_ago}
29 | - reply
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | renderComments(data) {
37 | if (
38 | data &&
39 | Object.hasOwnProperty.call(data, "comments") &&
40 | data.comments.length > 0
41 | ) {
42 | return (
43 |
44 | {data.comments.map((ele, key) => )}
45 |
46 | );
47 | }
48 | return null;
49 | }
50 |
51 | render() {
52 | const { state } = this.props;
53 | return (
54 |
55 | {this.renderDescription(state)}
56 | {this.renderComments(state)}
57 |
58 | );
59 | }
60 | }
61 |
62 | const mapStateToProps = state => state.story;
63 |
64 | export default connect(mapStateToProps)(Story);
65 |
--------------------------------------------------------------------------------
/src/Container.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 | import Item from "./components/Item";
4 | import Spinner from "./components/Spinner";
5 |
6 | export default class Container extends Component {
7 | constructor() {
8 | super();
9 | this.state = {
10 | page: 1
11 | };
12 | }
13 |
14 | componentDidMount() {
15 | window.scrollTo(0, 0);
16 | }
17 |
18 | scrollTop() {
19 | window.scrollTo(0, 0);
20 | }
21 |
22 | renderItem(data) {
23 | if (data && data.length > 0) {
24 | return data.map((ele, index) => (
25 |
31 | ));
32 | }
33 | }
34 |
35 | renderLoading() {
36 | const { match } = this.props;
37 | const page = Number(match.params.page);
38 | if (page > 1) {
39 | return (
40 |
41 | content is null, please read prev page!
42 |
43 | );
44 | } else {
45 | return (
46 |
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | renderList(data) {
54 | return (
55 |
56 | {(data && data.length) > 0 ? (
57 |
{this.renderItem(data)}
58 | ) : (
59 | this.renderLoading()
60 | )}
61 |
62 | );
63 | }
64 |
65 | renderPage(type) {
66 | const { match } = this.props;
67 | const page = Number(match.params.page);
68 | let newTypePrve;
69 | let newTypeNext;
70 | if (type) {
71 | newTypePrve = `/${type}/${page - 1 > 1 ? page - 1 : 1}`;
72 | newTypeNext = `/${type}/${page + 1}`;
73 | } else {
74 | newTypePrve = `/${page - 1 > 1 ? page - 1 : 1}`;
75 | newTypeNext = `/${page + 1}`;
76 | }
77 | return (
78 |
79 | this.scrollTop()}>
80 | {"← prev "}
81 |
82 | {page}
83 | this.scrollTop()}>
84 | {" next →"}
85 |
86 |
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | export default function register() {
12 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
13 | window.addEventListener("load", () => {
14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
15 | navigator.serviceWorker
16 | .register(swUrl)
17 | .then(registration => {
18 | registration.onupdatefound = () => {
19 | const installingWorker = registration.installing;
20 | installingWorker.onstatechange = () => {
21 | if (installingWorker.state === "installed") {
22 | if (navigator.serviceWorker.controller) {
23 | // At this point, the old content will have been purged and
24 | // the fresh content will have been added to the cache.
25 | // It's the perfect time to display a "New content is
26 | // available; please refresh." message in your web app.
27 | console.log("New content is available; please refresh.");
28 | } else {
29 | // At this point, everything has been precached.
30 | // It's the perfect time to display a
31 | // "Content is cached for offline use." message.
32 | console.log("Content is cached for offline use.");
33 | }
34 | }
35 | };
36 | };
37 | })
38 | .catch(error => {
39 | console.error("Error during service worker registration:", error);
40 | });
41 | });
42 | }
43 | }
44 |
45 | export function unregister() {
46 | if ("serviceWorker" in navigator) {
47 | navigator.serviceWorker.ready.then(registration => {
48 | registration.unregister();
49 | });
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | a {
8 | text-decoration: none;
9 | max-width: 100%;
10 | word-break: break-all;
11 | }
12 |
13 | .link {
14 | text-decoration: underline;
15 | color: #333333;
16 | cursor: pointer;
17 | }
18 |
19 | .user {
20 | text-decoration: underline;
21 | cursor: pointer;
22 | }
23 |
24 | .timeAgo {
25 | color: #999999;
26 | cursor: pointer;
27 | }
28 |
29 | a:link {
30 | color: #333333;
31 | }
32 | a:active {
33 | color: #333333;
34 | }
35 | a:visited {
36 | color: #333333;
37 | }
38 | a:hover {
39 | color: #333333;
40 | }
41 |
42 | ul {
43 | list-style: none;
44 | }
45 |
46 | ol {
47 | display: block;
48 | list-style-type: decimal;
49 | -webkit-margin-before: 1em;
50 | -webkit-margin-after: 1em;
51 | -webkit-margin-start: 0px;
52 | -webkit-margin-end: 0px;
53 | -webkit-padding-start: 40px;
54 | }
55 |
56 | .topbar {
57 | position: fixed;
58 | width: 100%;
59 | top: 0;
60 | z-index: 100;
61 | height: 48px;
62 | padding: 0 16px;
63 | background-color: #222;
64 | display: flex;
65 | align-items: center;
66 | }
67 |
68 | .topbar > ul {
69 | list-style: none;
70 | display: flex;
71 | align-items: center;
72 | height: 100%;
73 | max-width: 1200px;
74 | overflow: hidden;
75 | margin-left: -20px;
76 | }
77 |
78 | .topbar > ul > li {
79 | margin-right: 0.6rem;
80 | color: #ffffff;
81 | }
82 |
83 | .topbar > ul > li > a {
84 | color: #ffffff;
85 | text-decoration: none;
86 | }
87 |
88 | .content {
89 | }
90 |
91 | .content li {
92 | display: flex;
93 | flex-wrap: wrap;
94 | align-items: center;
95 | align-items: center;
96 | margin-left: -1.2rem;
97 | width: 100%;
98 | overflow: hidden;
99 | height: 4rem;
100 | border-bottom: 1px solid #ebebeb;
101 | padding: 0.3rem 0;
102 | }
103 |
104 | .content li div {
105 | width: 100%;
106 | }
107 |
108 | .content li > div:last-child {
109 | color: #999999;
110 | font-size: 0.8rem;
111 | }
112 |
113 | .item-footer {
114 | text-decoration: underline;
115 | }
116 |
117 | .story-top {
118 | border-bottom: 1px solid #ebebeb;
119 | }
120 |
121 | .story-title {
122 | font-size: 1.2rem;
123 | margin-left: 1.2rem;
124 | padding-top: 1rem;
125 | font-weight: bold;
126 | }
127 |
128 | .story-ol > li {
129 | height: 1.2rem;
130 | border-bottom: 0px;
131 | padding: 0px;
132 | color: #999999;
133 | }
134 |
135 | .comment-item {
136 | border-bottom: 1px solid #ebebeb;
137 | padding: 1rem;
138 | font-size: 0.8rem;
139 | max-width: 100%;
140 | }
141 |
142 | .page-navigation {
143 | margin-top: 60px;
144 | width: 100%;
145 | text-align: center;
146 | }
147 |
148 | .footer-page {
149 | width: 100%;
150 | height: 32px;
151 | text-align: center;
152 | line-height: 16px;
153 | }
154 |
155 | .loading {
156 | height: 100%;
157 | width: 100%;
158 | display: flex;
159 | justify-content: center;
160 | align-items: center;
161 | min-height: 400px;
162 | }
163 |
164 | .one-line-ellipsis {
165 | overflow: hidden;
166 | text-overflow: ellipsis;
167 | white-space: nowrap;
168 | }
169 |
170 | .logo {
171 | background: #fb651e;
172 | color: #fff;
173 | width: 20px;
174 | height: 20px;
175 | text-align: center;
176 | vertical-align: middle;
177 | line-height: 23px;
178 | -webkit-touch-callout: none;
179 | -webkit-user-select: none;
180 | -khtml-user-select: none;
181 | -moz-user-select: none;
182 | -ms-user-select: none;
183 | user-select: none;
184 | }
185 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import { createStore, applyMiddleware } from "redux";
5 | import { Provider } from "react-redux";
6 | import thunk from "redux-thunk";
7 | import reducer from "./reducers";
8 |
9 | import { BrowserRouter as Router, Route, Link } from "react-router-dom";
10 |
11 | import Default from "./Default";
12 | import App from "./App";
13 | import Newest from "./Newest";
14 | import Show from "./Show";
15 | import Ask from "./Ask";
16 | import Jobs from "./Jobs";
17 | import Story from "./Story";
18 |
19 | import registerServiceWorker from "./registerServiceWorker";
20 |
21 | import "./index.css";
22 |
23 | const middleware = [thunk];
24 |
25 | const store = createStore(reducer, applyMiddleware(...middleware));
26 |
27 | const defaultChannelList = ["news", "newest", "show", "ask", "jobs"];
28 |
29 | const changeChannel = event => {
30 | const channelList = document.querySelectorAll(".channel > a");
31 | if (channelList && channelList.length > 0) {
32 | for (let i = 0; i < channelList.length; i += 1) {
33 | channelList[i].setAttribute("style", "");
34 | }
35 | }
36 | event.target.setAttribute(
37 | "style",
38 | "border-bottom: 4px solid rgb(255, 102, 0)"
39 | );
40 | };
41 |
42 | const initChannel = () => {
43 | const path = window.location.pathname.match(/\/([a-z]*)\/([0-9]*)/, "g");
44 | let channel = "news";
45 | if (path) {
46 | channel = path[1];
47 | }
48 | const index = defaultChannelList.indexOf(channel);
49 | const channelList = document.querySelectorAll(".channel > a");
50 | index > -1 &&
51 | channelList[index].setAttribute(
52 | "style",
53 | "border-bottom: 4px solid rgb(255, 102, 0)"
54 | );
55 | };
56 |
57 | ReactDOM.render(
58 |
59 |
60 |
61 |
62 |
Y
63 |
64 | -
65 | changeChannel(event)}>
66 | NEWS
67 |
68 |
69 | -
70 | changeChannel(event)}>
71 | NEW
72 |
73 |
74 | -
75 | changeChannel(event)}>
76 | SHOW
77 |
78 |
79 | -
80 | changeChannel(event)}>
81 | ASK
82 |
83 |
84 | -
85 | changeChannel(event)}>
86 | JOBS
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | ,
101 | document.getElementById("root")
102 | );
103 |
104 | window.addEventListener("popstate", () => initChannel());
105 |
106 | initChannel();
107 | registerServiceWorker();
108 |
--------------------------------------------------------------------------------