├── 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 |
6 |
7 |
8 |
9 |
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 |
    1. points: {data.points}
    2. 27 |
    3. comments_count: {data.comments_count}
    4. 28 |
    5. time_ago: {data.time_ago}
    6. 29 |
    7. reply
    8. 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 | --------------------------------------------------------------------------------