├── src
├── containers
│ ├── Embient
│ │ ├── action.js
│ │ ├── index.js
│ │ └── reducer.js
│ ├── MusicTime
│ │ ├── index.js
│ │ └── reducer.js
│ ├── DashBoard
│ │ ├── HoverTitle.js
│ │ ├── SvgIcons
│ │ │ ├── index.js
│ │ │ ├── StyledSvg.js
│ │ │ ├── NewsSVG.js
│ │ │ ├── RadioSVG.js
│ │ │ └── IdeaSVG.js
│ │ ├── styled
│ │ │ ├── NormalLink.js
│ │ │ ├── Container.js
│ │ │ └── Card.js
│ │ └── index.js
│ ├── NewsFeed
│ │ ├── V2EX
│ │ │ ├── styled
│ │ │ │ ├── H1.js
│ │ │ │ ├── Chevron.js
│ │ │ │ ├── IMG.js
│ │ │ │ ├── NoPaddingXS.js
│ │ │ │ ├── Row.js
│ │ │ │ ├── PBody.js
│ │ │ │ ├── Background.js
│ │ │ │ ├── PHeading.js
│ │ │ │ ├── MarkDown.js
│ │ │ │ ├── Badge.js
│ │ │ │ ├── Tab.js
│ │ │ │ ├── Panel.js
│ │ │ │ ├── PFooter.js
│ │ │ │ ├── Container.js
│ │ │ │ └── Cards.js
│ │ │ ├── format.js
│ │ │ ├── components
│ │ │ │ ├── Replies.js
│ │ │ │ ├── HotItem.js
│ │ │ │ ├── TopicInfo.js
│ │ │ │ ├── ReplyItem.js
│ │ │ │ ├── Topic.js
│ │ │ │ ├── Hot.js
│ │ │ │ ├── Main.js
│ │ │ │ ├── UserInfo.js
│ │ │ │ └── Content.js
│ │ │ ├── reducer.js
│ │ │ └── index.js
│ │ ├── HackerNews
│ │ │ ├── styled
│ │ │ │ ├── UL.js
│ │ │ │ ├── FooterItem.js
│ │ │ │ ├── Footer.js
│ │ │ │ ├── A.js
│ │ │ │ ├── Header.js
│ │ │ │ └── Wrapper.js
│ │ │ ├── reducer.js
│ │ │ ├── index.js
│ │ │ └── components
│ │ │ │ └── Story.js
│ │ ├── Github
│ │ │ ├── styled
│ │ │ │ ├── Des.js
│ │ │ │ ├── UL.js
│ │ │ │ ├── SideWrapper.js
│ │ │ │ ├── LI.js
│ │ │ │ ├── Title.js
│ │ │ │ ├── DescriptionWrapper.js
│ │ │ │ └── SideItem.js
│ │ │ ├── reducer.js
│ │ │ ├── components
│ │ │ │ └── Repo.js
│ │ │ └── index.js
│ │ ├── reducer.js
│ │ ├── styled
│ │ │ ├── Container.js
│ │ │ ├── LINK.js
│ │ │ └── HeaderContainer.js
│ │ ├── components
│ │ │ ├── GoTop.js
│ │ │ ├── Header.js
│ │ │ ├── NavLink.js
│ │ │ └── Icon.js
│ │ ├── index.js
│ │ ├── selector.js
│ │ └── saga.js
│ ├── WritePad
│ │ ├── styled
│ │ │ └── Container.js
│ │ ├── reducer.js
│ │ ├── index.js
│ │ └── selector.js
│ ├── App
│ │ ├── reducer.js
│ │ ├── index.js
│ │ ├── rootSaga.js
│ │ ├── Global.scss
│ │ ├── constant.js
│ │ ├── components
│ │ │ └── NavBtn.js
│ │ └── selectors.js
│ └── Editor
│ │ ├── styled
│ │ ├── Textarea.js
│ │ ├── Wrapper.js
│ │ ├── UL.js
│ │ └── LI.js
│ │ ├── selector.js
│ │ ├── components
│ │ ├── fileList.js
│ │ ├── EditorPanel.js
│ │ ├── BrowseFileModal.js
│ │ ├── SaveFileModal.js
│ │ └── fileItem.js
│ │ ├── action.js
│ │ ├── reducer.js
│ │ └── index.js
├── index.js
├── reducers.js
├── Root.js
└── store.js
├── .gitignore
├── .eslintrc
├── .babelrc
├── index.html
├── dist
├── index.html
└── manifest-0a7ebab1f4e1524c44c8.js
├── webpack.config.js
├── server.js
├── package.json
└── README.md
/src/containers/Embient/action.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/containers/Embient/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/containers/MusicTime/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .happypack
4 | *.map
--------------------------------------------------------------------------------
/src/containers/DashBoard/HoverTitle.js:
--------------------------------------------------------------------------------
1 | export const HoverTitle = {
2 | idea: '专注写作,输出灵感',
3 | news: '社区热点,跟踪趋势',
4 | radio: '放松一刻,醒神节奏'
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "plugins": [
4 | "react"
5 | ],
6 | "rules": {
7 | },
8 | "extends": ["plugin:react/recommended"]
9 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", {
4 | "module": false,
5 | "loose": true
6 | }],
7 | "react",
8 | "stage-2",
9 | "es2016"
10 | ]
11 | }
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/H1.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const H1 = styled.h1`
4 | line-height: 1.1;
5 | margin: .67em 0;
6 | `
7 |
8 | export default H1
9 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Chevron.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Chevron = styled.span`
4 | font-family: Lucida Grande;
5 | font-weight: 500;
6 | `
7 |
8 | export default Chevron
9 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/SvgIcons/index.js:
--------------------------------------------------------------------------------
1 | import IdeaSVG from './IdeaSVG'
2 | import RadioSVG from './RadioSVG'
3 | import NewsSVG from './NewsSVG'
4 |
5 | export {
6 | IdeaSVG,
7 | RadioSVG,
8 | NewsSVG
9 | }
10 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/styled/UL.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const UL = styled.ul`
4 | padding: 30px;
5 | display: flex;
6 | flex-direction: column;
7 | flex-grow: 1;
8 | `
9 |
10 | export default UL
11 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/styled/FooterItem.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const FooterItem = styled.span`
4 | margin-right: 15px;
5 | font-size: 1.6rem;
6 | color: grey;
7 | `
8 |
9 | export default FooterItem
10 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/styled/NormalLink.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { Link } from 'react-router-dom'
3 |
4 | const NormalLink = styled(Link)`
5 | text-decoration: none;
6 | color: #354a5d;
7 | `
8 |
9 | export default NormalLink
10 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/IMG.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const IMG = styled.img`
4 | border: 0;
5 | width: 50px;
6 | height: 50px;
7 | border-radius: 4px;
8 | vertical-align: middle;
9 | `
10 |
11 | export default IMG
12 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/styled/Des.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Des = styled.p`
4 | color: #586069 !important;
5 | margin-bottom: 8px;
6 | line-height: 1.5;
7 | margin: 0;
8 | font-size: 1.4rem;
9 | `
10 | export default Des
11 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/styled/Footer.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Footer = styled.footer`
4 | display: flex;
5 | flex-wrap: wrap;
6 | padding-top: 15px;
7 | line-height: 1.5em;
8 | `
9 |
10 | export default Footer
11 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/NoPaddingXS.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const NoPaddingXS = styled.div`
4 | position: relative;
5 | min-height: 1px;
6 | padding-right: 15px;
7 | padding-left: 15px;
8 | `
9 |
10 | export default NoPaddingXS
11 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Row.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Row = styled.div`
4 | margin-right: -15px;
5 | margin-left: -15px;
6 | &:before {
7 | display: table;
8 | content: " ";
9 | }
10 | `
11 |
12 | export default Row
13 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/styled/A.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const A = styled.a`
4 | &:not([type=button]) {
5 | color: #222;
6 | text-decoration: none;
7 | }
8 | &:visited {
9 | color: #555;
10 | }
11 | `
12 |
13 | export default A
14 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/styled/UL.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const UL = styled.ul`
4 | min-width: 320px;
5 | width: 100%;
6 | padding: 30px;
7 | box-sizing: border-box;
8 | display: flex;
9 | flex-direction: column;
10 | `
11 |
12 | export default UL
13 |
--------------------------------------------------------------------------------
/src/containers/WritePad/styled/Container.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Container = styled.div`
4 | height: 100vh;
5 | justify-content: center;
6 | display: flex;
7 | box-sizing: border-box;
8 | background: white;
9 | `
10 |
11 | export default Container
12 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/PBody.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const PBody = styled.div`
4 | padding: 0 15px;
5 | &:before {
6 | display: table;
7 | content: " ";
8 | }
9 | &:after {
10 | clear: both;
11 | }
12 | `
13 |
14 | export default PBody
15 |
--------------------------------------------------------------------------------
/src/containers/WritePad/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux-immutable'
2 | import editor from '../Editor/reducer'
3 | import ambientSound from '../Embient/reducer'
4 |
5 | const writePad = combineReducers({
6 | editor,
7 | ambientSound
8 | })
9 |
10 | export default writePad
11 |
--------------------------------------------------------------------------------
/src/containers/MusicTime/reducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | const initialState = Immutable.fromJS({})
4 |
5 | const musicTime = (state = initialState, action) => {
6 | switch (action.type) {
7 | default:
8 | return state
9 | }
10 | }
11 |
12 | export default musicTime
13 |
--------------------------------------------------------------------------------
/src/containers/Embient/reducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | const initialState = Immutable.fromJS({})
4 |
5 | const embientSound = (state = initialState, action) => {
6 | switch (action.type) {
7 | default:
8 | return state
9 | }
10 | }
11 |
12 | export default embientSound
13 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux-immutable'
2 | import hackerNews from './HackerNews/reducer'
3 | import github from './Github/reducer'
4 | import v2ex from './V2EX/reducer'
5 |
6 | const newsFeed = combineReducers({ hackerNews, github, v2ex })
7 |
8 | export default newsFeed
9 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/styled/SideWrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const SideWrapper = styled.div`
4 | display: flex;
5 | @media all and (max-width:768px) {
6 | flex-direction: row-reverse;
7 | justify-content: flex-end;
8 | }
9 | `
10 |
11 | export default SideWrapper
12 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/styled/LI.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const LI = styled.li`
4 | display: flex;
5 | list-style: none;
6 | margin: 1rem 0;
7 | flex-shrink: 0;
8 | @media all and (max-width:768px) {
9 | flex-direction: column;
10 | }
11 | `
12 |
13 | export default LI
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React from "react";
3 | import { render } from "react-dom";
4 | import Root from "./Root";
5 |
6 | render(
7 | ,
8 | /* eslint-disbale no-undef */
9 | document.getElementById("root")
10 | /* eslint-enable no-undef */
11 | );
12 |
13 | // module.hot.accept()
14 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/styled/Title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Title = styled.h2`
4 | & a {
5 | font-size: 2rem;
6 | color: #0366d6;
7 | font-weight: 500;
8 | margin-top: 0;
9 | margin-bottom: 1rem;
10 | text-decoration: none;
11 | }
12 | `
13 | export default Title
14 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/styled/Header.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Header = styled.h2`
4 | margin: 0;
5 | line-height: 1.6em;
6 | font-size: 2.2rem;
7 | font-weight: 500;
8 | word-break: break-all;
9 | word-break: break-word;
10 | hyphens: auto;
11 | `
12 |
13 | export default Header
14 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Background.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Background = styled.div`
4 | background: #e2e2e2;
5 | padding-top: 20px;
6 | flex-grow: 1;
7 | heigth: auto;
8 | &:before {
9 | display: table;
10 | content: " ";
11 | }
12 | `
13 |
14 | export default Background
15 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/styled/DescriptionWrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const DescriptionWrapper = styled.div`
4 | box-sizing: border-box;
5 | width: calc(100% - 400px);
6 | margin-bottom: 1rem;
7 | @media all and (max-width:768px) {
8 | width: 100%;
9 | }
10 | `
11 |
12 | export default DescriptionWrapper
13 |
--------------------------------------------------------------------------------
/src/containers/App/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux-immutable'
2 | import writePad from '../WritePad/reducer'
3 | import newsFeed from '../NewsFeed/reducer'
4 | import musicTime from '../MusicTime/reducer'
5 |
6 | const appReducer = combineReducers({
7 | writePad,
8 | newsFeed,
9 | musicTime
10 | })
11 |
12 | export default appReducer
13 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/styled/SideItem.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const SideItem = styled.div`
4 | cursor: default;
5 | text-align: center;
6 | width: 200px;
7 | &:hover {
8 | color: #009688;
9 | }
10 | @media all and (max-width:768px) {
11 | text-align: left;
12 | }
13 | `
14 |
15 | export default SideItem
16 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/styled/Container.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Container = styled.div`
4 | display: flex;
5 | box-sizing: border-box;
6 | background: white;
7 | padding-top: 56px;
8 | & .clearfix:after {
9 | clear: both;
10 | display: table;
11 | content: " ";
12 | }
13 | `
14 |
15 | export default Container
16 |
--------------------------------------------------------------------------------
/src/containers/WritePad/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Editor from '../Editor/'
3 | import Container from './styled/Container'
4 |
5 | class WritePad extends Component {
6 | render () {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 | }
14 |
15 | export default WritePad
16 |
--------------------------------------------------------------------------------
/src/containers/Editor/styled/Textarea.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Textarea from 'react-textarea-autosize'
4 |
5 | const AutoSizeTextarea = styled(Textarea)`
6 | outline: none;
7 | border: none;
8 | resize: none;
9 | & ul, & ol {
10 | list-style: initial;
11 | }
12 | `
13 |
14 | export default AutoSizeTextarea
15 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | CoderPad
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/reducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import * as actions from '../../App/constant'
3 |
4 | const github = (state = Immutable.List([]), action) => {
5 | switch (action.type) {
6 | case actions.FETCH_GITHUB_SUCCESS:
7 | return (state = Immutable.List(action.payload))
8 | default:
9 | return state
10 | }
11 | }
12 |
13 | export default github
14 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/styled/LINK.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { NavLink } from 'react-router-dom'
3 |
4 | const LINK = styled(NavLink)`
5 | &.btn-active{
6 | background-color: #f5f5f5;
7 | }
8 | & button {
9 | transition: all 0.2s ease-in-out !important;
10 | }
11 | &.btn-active button {
12 | transform: scale(1.3)
13 | }
14 | `
15 |
16 | export default LINK
17 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/reducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import * as actions from '../../App/constant'
3 |
4 | const hackerNews = (state = Immutable.List([]), action) => {
5 | switch (action.type) {
6 | case actions.FETCH_HACKERNEWS_SUCCESS:
7 | return (state = Immutable.List(action.payload))
8 | default:
9 | return state
10 | }
11 | }
12 |
13 | export default hackerNews
14 |
--------------------------------------------------------------------------------
/src/containers/WritePad/selector.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect'
2 | import { makeSelectWritePad } from '../App/selectors'
3 |
4 | export const makeSelectEditor = createSelector(
5 | makeSelectWritePad,
6 | writePadState => writePadState.get('editor')
7 | )
8 |
9 | export const makeSelectAmbientSound = createSelector(
10 | makeSelectWritePad,
11 | writePadState => writePadState.get('ambientSound')
12 | )
13 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/PHeading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | const PHeading = styled.div`
5 | padding: 10px 15px;
6 | border-bottom: 1px solid transparent;
7 | border-top-left-radius: 3px;
8 | border-top-right-radius: 3px;
9 | text-align: left;
10 | color: #333;
11 | background-color: #fff;
12 | border-color: #ddd;
13 | `
14 |
15 | export default PHeading
16 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/styled/Wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Wrapper = styled.li`
4 | list-style: none;
5 | flex-grow: 1;
6 | margin: 10px 0;
7 | padding: 10px 0;
8 | background-image: linear-gradient(to right, #fff 50%, #FAFAFA 50%);
9 | background-size: 200%;
10 | transition: all 0.3s;
11 | &:hover {
12 | background-position: -100%;
13 | }
14 | `
15 |
16 | export default Wrapper
17 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/format.js:
--------------------------------------------------------------------------------
1 | export const pluralize = (time, label) => time + label
2 |
3 | export const date = time => {
4 | const between = Math.round(new Date().getTime() / 1000) - time
5 | if (between < 3600) {
6 | return pluralize(~~(between / 60), ' 分钟前')
7 | } else if (between < 86400) {
8 | return pluralize(~~(between / 3600), ' 小时前')
9 | } else {
10 | return pluralize(~~(between / 86400), ' 天前')
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/MarkDown.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import PBody from './PBody'
3 |
4 | const MarkDown = styled(PBody)`
5 | color: #333;
6 | position: relative;
7 | line-height: 1.8em;
8 | font-size: 14px;
9 | text-overflow: ellipsis;
10 | word-wrap: break-word;
11 | font-family: PingFang SC,Hiragino Sans GB,Helvetica,Arial,Source Han Sans CN,Roboto,Heiti SC,Microsoft Yahei,sans-serif!important;
12 | `
13 |
14 | export default MarkDown
15 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Badge.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Badge = styled.span`
4 | padding: 3px 10px;
5 | background-color: #aab0c6;
6 | display: inline-block;
7 | min-width: 10px;
8 | padding: 3px 7px;
9 | font-size: 12px;
10 | font-weight: 700;
11 | line-height: 1;
12 | color: #fff;
13 | text-align: center;
14 | white-space: nowrap;
15 | vertical-align: middle;
16 | border-radius: 10px;
17 | `
18 |
19 | export default Badge
20 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Tab.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom'
2 | import styled from 'styled-components'
3 |
4 | const Tab = styled(Link)`
5 | display: inline-block;
6 | font-size: 13px;
7 | line-height: 13px;
8 | padding: 5px 8px;
9 | margin-right: 5px;
10 | border-radius: 3px;
11 | color: #555 !important;
12 | &:hover {
13 | background-color: #f5f5f5;
14 | color: #000;
15 | text-decoration: none;
16 | }
17 | `
18 |
19 | export default Tab
20 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/styled/Container.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Container = styled.div`
4 | height: 100vh;
5 | width: 100vw;
6 | padding: 0 10vw;
7 | box-sizing: border-box;
8 | display: flex;
9 | justify-content: space-around;
10 | align-items: center;
11 | & > div{
12 | flex-basis: 290px;
13 | flex-grow: 1;
14 | };
15 | @media all and (max-width:768px) {
16 | & {
17 | flex-flow: column;
18 | padding: 10vh 0;
19 | }
20 | }
21 | `
22 |
23 | export default Container
24 |
--------------------------------------------------------------------------------
/src/containers/Editor/styled/Wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Wrapper = styled.div`
4 | flex-basis: 55vw;
5 | padding-top: 15vh;
6 | & .hidden-toggle {
7 | display: none;
8 | }
9 | & .markdown {
10 | min-height: 500px;
11 | width: 100%;
12 | padding: 0;
13 | font-size: 20px;
14 | background-color: transparent;
15 | color: #424242;
16 | font-family: 'Inconsolata', monospace;
17 | line-height: 1.5;
18 | overflow-x: hidden;
19 | }
20 | `
21 |
22 | export default Wrapper
23 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/styled/HeaderContainer.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { Paper } from 'material-ui'
3 |
4 | const HeaderConatiner = styled(Paper)`
5 | position: fixed !important;
6 | top: 0 !important;
7 | width: 100% !important;
8 | border: 0 none !important;
9 | overflow: hidden !important;
10 | z-index: 1000;
11 | &>div {
12 | display: flex !important;
13 | justify-content: space-around !important;
14 | }
15 | & button {
16 | height: 100%;
17 | }
18 | `
19 |
20 | export default HeaderConatiner
21 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Panel.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Panel = styled.div`
4 | margin-bottom: 20px;
5 | background-color: #fff;
6 | border: 1px solid #ddd;
7 | border-radius: 4px;
8 | box-shadow: 0 2px 3px rgba(0,0,0,.1);
9 | font-size: 12px;
10 | &:after {
11 | clear:both;
12 | }
13 | & a, & a:link {
14 | color: #778087;
15 | word-break: break-all;
16 | text-decoration: none;
17 | }
18 | & a:not(.tab):hover {
19 | text-decoration: underline;
20 | }
21 | `
22 |
23 | export default Panel
24 |
--------------------------------------------------------------------------------
/src/containers/Editor/styled/UL.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const UL = styled.ul`
4 | position: fixed;
5 | top: calc(15vh - 15px);
6 | right: 0;
7 | display: flex;
8 | padding-right: 3vw;
9 | flex-direction: column;
10 | & svg {
11 | height: 30px !important;
12 | width: 30px !important;
13 | fill: #eee !important;
14 | cursor: pointer;
15 | margin: 10px 0;
16 | }
17 |
18 | & svg:hover {
19 | fill: #B0BEC5 !important;
20 | }
21 |
22 | & svg.active {
23 | fill: #78909c !important;
24 | }
25 | `
26 |
27 | export default UL
28 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/components/GoTop.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Back2Top from 'react-back2top'
4 | import { FloatingActionButton } from 'material-ui'
5 | import UpIcon from 'material-ui/svg-icons/navigation/arrow-upward'
6 |
7 | const GoTopWrapper = styled(Back2Top)`
8 | position: fixed !important;
9 | bottom: 15px;
10 | right: 15px;
11 | `
12 |
13 | const GoTop = () => (
14 |
15 |
16 |
17 |
18 |
19 | )
20 |
21 | export default GoTop
22 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/PFooter.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const PFooter = styled.div`
4 | padding: 10px 15px;
5 | background-color: #f5f5f5;
6 | border-top: 1px solid #ddd;
7 | border-bottom-right-radius: 3px;
8 | border-bottom-left-radius: 3px;
9 | & .pf {
10 | font-size: 11px;
11 | line-height: 12px;
12 | color: #333;
13 | display: inline-block;
14 | padding: 3px 10px;
15 | text-shadow: 0 1px 0 #fff;
16 | }
17 | & .pf, & .pf:hover {
18 | text-decoration: none;
19 | border-radius: 15px;
20 | }
21 | `
22 |
23 | export default PFooter
24 |
--------------------------------------------------------------------------------
/src/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route } from 'react-router-dom'
3 | /** Child Components */
4 | import NavBtn from './components/NavBtn'
5 | import DashBoard from '../DashBoard/'
6 | import WritePad from '../WritePad/'
7 | import NewsFeed from '../NewsFeed/'
8 | import './Global.scss'
9 |
10 | const App = () => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | export default App
22 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | CoderPad
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/containers/Editor/styled/LI.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const LI = styled.li`
4 | list-style: none;
5 | padding: 1rem;
6 | font-size: 1.6rem;
7 | font-family: Roboto, sans-serif;
8 | display: flex;
9 | justify-content: space-between;
10 | align-items: center;
11 | &:hover {
12 | background: #f5f5f5;
13 | }
14 | & span {
15 | cursor: pointer;
16 | flex-grow: 1;
17 | }
18 | & svg {
19 | cursor: pointer;
20 | fill: #bdbdbd !important;
21 | }
22 | & svg:hover {
23 | fill: #607d8b !important;
24 | }
25 | & input {
26 | border: none!important;
27 | }
28 | `
29 |
30 | export default LI
31 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Container.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Container = styled.div`
4 | padding-right: 15px;
5 | padding-left: 15px;
6 | margin-right: auto;
7 | margin-left: auto;
8 | & *, & {
9 | box-sizing: border-box;
10 | }
11 | &:before {
12 | display: table;
13 | content: " ";
14 | }
15 | &.clearfix:after {
16 | clear" both;
17 | }
18 | @media all and (min-width: 768px) {
19 | width: 750px;
20 | }
21 | @media all and (min-width: 992px) {
22 | width: 970px;
23 | }
24 | @media all and (min-width: 1200px) {
25 | width: 1170px;
26 | }
27 | `
28 |
29 | export default Container
30 |
--------------------------------------------------------------------------------
/src/containers/App/rootSaga.js:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects'
2 | import {
3 | watchLoadHackerNews,
4 | watchHackerNews,
5 | watchLoadGitHub,
6 | watchGithub,
7 | watchLoadV2exTopic,
8 | watchV2exTopic,
9 | watchLoadV2exTopics,
10 | watchV2exTopics,
11 | watchLoadV2exHot,
12 | watchV2exHot
13 | } from '../NewsFeed/saga'
14 |
15 | export default function * rootSaga () {
16 | yield all([
17 | // watchLoadHackerNews(),
18 | // watchLoadGitHub(),
19 | // watchLoadV2exTopic(),
20 | // watchLoadV2exTopics(),
21 | // watchLoadV2exHot(),
22 | watchHackerNews(),
23 | watchGithub(),
24 | watchV2exTopic(),
25 | watchV2exTopics(),
26 | watchV2exHot()
27 | ])
28 | }
29 |
--------------------------------------------------------------------------------
/src/containers/App/Global.scss:
--------------------------------------------------------------------------------
1 | $font-stack: "Helvetica Neue",
2 | "Arial",
3 | " Segoe UI",
4 | "PingFang SC",
5 | "Hiragino Sans GB",
6 | "STHeiti",
7 | "Microsoft YaHei",
8 | "Microsoft JhengHei",
9 | "Source Han Sans SC",
10 | "Noto Sans CJK SC",
11 | "Source Han Sans CN",
12 | "Noto Sans SC",
13 | "Source Han Sans TC",
14 | "Noto Sans CJK TC",
15 | "WenQuanYi Micro Hei",
16 | SimSun,
17 | sans-serif;
18 | $primary-color: #354A5D;
19 | html,
20 | body {
21 | font: 62.5% $font-stack;
22 | color: $primary-color;
23 | margin: 0;
24 | padding: 0;
25 | }
26 |
27 | ul,
28 | li {
29 | margin: 0;
30 | padding: 0;
31 | }
32 |
33 | button,
34 | input,
35 | select,
36 | textarea {
37 | margin: 0;
38 | font-size: 100%;
39 | font-family: inherit;
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/src/reducers.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable'
2 | import { combineReducers } from 'redux-immutable'
3 | import { LOCATION_CHANGE } from 'react-router-redux'
4 | import globalReducer from './containers/App/reducer'
5 |
6 | const routeInitialState = fromJS({
7 | locationBeforeTransitions: null
8 | })
9 | /**
10 | * Merge route into the global application state
11 | */
12 | function routeReducer (state = routeInitialState, action) {
13 | switch (action.type) {
14 | case LOCATION_CHANGE:
15 | return state.merge({
16 | locationBeforeTransitions: action.payload
17 | })
18 | default:
19 | return state
20 | }
21 | }
22 |
23 | export default function createAppReducer () {
24 | return combineReducers({
25 | route: routeReducer,
26 | global: globalReducer
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/src/containers/App/constant.js:
--------------------------------------------------------------------------------
1 | export const TOGGLE_PREVIEW = 'TOGGLE_PREVIEW'
2 |
3 | export const EDIT_MARKDOWN = 'EDIT_MARKDOWN'
4 |
5 | export const TOGGLE_SAVEFILE = 'TOGGLE_SAVEFILE'
6 |
7 | export const SAVE_NEWFILE = 'SAVE_NEWFILE'
8 |
9 | export const REMOVE_FILE = 'REMOVE_FILE'
10 |
11 | export const TOGGLE_BROWSE = 'TOGGLE_BROWSE'
12 |
13 | export const LOAD_LOCALFILES = 'LOAD_LOCALFILES'
14 |
15 | export const FETCH_HACKERNEWS_SUCCESS = 'FETCH_HACKERNEWS_SUCCESS'
16 |
17 | export const FETCH_GITHUB_SUCCESS = 'FETCH_GITHUB_SUCCESS'
18 |
19 | export const FETCH_V2EX_HOT_SUCCESS = 'FETCH_V2EX_HOT_SUCCESS'
20 |
21 | export const FETCH_V2EX_TOPIC_SUCCESS = 'FETCH_V2EX_TOPIC_SUCCESS'
22 |
23 | export const FETCH_V2EX_TOPICS_SUCCESS = 'FETCH_V2EX_TOPICS_SUCCESS'
24 |
25 | export const CLEAN_TOPIC_CACHE = 'CLEAN_TOPIC_CACHE'
26 |
--------------------------------------------------------------------------------
/src/containers/Editor/selector.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect'
2 | import { makeSelectEditor } from '../WritePad/selector'
3 |
4 | export const makeSelectIsPreview = createSelector(
5 | makeSelectEditor,
6 | editorState => editorState.get('isPreview')
7 | )
8 |
9 | export const makeSelectIsSaving = createSelector(
10 | makeSelectEditor,
11 | editorState => editorState.get('isSaving')
12 | )
13 |
14 | export const makeSelectIsBrowsing = createSelector(
15 | makeSelectEditor,
16 | editorState => editorState.get('isBrowsing')
17 | )
18 |
19 | export const makeSelectTextValue = createSelector(
20 | makeSelectEditor,
21 | editorState => editorState.get('textValue')
22 | )
23 |
24 | export const makeSelectSavedFiles = createSelector(
25 | makeSelectEditor,
26 | editorState => editorState.get('savedFiles')
27 | )
28 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | /** Child Components */
3 | import { IdeaSVG, RadioSVG, NewsSVG } from './SvgIcons/'
4 | /** Styled Components */
5 | import Card from './styled/Card'
6 | import Container from './styled/Container'
7 | import NormalLink from './styled/NormalLink'
8 |
9 | class DashBoard extends PureComponent {
10 | render () {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | export default DashBoard
34 |
--------------------------------------------------------------------------------
/src/containers/App/components/NavBtn.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React from "react";
3 | import styled from "styled-components";
4 | import { Link } from "react-router-dom";
5 | import FloatingActionButton from "material-ui/FloatingActionButton";
6 | import Apps from "material-ui/svg-icons/navigation/apps";
7 | import ContentAdd from "material-ui/svg-icons/content/add";
8 | /* eslint-disable */
9 | const FlatBtn = styled.div`
10 | transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
11 | position: fixed;
12 | top: 15px;
13 | left: 20px;
14 | height: 24px;
15 | width: 24px;
16 | z-index: 1200;
17 | cursor: pointer;
18 | &:hover {
19 | transform: scale(1.2)
20 | }
21 | &:hover svg {
22 | fill: #43a047 !important;
23 | }
24 | & svg {
25 | fill: #476268 !important;
26 | }
27 | `;
28 | const NavBtn = () => (
29 |
30 |
31 |
32 |
33 |
34 | );
35 |
36 | export default NavBtn;
37 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | /** Styled Components */
5 | import HeaderConatiner from "../styled/HeaderContainer";
6 | /** Material Components */
7 | import { BottomNavigation } from "material-ui/BottomNavigation";
8 | /** Child Components */
9 | import NavLink from "./NavLink";
10 |
11 | class Header extends PureComponent {
12 | // if we don't receive location as paramter, the PureComponent's shouldComponnetUpdate will block the actual location change, so the NavLink won't work correctly
13 | static propTypes = {
14 | location: PropTypes.object
15 | };
16 | render() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | export default Header;
30 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/Replies.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | /**Child Components */
5 | import ReplyItem from "./ReplyItem";
6 | /**Styled Components */
7 | import Panel from "../styled/Panel";
8 | import PHeading from "../styled/PHeading";
9 | import PBody from "../styled/PBody";
10 |
11 | class Replies extends PureComponent {
12 | static propTypes = {
13 | replies: PropTypes.array
14 | };
15 | render() {
16 | let count = this.props.replies.length;
17 | return (
18 |
19 |
20 | 共收到 {count} 条回复
21 |
22 |
23 | {this.props.replies.length
24 | ? this.props.replies.map(reply => (
25 |
26 | ))
27 | : ""}
28 |
29 |
30 | );
31 | }
32 | }
33 |
34 | export default Replies;
35 |
--------------------------------------------------------------------------------
/src/containers/App/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect'
2 |
3 | const selectGlobal = (state) => state.get('global')
4 |
5 | const makeSelectWritePad = createSelector(
6 | selectGlobal,
7 | appState => appState.get('writePad')
8 | )
9 |
10 | const makeSelectNewsFeed = createSelector(
11 | selectGlobal,
12 | appState => appState.get('newsFeed')
13 | )
14 |
15 | const makeSelectMusicTime = createSelector(
16 | selectGlobal,
17 | appState => appState.get('musicTime')
18 | )
19 |
20 | const makeSelectLocationState = () => {
21 | let prevRoutingState
22 | let prevRoutingStateJS
23 |
24 | return (state) => {
25 | const routingState = state.get('route') // or state.route
26 |
27 | if (!routingState.equals(prevRoutingState)) {
28 | prevRoutingState = routingState
29 | prevRoutingStateJS = routingState.toJS()
30 | }
31 | return prevRoutingStateJS
32 | }
33 | }
34 |
35 | export {
36 | selectGlobal,
37 | makeSelectWritePad,
38 | makeSelectNewsFeed,
39 | makeSelectMusicTime,
40 | makeSelectLocationState
41 | }
42 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/fileList.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | /** Child Components */
5 | import FileItem from "./fileItem";
6 |
7 | class FileList extends PureComponent {
8 | static propTypes = {
9 | savedFiles: PropTypes.object,
10 | openFile: PropTypes.func,
11 | toggleBrowse: PropTypes.func,
12 | removeFile: PropTypes.func
13 | };
14 |
15 | render() {
16 | const { savedFiles, openFile, toggleBrowse, removeFile } = this.props;
17 | return (
18 |
19 | {Object.keys(savedFiles).map((item, idx) => {
20 | return (
21 |
29 | );
30 | })}
31 |
32 | );
33 | }
34 | }
35 |
36 | export default FileList;
37 |
--------------------------------------------------------------------------------
/src/Root.js:
--------------------------------------------------------------------------------
1 | // Needed for redux-saga es6 generator support
2 | import 'babel-polyfill'
3 | // Import all the third party stuff
4 | import React from 'react'
5 | import Immutable from 'immutable'
6 | import { Provider } from 'react-redux'
7 | import createHistory from 'history/createBrowserHistory'
8 | import { ConnectedRouter } from 'react-router-redux'
9 | import reactTapEventPlugin from 'react-tap-event-plugin'
10 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
11 | // Import root app
12 | import App from './containers/App'
13 | // Import selector for `syncHistoryWithStore`
14 | import configureStore from './store'
15 | import rootSaga from './containers/App/rootSaga'
16 |
17 | reactTapEventPlugin()
18 | const initialState = Immutable.fromJS({})
19 | const history = createHistory()
20 | const store = configureStore(initialState)
21 | store.runSaga(rootSaga)
22 |
23 | const Root = () => (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 |
33 | export default Root
34 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/reducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import * as actions from '../../App/constant'
3 |
4 | const v2ex = (
5 | state = Immutable.fromJS({
6 | topics: [],
7 | hot: [],
8 | topic: {
9 | topicInfo: {},
10 | replies: []
11 | }
12 | }),
13 | action
14 | ) => {
15 | switch (action.type) {
16 | case actions.FETCH_V2EX_TOPIC_SUCCESS:
17 | return state.set(
18 | 'topic',
19 | Immutable.fromJS({
20 | topicInfo: action.payload.topicInfo,
21 | replies: action.payload.replies
22 | })
23 | )
24 | case actions.FETCH_V2EX_TOPICS_SUCCESS:
25 | return state.set('topics', Immutable.List(action.payload.topics))
26 | case actions.FETCH_V2EX_HOT_SUCCESS:
27 | return state.set('hot', Immutable.List(action.payload.topics))
28 | case actions.CLEAN_TOPIC_CACHE:
29 | return state.set(
30 | 'topic',
31 | Immutable.Map({
32 | topicInfo: Immutable.Map({}),
33 | replies: Immutable.List([])
34 | })
35 | )
36 | default:
37 | return state
38 | }
39 | }
40 |
41 | export default v2ex
42 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/Github/components/Repo.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | /** Styled Components */
5 | import LI from "../styled/LI";
6 | import DescriptionWrapper from "../styled/DescriptionWrapper";
7 | import Title from "../styled/Title";
8 | import Des from "../styled/Des";
9 | import SideWrapper from "../styled/SideWrapper";
10 | import SideItem from "../styled/SideItem";
11 |
12 | class Repo extends PureComponent {
13 | static propTypes = {
14 | repo: PropTypes.object
15 | };
16 | render() {
17 | const { stars, url, description, language, user, name } = this.props.repo;
18 | return (
19 |
20 |
21 | {user}/{name}
22 | {description || "No description"}
23 |
24 |
25 |
26 | {language || "Null"}
27 |
28 |
29 | ★{stars}
30 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | export default Repo;
38 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { Route, Link } from "react-router-dom";
3 | /** Tools */
4 | import PropTypes from "prop-types";
5 | /** Styled Components */
6 | import Container from "./styled/Container";
7 | /** Child Components */
8 | import Header from "./components/Header";
9 | /** Child Container Components */
10 | import HackerNews from "./HackerNews/index";
11 | import Github from "./Github/index";
12 | import V2EX from "./V2EX/index";
13 | /** Third Party Components */
14 | import GoTop from "./components/GoTop";
15 |
16 | class NewsFeed extends PureComponent {
17 | static propTypes = {
18 | location: PropTypes.object,
19 | match: PropTypes.object
20 | };
21 | render() {
22 | const { match } = this.props;
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | export default NewsFeed;
36 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/selector.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect'
2 | import { makeSelectNewsFeed } from '../App/selectors'
3 |
4 | export const makeSelectHackerNews = createSelector(
5 | makeSelectNewsFeed,
6 | newsFeedState => newsFeedState.get('hackerNews').toArray()
7 | )
8 |
9 | export const makeSelectGithub = createSelector(
10 | makeSelectNewsFeed,
11 | newsFeedState => newsFeedState.get('github').toArray()
12 | )
13 |
14 | export const makeSelectV2ex = createSelector(
15 | makeSelectNewsFeed,
16 | newsFeedState => newsFeedState.get('v2ex')
17 | )
18 |
19 | export const makeSelectV2exTopic = createSelector(makeSelectV2ex, v2exState =>
20 | v2exState.get('topic')
21 | )
22 |
23 | export const makeSelectV2exTopicInfo = createSelector(
24 | makeSelectV2exTopic,
25 | topicState => {
26 | return topicState.get('topicInfo')
27 | }
28 | )
29 |
30 | export const makeSelectV2exReplies = createSelector(
31 | makeSelectV2exTopic,
32 | topicState => topicState.get('replies').toArray()
33 | )
34 |
35 | export const makeSelectV2exTopics = createSelector(makeSelectV2ex, v2exState =>
36 | v2exState.get('topics')
37 | )
38 |
39 | export const makeSelectV2exHot = createSelector(makeSelectV2ex, v2exState =>
40 | v2exState.get('hot')
41 | )
42 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/components/NavLink.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | /** Child Components */
5 | import { HackerNewsIcon, GitHubIcon, V2exIcon } from "./Icon";
6 | /** Styled Components */
7 | import LINK from "../styled/LINK";
8 | /** Material Components */
9 | import { BottomNavigationItem } from "material-ui/BottomNavigation";
10 |
11 | class NavLink extends PureComponent {
12 | static propTypes = {
13 | filter: PropTypes.string
14 | };
15 |
16 | mapFilterToItemConfig = filter => {
17 | switch (filter) {
18 | case "hackernews":
19 | return {
20 | icon:
21 | };
22 | case "github":
23 | return {
24 | icon:
25 | };
26 | case "v2ex":
27 | return {
28 | icon:
29 | };
30 | }
31 | };
32 |
33 | render() {
34 | const { filter } = this.props;
35 | const { label, icon } = this.mapFilterToItemConfig(filter);
36 | return (
37 |
41 |
42 |
43 | );
44 | }
45 | }
46 |
47 | export default NavLink;
48 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/styled/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import styledProps from 'styled-props'
4 | import { HoverTitle } from '../HoverTitle'
5 |
6 | const SlideInUp = keyframes`
7 | 0% {
8 | opacity: 0;
9 | visibility: visible;
10 | transform: translateY(100%);
11 | -webkit-transform: translateY(100%);
12 | }
13 | 100% {
14 | opacity: 1;
15 | transform: translateY(0);
16 | -webkit-transform: translateY(0);
17 | }
18 | `
19 |
20 | const Card = styled.div`
21 | position: relative;
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | border-radius: 20px;
26 | cursor: pointer;
27 | z-index: 10;
28 | width: 150px;
29 | &::after {
30 | display: none;
31 | transition: opacity 0.3s ease-in;
32 | content: '${styledProps(HoverTitle)}';
33 | position: absolute;
34 | bottom: -50px;
35 | width: 200px;
36 | left: -25px;
37 | text-align: center;
38 | font-size: 20px;
39 | }
40 | &:hover::after {
41 | display: block;
42 | -webkit-animation: 0.3s ease-in ${SlideInUp};
43 | animation: 0.3s ease-in ${SlideInUp};
44 | }
45 | @media all and (max-width:768px) {
46 | &::after {
47 | bottom: -35px;
48 | width: 200px;
49 | }
50 | }
51 | `
52 |
53 | export default Card
54 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/styled/Cards.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Card = styled.div`
4 | margin: 0 -15px;
5 | padding: 10px 15px;
6 | border-bottom: 1px solid #e2e2e2;
7 | `
8 |
9 | export const CardContent = styled.div`
10 | display: table-cell;
11 | vertical-align: top;
12 | `
13 |
14 | export const CardLeft = styled(CardContent)`
15 | padding-right: 10px;
16 | `
17 |
18 | export const IMG = styled.img`
19 | border: 0;
20 | width: 50px;
21 | height: 50px;
22 | border-radius: 4px;
23 | vertical-align: middle;
24 | `
25 |
26 | export const CardBody = styled(CardContent)`
27 | width: 10000px;
28 | overflow: hidden;
29 | zoom: 1;
30 | `
31 |
32 | export const CardRight = styled(CardContent)`
33 | padding-left: 10px;
34 | vertical-align: middle;
35 | `
36 |
37 | export const TopicCardBody = styled(CardBody)`
38 | & .title {
39 | font-size: 15px;
40 | margin-bottom: 0;
41 | }
42 | & .info {
43 | color: #aba8a6;
44 | font-size: 12px;
45 | padding-top: 8px;
46 | }
47 | & .info .separator {
48 | padding: 0 5px;
49 | }
50 | & .info>a {
51 | background-color: #f5f5f5;
52 | font-size: 12px !important;
53 | line-height: 10px;
54 | display: inline-block;
55 | padding: 4px;
56 | border-radius: 2px;
57 | text-decoration: none;
58 | color: #999;
59 | margin-right: 5px;
60 | }
61 | `
62 |
--------------------------------------------------------------------------------
/src/containers/Editor/action.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../App/constant'
2 |
3 | const togglePreview = () => ({
4 | type: actionTypes.TOGGLE_PREVIEW
5 | })
6 |
7 | const editMarkdown = value => ({
8 | type: actionTypes.EDIT_MARKDOWN,
9 | payload: value
10 | })
11 |
12 | const toggleSaveFile = () => ({
13 | type: actionTypes.TOGGLE_SAVEFILE
14 | })
15 |
16 | const toggleBrowse = () => ({
17 | type: actionTypes.TOGGLE_BROWSE
18 | })
19 |
20 | const saveNewFile = (name, textValue) => {
21 | let finalName = 'coderPad-' + name
22 |
23 | localStorage.setItem(finalName, textValue)
24 |
25 | return {
26 | type: actionTypes.SAVE_NEWFILE,
27 | name: finalName,
28 | textValue: textValue
29 | }
30 | }
31 |
32 | const removeFile = fileName => {
33 | localStorage.removeItem(fileName)
34 | return {
35 | type: actionTypes.REMOVE_FILE,
36 | name: fileName
37 | }
38 | }
39 |
40 | const loadLocalFiles = () => {
41 | let localSavedFiles = {}
42 | for (let name in localStorage) {
43 | if (name.indexOf('coderPad') > -1) {
44 | localSavedFiles[name] = localStorage.getItem(name)
45 | }
46 | }
47 | return {
48 | type: actionTypes.LOAD_LOCALFILES,
49 | payload: localSavedFiles
50 | }
51 | }
52 |
53 | export {
54 | toggleBrowse,
55 | togglePreview,
56 | editMarkdown,
57 | toggleSaveFile,
58 | saveNewFile,
59 | loadLocalFiles,
60 | removeFile
61 | }
62 |
--------------------------------------------------------------------------------
/dist/manifest-0a7ebab1f4e1524c44c8.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var n=window.webpackJsonp;window.webpackJsonp=function(t,u,c){for(var i,a,f,l=0,s=[];l
24 | Github Trending
25 | {!this.props.repos.length
26 | ?
30 | : this.props.repos.map(repo => )}
31 |
32 | );
33 | }
34 | }
35 |
36 | const mapDispatchToProps = dispatch => ({
37 | loadGithub: () => dispatch({ type: "LOAD_GITHUB" }),
38 | stopFetch: () => dispatch({ type: "STOP_FETCH" })
39 | });
40 |
41 | const mapStateToProps = state => ({
42 | repos: makeSelectGithub(state)
43 | });
44 |
45 | export default connect(mapStateToProps, mapDispatchToProps)(Github);
46 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/HotItem.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { Link } from "react-router-dom";
3 | /** Tools */
4 | import styled from "styled-components";
5 | import PropTypes from "prop-types";
6 | /** Styled Components */
7 | import IMG from "../styled/IMG";
8 | import { Card, CardLeft, CardBody } from "../styled/Cards";
9 |
10 | const HotItem = styled(Card)`
11 | padding: 10px;
12 | overflow: hidden;
13 | zoom: 1;
14 | `;
15 |
16 | const SmallImg = styled(IMG)`
17 | width: 24px;
18 | height: 24px;
19 | `;
20 |
21 | const HotItemBody = styled(CardBody)`
22 | & a.title {
23 | font-size: 13px;
24 | line-height: 120%;
25 | margin-bottom: 0;
26 | }
27 | `;
28 |
29 | class HotTopic extends PureComponent {
30 | static propTypes = {
31 | hotTopic: PropTypes.object
32 | };
33 | render() {
34 | const { id, member, title } = this.props.hotTopic;
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {title}
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default HotTopic;
53 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/SvgIcons/StyledSvg.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components'
2 |
3 | const rubberBand = keyframes`
4 | 0% {
5 | -webkit-transform: scale3d(1, 1, 1);
6 | transform: scale3d(1, 1, 1);
7 | }
8 | 30% {
9 | -webkit-transform: scale3d(1.15, 0.85, 1);
10 | transform: scale3d(1.15, 0.85, 1);
11 | }
12 | 40% {
13 | -webkit-transform: scale3d(0.95, 1.15, 1);
14 | transform: scale3d(0.95, 1.15, 1);
15 | }
16 | 50% {
17 | -webkit-transform: scale3d(1.15, 0.85, 1);
18 | transform: scale3d(1.15, 0.85, 1);
19 | }
20 | 65% {
21 | -webkit-transform: scale3d(.95, 1.05, 1);
22 | transform: scale3d(.95, 1.05, 1);
23 | }
24 | 75% {
25 | -webkit-transform: scale3d(1.05, .95, 1);
26 | transform: scale3d(1.05, .95, 1);
27 | }
28 | 100% {
29 | -webkit-transform: scale3d(1, 1, 1);
30 | transform: scale3d(1, 1, 1);
31 | }
32 | `
33 |
34 | const StyledSvg = styled.svg`
35 | fill: #354A5D;
36 | height: 150px;
37 | transition: all 0.3s ease-in;
38 | &:hover {
39 | -webkit-animation: 0.5s ease-in ${rubberBand};
40 | animation: 0.5s ease-in ${rubberBand};
41 | }
42 | & g.outline {
43 | fill: #354A5D;
44 | }
45 | & g.colour {
46 | transition: all 0.3s ease-in;
47 | opacity: 0;
48 | }
49 | &:hover g.colour {
50 | opacity: 1;
51 | }
52 | `
53 |
54 | export default StyledSvg
55 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/EditorPanel.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | import classNames from "classnames";
5 | /** Styled Components */
6 | import UL from "../styled/UL";
7 | /** Material Components */
8 | import Preview from "material-ui/svg-icons/action/visibility";
9 | import Save from "material-ui/svg-icons/content/save";
10 | import File from "material-ui/svg-icons/file/folder-open";
11 |
12 | class EditorPanel extends PureComponent {
13 | static propTypes = {
14 | toggleBrowse: PropTypes.func,
15 | togglePreview: PropTypes.func,
16 | toggleSaveFile: PropTypes.func,
17 | isBrowsing: PropTypes.bool,
18 | isPreview: PropTypes.bool,
19 | isSaving: PropTypes.bool
20 | };
21 | render() {
22 | const {
23 | toggleBrowse,
24 | togglePreview,
25 | toggleSaveFile,
26 | isBrowsing,
27 | isPreview,
28 | isSaving
29 | } = this.props;
30 | const previewIconCls = classNames({ active: isPreview });
31 | const saveIconCls = classNames({ active: isSaving });
32 | const browseIconCls = classNames({ active: isBrowsing });
33 | return (
34 |
39 | );
40 | }
41 | }
42 |
43 | export default EditorPanel;
44 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/TopicInfo.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { Link } from "react-router-dom";
3 | /** Tools */
4 | import styled from "styled-components";
5 | import PropTypes from "prop-types";
6 | import { date } from "../format";
7 | /** Styled Components */
8 | import IMG from "../styled/IMG";
9 | import Badge from "../styled/Badge";
10 | import {
11 | Card,
12 | CardContent,
13 | CardLeft,
14 | TopicCardBody,
15 | CardRight
16 | } from "../styled/Cards";
17 |
18 | class Topic extends PureComponent {
19 | static propTypes = {
20 | topic: PropTypes.object
21 | };
22 |
23 | render() {
24 | const { id, title, member, replies, node, created } = this.props.topic;
25 | return (
26 |
27 |
28 |
29 |
30 |
31 | {title}
32 |
41 |
42 |
43 | {replies}
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | export default Topic;
51 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | /** Actions && Selectors */
4 | import { makeSelectHackerNews } from "../selector";
5 | /** Tools */
6 | import styled from "styled-components";
7 | import PropTypes from "prop-types";
8 | /** Child Components */
9 | import Story from "./components/Story";
10 | /** Third Party Components */
11 | import ContentLoader from "react-content-loader";
12 | /** Styled Components */
13 | import UL from "./styled/UL";
14 |
15 | class HackerNews extends Component {
16 | static propTypes = {
17 | stopFetch: PropTypes.func,
18 | loadHackerNews: PropTypes.func,
19 | stories: PropTypes.array
20 | };
21 | componentDidMount() {
22 | this.props.stopFetch();
23 | this.props.loadHackerNews();
24 | }
25 |
26 | render() {
27 | return (
28 |
29 | HackerNews
30 | {!this.props.stories.length
31 | ?
35 | : this.props.stories.map(story => (
36 |
37 | ))}
38 |
39 | );
40 | }
41 | }
42 |
43 | const mapDispatchToProps = dispatch => ({
44 | loadHackerNews: () => dispatch({ type: "LOAD_HACKERNEWS" }),
45 | stopFetch: () => dispatch({ type: "STOP_FETCH" })
46 | });
47 |
48 | const mapStateToProps = state => ({
49 | stories: makeSelectHackerNews(state)
50 | });
51 |
52 | export default connect(mapStateToProps, mapDispatchToProps)(HackerNews);
53 |
--------------------------------------------------------------------------------
/src/containers/Editor/reducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import * as actionTypes from '../App/constant'
3 | /** Child Reducers */
4 | import ambientSound from '../Embient/reducer'
5 |
6 | // Initialization content
7 | const initialText = `## Welcome
8 |
9 | Hi, I am distraction-free text editor :)
10 |
11 | Features:
12 | - Support markdown syntax
13 | - Automatically save current text for you
14 | - Save / Download file`
15 |
16 | const initialState = Immutable.fromJS({
17 | textValue: initialText,
18 | isPreview: false,
19 | isSaving: false,
20 | isBrowsing: false,
21 | savedFiles: {}
22 | })
23 |
24 | const editor = (state = initialState, action) => {
25 | switch (action.type) {
26 | case actionTypes.TOGGLE_PREVIEW:
27 | return state.update('isPreview', isPreview => !isPreview)
28 | case actionTypes.TOGGLE_SAVEFILE:
29 | return state.update('isSaving', isSaving => !isSaving)
30 | case actionTypes.TOGGLE_BROWSE:
31 | return state.update('isBrowsing', isBrowsing => !isBrowsing)
32 | case actionTypes.EDIT_MARKDOWN:
33 | return state.set('textValue', action.payload)
34 | case actionTypes.SAVE_NEWFILE:
35 | return state.setIn(['savedFiles', action.name], action.textValue)
36 | case actionTypes.REMOVE_FILE:
37 | return state.deleteIn(['savedFiles', action.name])
38 | case actionTypes.LOAD_LOCALFILES:
39 | // pitfall alert: ensure that your value is immutable type
40 | // So this is a downside of immutable.js
41 | return state.set('savedFiles', Immutable.fromJS(action.payload))
42 | default:
43 | return state
44 | }
45 | }
46 |
47 | export default editor
48 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/ReplyItem.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import styled from "styled-components";
4 | import PropTypes from "prop-types";
5 | /** Helper */
6 | import { date } from "../format";
7 | /**Styled Component */
8 | import { Card, CardLeft, CardBody } from "../styled/Cards";
9 | import IMG from "../styled/IMG";
10 | import MarkDown from "../styled/MarkDown";
11 |
12 | const ReplyBody = styled(CardBody)`
13 | & .info {
14 | margin-bottom: 6px;
15 | }
16 | & .info .name {
17 | font-weight: 700;
18 | padding-right: 5px;
19 | font-size: 13px;
20 | }
21 | & .info .ago {
22 | color: #bbb;
23 | }
24 | `;
25 |
26 | const ReplyContent = styled(MarkDown)`
27 | max-width: 1100px;
28 | margin: 0 auto;
29 | & span img {
30 | width: 100%;
31 | }
32 | `;
33 |
34 | class ReplyItem extends PureComponent {
35 | static propTypes = {
36 | reply: PropTypes.object
37 | };
38 | render() {
39 | const { member, created, content_rendered } = this.props.reply;
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {member.username}
51 | {date(created)}
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | export default ReplyItem;
63 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/Topic.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | /** Actions && Selectors */
4 | import { makeSelectV2exReplies, makeSelectV2exTopicInfo } from "../../selector";
5 | /** Tools */
6 | import styled from "styled-components";
7 | import PropTypes from "prop-types";
8 | /** Child Components */
9 | import Replies from "./Replies";
10 | import Content from "./Content";
11 |
12 | class Topic extends Component {
13 | static propTypes = {
14 | match: PropTypes.object,
15 | topic: PropTypes.object,
16 | replies: PropTypes.array,
17 | cleanCache: PropTypes.func,
18 | loadV2exTopic: PropTypes.func
19 | };
20 |
21 | componentWillMount() {
22 | this.props.cleanCache();
23 | }
24 |
25 | componentDidMount() {
26 | this.props.loadV2exTopic(this.props.match.params.id);
27 | }
28 |
29 | render() {
30 | const { topic, replies } = this.props;
31 | const isEmpty = topic.id === -1; // Guarantee Flag
32 | return (
33 |
34 | {!isEmpty
35 | ?
36 |
37 |
38 |
39 | :
Seems we cannot fetch the topics, check out later :(
}
40 |
41 | );
42 | }
43 | }
44 |
45 | const mapStateToProps = state => ({
46 | topic: makeSelectV2exTopicInfo(state),
47 | replies: makeSelectV2exReplies(state)
48 | });
49 |
50 | const mapDispatchToProps = dispatch => ({
51 | loadV2exTopic: id => dispatch({ type: "LOAD_V2EX_TOPIC", id }),
52 | cleanCache: () => dispatch({ type: "CLEAN_TOPIC_CACHE" })
53 | });
54 |
55 | export default connect(mapStateToProps, mapDispatchToProps)(Topic);
56 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import { createLogger } from 'redux-logger'
3 | import { routerMiddleware } from 'react-router-redux'
4 | import { fromJS } from 'immutable'
5 | import createSagaMiddleware from 'redux-saga'
6 | import createAppReducer from './reducers'
7 | import Perf from 'react-addons-perf'
8 |
9 | const sagaMiddleware = createSagaMiddleware()
10 |
11 | export default function configureStore (initialState = {}, history) {
12 | const win = window
13 | win.perf = Perf
14 | /**
15 | * Create the store with three middlewares
16 | * 1. action Logger: mark every dispatch
17 | * 2. sagaMiddleware: Makes redux-sagas work
18 | * 3. routerMiddleware: Syncs the location/URL path to the state
19 | */
20 | const middlewarles = []
21 | if (process.env.NODE_ENV !== 'production') {
22 | middlewarles.push(
23 | createLogger({
24 | collapsed: false,
25 | stateTransformer: state => state.toJS()
26 | })
27 | )
28 | }
29 | middlewarles.push(sagaMiddleware)
30 | middlewarles.push(routerMiddleware(history))
31 |
32 | const enhancers = [applyMiddleware(...middlewarles)]
33 |
34 | // If Redux DevTools Extension is installed, use it, otherwise use Redux compose
35 | /* eslint-disable no-underscore-dangle */
36 | const composeEnhancers = process.env.NODE_ENV !== 'production' &&
37 | typeof window === 'object' &&
38 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
39 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
40 | : compose
41 | /* eslint-enable */
42 |
43 | const store = createStore(
44 | createAppReducer(),
45 | fromJS(initialState),
46 | composeEnhancers(...enhancers)
47 | )
48 |
49 | // Extensions
50 | store.runSaga = sagaMiddleware.run
51 |
52 | return store
53 | }
54 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/BrowseFileModal.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { connect } from "react-redux";
3 | /** Tools */
4 | import PropTypes from "prop-types";
5 | /** Child Components */
6 | import FileList from "./fileList";
7 | import FileItem from "./fileItem";
8 | /** Maerial Components */
9 | import Dialog from "material-ui/Dialog";
10 | import FlatButton from "material-ui/FlatButton";
11 | import RaisedButton from "material-ui/RaisedButton";
12 |
13 | class BrowseFileModal extends PureComponent {
14 | static propTypes = {
15 | isBrowsing: PropTypes.bool,
16 | toggleBrowse: PropTypes.func,
17 | savedFiles: PropTypes.object,
18 | loadLocalFiles: PropTypes.func,
19 | openFile: PropTypes.func,
20 | removeFile: PropTypes.func
21 | };
22 |
23 | render() {
24 | const {
25 | isBrowsing,
26 | toggleBrowse,
27 | savedFiles,
28 | openFile,
29 | removeFile
30 | } = this.props;
31 |
32 | const actions = [
33 |
39 | ];
40 |
41 | return (
42 |
43 |
69 |
70 | );
71 | }
72 | }
73 |
74 | export default BrowseFileModal;
75 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/HackerNews/components/Story.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import styled from "styled-components";
4 | import PropTypes from "prop-types";
5 | /** Styled Components */
6 | import Wrapper from "../styled/Wrapper";
7 | import A from "../styled/A";
8 | import Header from "../styled/Header";
9 | import Footer from "../styled/Footer";
10 | import FooterItem from "../styled/FooterItem";
11 |
12 | class Story extends PureComponent {
13 | static propTypes = {
14 | story: PropTypes.object
15 | };
16 |
17 | render() {
18 | const { by, id, commentCount, title, points, url, ago } = this.props.story;
19 |
20 | const Points = styled(FooterItem)`
21 | color: #757575;
22 | `;
23 |
24 | const By = styled(FooterItem)`
25 | &, & a {
26 | color: #607D8B !important;
27 | }
28 | & a {
29 | font-weight: 500;
30 | }
31 | `;
32 |
33 | const TimeStamp = styled(FooterItem)`
34 | color: #009688;
35 | margin-right: 1rem;
36 | `;
37 |
38 | return (
39 |
40 |
49 |
73 |
74 | );
75 | }
76 | }
77 |
78 | export default Story;
79 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { connect } from "react-redux";
3 | import { Route, Link, Switch } from "react-router-dom";
4 | /** Actions && Selectors */
5 | import { makeSelectV2exTopics, makeSelectV2exHot } from "../selector";
6 | /** Tools */
7 | import styled from "styled-components";
8 | import PropTypes from "prop-types";
9 | /** Child Components */
10 | import Hot from "./components/Hot";
11 | import Main from "./components/Main";
12 | import UserInfo from "./components/UserInfo";
13 | import Topic from "./components/Topic";
14 | /** Third Party Components */
15 | import ContentLoader from "react-content-loader";
16 | /** Styled Components */
17 | import Row from "./styled/Row";
18 | import Container from "./styled/Container";
19 | import Background from "./styled/Background";
20 | import NoPaddingXS from "./styled/NoPaddingXS";
21 |
22 | class V2EX extends PureComponent {
23 | static propTypes = {
24 | match: PropTypes.object,
25 | location: PropTypes.object
26 | };
27 |
28 | render() {
29 | const MainWrapper = styled(NoPaddingXS)`
30 | @media all and (min-width: 992px) {
31 | width: 75%;
32 | float: left;
33 | }
34 | `;
35 |
36 | const SideWrapper = styled(NoPaddingXS)`
37 | @media all and (min-width: 992px) {
38 | width: 25%;
39 | float: left;
40 | }
41 | `;
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 | }
53 | />
54 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 | }
70 |
71 | export default V2EX;
72 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/Hot.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | /** Actions && Selectors */
4 | import { makeSelectV2exHot } from "../../selector";
5 | /** Tools */
6 | import styled from "styled-components";
7 | import PropTypes from "prop-types";
8 | /**Child Components */
9 | import HotItem from "./HotItem";
10 | /**Styled Components */
11 | import Panel from "../styled/Panel";
12 | import PHeading from "../styled/PHeading";
13 | import PBody from "../styled/PBody";
14 |
15 | const HotPanel = styled(Panel)`
16 | & .fade {
17 | opacity: 100;
18 | color: #ccc;
19 | margin: 0;
20 | }
21 | `;
22 |
23 | class Hot extends Component {
24 | static propTypes = {
25 | hotTopics: PropTypes.array,
26 | loadHotTopics: PropTypes.func
27 | };
28 |
29 | componentDidMount() {
30 | this.props.loadHotTopics();
31 | }
32 |
33 | checkArrayEqual(arr1, arr2) {
34 | arr1.forEach((hotTopic, idx) => {
35 | if (hotTopic.content !== arr2[idx].content) {
36 | return false;
37 | }
38 | });
39 | return true;
40 | }
41 |
42 | shouldComponentUpdate(nextProps) {
43 | if (
44 | this.props.hotTopics.length &&
45 | this.checkArrayEqual(this.props.hotTopics, nextProps.hotTopics)
46 | ) {
47 | console.log(
48 | "%c Hot Topics remain unchanged",
49 | "color: #2196f3; font-weight: lighter; font-size: 20px"
50 | );
51 | return false;
52 | }
53 | return true;
54 | }
55 |
56 | render() {
57 | return (
58 |
59 |
60 | 今日热议主题
61 |
62 |
63 | {this.props.hotTopics &&
64 | this.props.hotTopics.map(hotTopic => (
65 |
66 | ))}
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | const mapStateToProps = state => ({
74 | hotTopics: makeSelectV2exHot(state).toArray()
75 | });
76 |
77 | const mapDispatchToProps = dispatch => ({
78 | loadHotTopics: () => dispatch({ type: "LOAD_V2EX_HOT" })
79 | });
80 |
81 | export default connect(mapStateToProps, mapDispatchToProps)(Hot);
82 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/SaveFileModal.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | /** Material Components */
5 | import Dialog from "material-ui/Dialog";
6 | import FlatButton from "material-ui/FlatButton";
7 | import RaisedButton from "material-ui/RaisedButton";
8 | import DatePicker from "material-ui/DatePicker";
9 | import TextField from "material-ui/TextField";
10 |
11 | class SaveFileModal extends PureComponent {
12 | static propTypes = {
13 | isSaving: PropTypes.bool,
14 | onSave: PropTypes.func,
15 | onCancel: PropTypes.func,
16 | textValue: PropTypes.string
17 | };
18 |
19 | handleSave = () => {
20 | const name = this.refs.input.getValue();
21 | const textValue = this.props.textValue;
22 | this.props.onSave(name, textValue);
23 | this.props.onCancel();
24 | };
25 |
26 | mockSubmit = e => {
27 | e.keyCode === 13 && this.handleSave();
28 | };
29 |
30 | render() {
31 | const { isSaving, onSave, onCancel, textValue } = this.props;
32 | const actions = [
33 | ,
39 |
45 | ];
46 | return (
47 |
48 |
73 |
74 | );
75 | }
76 | }
77 |
78 | export default SaveFileModal;
79 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const pkg = require("./package.json");
3 | const webpack = require("webpack");
4 | const path = require("path");
5 | const HappyPack = require("happypack");
6 | const HtmlWebpackPlugin = require("html-webpack-plugin");
7 | var BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
8 | .BundleAnalyzerPlugin;
9 |
10 | module.exports = {
11 | entry: {
12 | app: "./src/index.js",
13 | vendor: Object.keys(pkg.dependencies)
14 | },
15 | // devtool: "cheap-module-source-map",
16 | output: {
17 | path: path.resolve(__dirname, "dist"),
18 | filename: "[name]-[chunkhash].js"
19 | },
20 | devServer: {
21 | historyApiFallback: true,
22 | contentBase: path.join(__dirname, "dist"),
23 | compress: true,
24 | hot: true,
25 | port: 8080
26 | },
27 | resolve: {
28 | extensions: [".json", ".js", ".jsx", ".css"]
29 | },
30 | node: {
31 | fs: "empty" // For error: Can't resolve 'fs' in /node_modules/browserslist
32 | },
33 | module: {
34 | loaders: [
35 | {
36 | test: /\.jsx|js?$/,
37 | exclude: /node_modules/,
38 | // loaders: ["happypack/loader?id=buildStuff"]
39 | loaders: ["react-hot-loader", "babel-loader"] //"eslint-loader"
40 | },
41 | {
42 | test: /\.css$/,
43 | // loaders: ["happypack/loader?id=css"]
44 | loaders: ["style-loader", "css-loader"]
45 | },
46 | {
47 | test: /\.scss$/,
48 | // loaders: ["happypack/loader?id=scss"]
49 | loaders: ["style-loader", "css-loader", "sass-loader"]
50 | }
51 | ]
52 | },
53 | plugins: [
54 | new BundleAnalyzerPlugin(),
55 | new HtmlWebpackPlugin({
56 | template: "./index.html",
57 | filename: "./index.html",
58 | title: "CoderPad"
59 | }),
60 | new webpack.optimize.CommonsChunkPlugin({
61 | names: ["vendor", "manifest"]
62 | })
63 | // new HappyPack({
64 | // id: "buildStuff",
65 | // loaders: ["react-hot-loader", "babel-loader", "eslint-loader"]
66 | // }),
67 | // new HappyPack({
68 | // id: "css",
69 | // loaders: ["style-loader", "css-loader"]
70 | // }),
71 | // new HappyPack({
72 | // id: "scss",
73 | // loaders: ["style-loader", "css-loader", "sass-loader"]
74 | // })
75 | ]
76 | };
77 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const AV = require('leanengine')
3 | const axios = require('axios')
4 | const path = require('path')
5 |
6 | AV.init({
7 | appId: process.env.LEANCLOUD_APP_ID || 'Cct4URhkJo6VKYACsR3MklFt-gzGzoHsz',
8 | appKey: process.env.LEANCLOUD_APP_KEY || 'BeJzllYAryhOzoNX0piav8tw',
9 | masterKey: process.env.LEANCLOUD_APP_MASTER_KEY || 'U5jcAlilltDr1vJmyfwIqC2D'
10 | })
11 |
12 | const app = express()
13 | app.use(AV.express())
14 | app.use(express.static(path.resolve(__dirname, './dist')))
15 |
16 | const V2EX_BASE_URL = 'https://www.v2ex.com/api'
17 |
18 | // v2ex topic info
19 | app.get('/news/v2ex/topics/show.json?', function (req, response) {
20 | let url = ''
21 | if (req.query.id) {
22 | const id = req.query.id
23 | url = `${V2EX_BASE_URL}/topics/show.json?id=${id}`
24 | axios.get(url).then(res => {
25 | response.send(res.data[0])
26 | })
27 | } else if (req.query['node_name']) {
28 | const contentType = req.query['node_name']
29 | url = `${V2EX_BASE_URL}/topics/show.json?node_name=${contentType}`
30 | axios.get(url).then(res => response.send(res.data))
31 | }
32 | })
33 |
34 | // v2ex topic replies
35 | app.get('/news/v2ex/replies/show.json?', function (req, response) {
36 | let repliesUrl = ''
37 | if (req.query['topic_id']) {
38 | const id = req.query['topic_id']
39 | repliesUrl = `${V2EX_BASE_URL}/replies/show.json?topic_id=${id}`
40 | axios.get(repliesUrl).then(res => {
41 | response.send(res.data)
42 | })
43 | }
44 | })
45 |
46 | // v2ex hot
47 | app.get('/news/v2ex/topics/hot.json', function (req, res) {
48 | const url = `${V2EX_BASE_URL}/topics/hot.json`
49 | axios.get(url).then(response => res.send(response.data))
50 | })
51 |
52 | // v2ex latest
53 | app.get('/news/v2ex/topics/latest.json', function (req, res) {
54 | const url = `${V2EX_BASE_URL}/topics/latest.json`
55 | axios.get(url).then(response => res.send(response.data))
56 | })
57 |
58 | // app.use(
59 | // "^(?!.*?topics)(?!.*?replies)(?!.*?vendor)(?!.*?app)(?!.*?manifest).*$",
60 | // (req, res) => {
61 | // res.sendFile(path.resolve(__dirname, "./dist/index.html"));
62 | // }
63 | // );
64 |
65 | app.get('*', (req, res) => {
66 | res.sendFile(path.resolve(__dirname, './dist/index.html'))
67 | })
68 | // app.listen(process.env.LEANCLOUD_APP_PORT);
69 | app.listen(3000)
70 | console.log('Server started')
71 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/Main.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | /** Actions && Selectors */
4 | import { makeSelectV2exTopics } from "../../selector";
5 | /** Tools */
6 | import PropTypes from "prop-types";
7 | /** Third Party Components */
8 | import ContentLoader from "react-content-loader";
9 | /** Child Components */
10 | import TopicInfo from "./TopicInfo";
11 | /** styled-components */
12 | import Panel from "../styled/Panel";
13 | import PHeading from "../styled/PHeading";
14 | import PBody from "../styled/PBody";
15 | import Tab from "../styled/Tab";
16 |
17 | class Main extends Component {
18 | static propTypes = {
19 | location: PropTypes.object,
20 | stopFetch: PropTypes.func,
21 | loadV2exTopics: PropTypes.func,
22 | topics: PropTypes.array
23 | };
24 |
25 | componentDidMount() {
26 | this.props.stopFetch();
27 | const nodeName = new URLSearchParams(this.props.location.search).get("tab");
28 | const contentType = nodeName ? nodeName : "programmer";
29 | this.props.loadV2exTopics(contentType);
30 | }
31 |
32 | render() {
33 | const { topics } = this.props;
34 | return (
35 |
36 |
37 | 程序员
38 | Python
39 | JavaScript
40 | 分享创造
41 | Node.js
42 | 酷工作
43 | 最新
44 |
45 |
46 | {topics.length
47 | ? topics.map(topic => )
48 | : }
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | const mapStateToProps = state => ({
59 | topics: makeSelectV2exTopics(state).toArray()
60 | });
61 |
62 | const mapDispatchToProps = dispatch => ({
63 | loadV2exTopics: contentType =>
64 | dispatch({ type: "LOAD_V2EX_TOPICS", contentType }),
65 | stopFetch: () => dispatch({ type: "STOP_FETCH" })
66 | });
67 |
68 | export default connect(mapStateToProps, mapDispatchToProps)(Main);
69 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/fileItem.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import PropTypes from "prop-types";
4 | import styled from "styled-components";
5 | /** Styled Components */
6 | import LI from "../styled/LI";
7 | /** Material Components */
8 | import TextField from "material-ui/TextField";
9 | import deleteBtn from "material-ui/svg-icons/action/delete";
10 | import Download from "material-ui/svg-icons/content/archive";
11 |
12 | const Delete = styled(deleteBtn)`
13 | align-items: flex-end;
14 | `;
15 |
16 | class FileItem extends PureComponent {
17 | static propTypes = {
18 | fileName: PropTypes.string,
19 | content: PropTypes.string,
20 | openFile: PropTypes.func,
21 | toggleBrowse: PropTypes.func,
22 | removeFile: PropTypes.func
23 | };
24 | destroyClickedElement = event => {
25 | document.body.removeChild(event.target);
26 | };
27 |
28 | saveTextAsFile = (text, filename) => {
29 | var textFileAsBlob = new Blob([text], { type: "text/plain" });
30 | var downloadLink = document.createElement("a");
31 | downloadLink.download = filename + ".md";
32 | downloadLink.innerHTML = "Download File";
33 | if (window.webkitURL != null) {
34 | // Chrome allows the link to be clicked
35 | // without actually adding it to the DOM.
36 | downloadLink.href = window.webkitURL.createObjectURL(textFileAsBlob);
37 | } else {
38 | // Firefox requires the link to be added to the DOM
39 | // before it can be clicked.
40 | downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
41 | downloadLink.onclick = this.destroyClickedElement;
42 | downloadLink.style.display = "none";
43 | document.body.appendChild(downloadLink);
44 | }
45 | downloadLink.click();
46 | };
47 |
48 | downloadFile = () => {
49 | this.saveTextAsFile(this.props.content, this.props.fileName.substr(9));
50 | };
51 |
52 | render() {
53 | const {
54 | fileName,
55 | content,
56 | openFile,
57 | toggleBrowse,
58 | removeFile
59 | } = this.props;
60 |
61 | return (
62 |
63 | {
65 | openFile(fileName);
66 | toggleBrowse();
67 | }}
68 | >
69 | {fileName.substr(9)}
70 |
71 | removeFile(fileName)} />
72 |
73 |
74 | );
75 | }
76 | }
77 |
78 | export default FileItem;
79 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/UserInfo.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | /** Tools */
3 | import styled from 'styled-components'
4 | /** Styled Components */
5 | import IMG from '../styled/IMG'
6 | import PBody from '../styled/PBody'
7 | import Panel from '../styled/Panel'
8 |
9 | const InfoBody = styled(PBody)`
10 | padding: 10px;
11 | & .top {
12 | line-height: 120%;
13 | padding-bottom: 8px;
14 | }
15 | & .bigger {
16 | font-size: 16px;
17 | }
18 | & img {
19 | vertical-align: middle;
20 | }
21 | & .inlineBlock {
22 | display: inline-block;
23 | text-align: center;
24 | }
25 | & .bottomBorder {
26 | margin: 8px -10px;
27 | border-bottom: 1px solid #e2e2e2;
28 | }
29 | `
30 |
31 | class UserInfo extends PureComponent {
32 | render () {
33 | return (
34 |
35 |
36 |
75 |
76 |
77 | )
78 | }
79 | }
80 |
81 | export default UserInfo
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CoderPad",
3 | "version": "1.1.0",
4 | "description": "Newsly Application built with react & redux & redux-saga",
5 | "author": "KYLEWH",
6 | "license": "ISC",
7 | "main": "./src/index.js",
8 | "scripts": {
9 | "start": "node server.js",
10 | "dev": "webpack-dev-server --inline --progress --colors --hot",
11 | "clean": "rimraf dist",
12 | "build": "npm run clean && webpack -p"
13 | },
14 | "engines": {
15 | "node": "7.6.0",
16 | "npm": "4.1.2"
17 | },
18 | "keywords": [
19 | "react",
20 | "redux",
21 | "Newsly"
22 | ],
23 | "babel": {
24 | "presets": [
25 | "es2015",
26 | "es2016",
27 | "react",
28 | "stage-2"
29 | ]
30 | },
31 | "dependencies": {
32 | "axios": "^0.16.1",
33 | "babel-polyfill": "^6.23.0",
34 | "caniuse-api": "^1.6.1",
35 | "classnames": "^2.2.5",
36 | "history": "^4.6.1",
37 | "immutable": "^3.8.1",
38 | "express": "^4.15.3",
39 | "leanengine": "^2.0.3",
40 | "lodash": "^4.17.4",
41 | "marked": "^0.3.6",
42 | "material-ui": "^0.18.0",
43 | "moment": "^2.18.1",
44 | "prop-types": "^15.5.10",
45 | "react": "^15.5.4",
46 | "react-back2top": "^0.1.5",
47 | "react-dom": "^15.5.4",
48 | "react-redux": "^5.0.4",
49 | "react-router-dom": "^4.1.1",
50 | "react-router-redux": "^5.0.0-alpha.6",
51 | "react-tap-event-plugin": "^2.0.1",
52 | "react-textarea-autosize": "^4.3.2",
53 | "redux": "^3.6.0",
54 | "redux-immutable": "^4.0.0",
55 | "redux-logger": "^3.0.1",
56 | "redux-saga": "^0.15.3",
57 | "reselect": "^3.0.1",
58 | "styled-components": "^2.0.0",
59 | "styled-props": "^0.2.0"
60 | },
61 | "devDependencies": {
62 | "babel-core": "^6.24.1",
63 | "babel-eslint": "^7.2.3",
64 | "babel-loader": "^7.0.0",
65 | "babel-preset-es2015": "^6.24.1",
66 | "babel-preset-es2016": "^6.24.1",
67 | "babel-preset-react": "^6.24.1",
68 | "babel-preset-stage-2": "^6.24.1",
69 | "css-loader": "^0.28.1",
70 | "eslint": "^3.19.0",
71 | "eslint-config-standard": "^10.2.1",
72 | "eslint-loader": "^1.7.1",
73 | "eslint-plugin-import": "^2.2.0",
74 | "eslint-plugin-node": "^4.2.2",
75 | "eslint-plugin-promise": "^3.5.0",
76 | "eslint-plugin-react": "^6.10.3",
77 | "eslint-plugin-standard": "^3.0.1",
78 | "happypack": "^3.0.3",
79 | "html-webpack-plugin": "^2.28.0",
80 | "node-sass": "^4.5.3",
81 | "react-addons-perf": "^15.4.2",
82 | "react-content-loader": "^1.3.1",
83 | "react-hot-loader": "^1.3.1",
84 | "rimraf": "^2.6.1",
85 | "sass-loader": "^6.0.5",
86 | "style-loader": "^0.17.0",
87 | "webpack": "^2.5.1",
88 | "webpack-bundle-analyzer": "^2.8.2",
89 | "webpack-dev-server": "^2.4.5"
90 | }
91 | }
--------------------------------------------------------------------------------
/src/containers/NewsFeed/V2EX/components/Content.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | /** Tools */
3 | import styled from "styled-components";
4 | import PropTypes from "prop-types";
5 | /** Third Party Components */
6 | import ContentLoader from "react-content-loader";
7 | /** Helper */
8 | import { date } from "../format";
9 | /**Styled Components */
10 | import Panel from "../styled/Panel";
11 | import PHeading from "../styled/PHeading";
12 | import PBody from "../styled/PBody";
13 | import MarkDown from "../styled/MarkDown";
14 | import PFooter from "../styled/PFooter";
15 | import Chevron from "../styled/Chevron";
16 | import H1 from "../styled/H1";
17 | import { CardBody, TopicCardBody } from "../styled/Cards";
18 |
19 | const TopicHead = styled(PHeading)`
20 | font-size: 14px;
21 | line-height: 120%;
22 | `;
23 |
24 | const HeadBody = styled(TopicCardBody)`
25 | text-align: left;
26 | `;
27 |
28 | const TopicTitle = styled(H1)`
29 | font-size: 24px;
30 | font-weight: 400;
31 | margin-top: 12px;
32 | margin-bottom: 8px;
33 | `;
34 |
35 | const TopicContent = styled(MarkDown)`
36 | padding: 10px;
37 | & span img {
38 | max-width: 100%;
39 | }
40 | `;
41 |
42 | class Content extends PureComponent {
43 | static propTypes = {
44 | topic: PropTypes.object
45 | };
46 | render() {
47 | const {
48 | id,
49 | member,
50 | node,
51 | title,
52 | created,
53 | content_rendered
54 | } = this.props.topic.toJS();
55 |
56 | return (
57 |
58 | {this.props.topic.toJS() &&
59 | id &&
60 |
61 |
62 |
63 |

68 |
69 |
70 | V2EX
71 | ›
72 | {node.title}
73 | {title}
74 |
85 |
86 |
87 |
88 |
93 |
94 |
95 | 加入收藏
96 | Tweet
97 | Weibo
98 | 忽略主题
99 | 感谢
100 |
101 | }
102 | {!id &&
103 |
}
107 |
108 | );
109 | }
110 | }
111 |
112 | export default Content;
113 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/SvgIcons/NewsSVG.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import StyledSvg from './StyledSvg'
3 |
4 | const NewsSVG = () => (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 |
27 | export default NewsSVG
28 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/components/Icon.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SvgIcon from 'material-ui/SvgIcon'
3 |
4 | export const HackerNewsIcon = () => (
5 |
6 |
7 |
8 |
12 |
13 |
14 | )
15 |
16 | export const GitHubIcon = () => (
17 |
18 |
19 |
23 |
24 |
25 | )
26 |
27 | export const V2exIcon = () => (
28 |
29 |
30 |
33 |
34 |
35 |
42 |
47 | V 2
48 |
49 |
54 | E X
55 |
56 |
57 |
58 | )
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Coderpad
2 |
3 | 
4 | - [在线地址](http://coderpad.leanapp.cn/)
5 | - [GITHUB](https://github.com/kylewh/Coderpad)
6 |
7 | 大家伙儿们,又见面了😉。 自上次Byemess Todo之后,觉得自身不足还是挺多的,期间又萌生了一些将它重构加上更多新特性的想法,之后技术磨炼一阵再来好好改造它。对于Learn by doing这种事情,一次就会上瘾啊有木有❤️,于是乎本着继续精进练习React技术栈,以及实践更多相关技术的初衷,besides that,自己还想再准备一个小项目来为找工作打底气,于是乎就有了CoderPad。
8 |
9 | ## 💡 WHY IS THIS
10 | 在最开始的时候,是想做一个催稿app,又是一个集成的idea:提供分类书单,可以记录阅读情况,然后根据这个情况设定或者后台计算智能推荐:何时去写一篇相关的博客(技术博客),当然写作也是在这个app里完成,然后自动部署至github page。 名字我都想好了,就叫催乎...(知乎er们懂的),奈何这是个大工程啊... 我就造出了这么个只有**编辑,阅读的阉割版**。 另外关于写完markdown直接部署生成静态博客的app我推荐好基友的[Page.qy => 🤘 无代码建站,基于Node.js,React和Electron](https://zhuanlan.zhihu.com/p/26917820?utm_medium=social&utm_source=ZHShareTargetIDMore),很用心的app,向他学习之,他马上又会写出一个UI逆天美的音乐播放器了,你们可以关注他。
11 |
12 | ## 🚀 WHAT IT IS
13 | 1. **Markdown:** 支持本地缓存,保存/删除/查看/下载,追求极简。
14 | 
15 | 2. **NewsFeed:** 集成v2ex,HackerNews-Top Stories, Github-Trending
16 | 
17 | 3. ~~**Music**: 暂未施工~~
18 |
19 | ## 🔗Techniques
20 | - **老朋友React全家桶**: 对于这块,值得一提的是react router v4,相对于v3的巨大改动,extremely make sense. 让route与组件化思想更贴切,有种幻觉:定义子route更加随心所欲了。至于为什么... 请君上手感受。
21 |
22 | - **Immutable**: 有一些细微的坑,主要体现在数据类型转化上,immutable会将原生JS数据类型进行包装,如Map,List,在对它们进行提取的时候需要注意是否已经转化为原生JS,否则容易出错。 我的建议就是时刻注意提取的数据是什么类型,结合PropTypes进行参数检测,出错时先console看看,一般很快可以解决。 对于多层对象嵌套的时候,为了保险,手动将被嵌套的对象进行指定类型转化,比如`{ list: [] } => { list:Immutable.List([]) }`,如果要偷懒的话可以直接使用`fromJS`,但是这个方法渗透性强,会将所有内嵌结构进行转化,在本项目的后期重构里就遇到了子数组遍历出来全是immutable object的情况,需要手动再次转化,很是恶心。 这些缺点在redux文档里也有表述,在具体实践后才能有更直观的理解。 参照: [What are the issues with using Immutable.JS?](http://redux.js.org/docs/recipes/UsingImmutableJS.html#issues-with-immutable-js).但不可否认Immutable.js非常符合react的思想,都在处理大规模数据时彰显优势。
23 |
24 | - **Reselect**:它用来替代我们手写的state selector, 它的主要思想: state1 + state2 => state3, 缓存先决state,它们如果计算结果是相同的,就使用缓存结果不去改变最终state,同样也是immutable思想。 在结合immutable.js的时候,也是坑啊,还是那个老问题,数据类型,state嵌套越深,越麻烦。 所以,现在明白为什么**强调Redux State扁平化**了吧?
25 |
26 | - **Redux Saga**: Oh my.. 无比亲切,至于原因: 我写过这么一篇文章:[From Iterator To Async](https://kylewh.github.io/2017/03/24/From-Iterator-To-Async/). Saga致力于解决复杂场景下的异步流程控制,用它来管理action触发,酸爽无比。 毕竟控制异步流程这种成就在JS话题下本身就是爽的不要不要的。 本质是使用generator,对于理解CO库的同学们,掌握saga不在话下。在操作极其频繁的场景下(比如游戏),你会感受到他的威力。 推荐一篇文章: [Async operations using redux-saga](https://medium.freecodecamp.com/async-operations-using-redux-saga-2ba02ae077b3), 在本项目里我主要用它来控制news数据的拉取,采用axios.
27 |
28 | - **Styled Components**: 老朋友,更新了2.0版本,同样配合**styled-props**,效果拔群。 至于一些宏观上的样式设置,的确不如直接写CSS那么直观。 我采用的方法是,特性按组件写,通性和一些涉及多层级样式都放在wrapper里。 也许单独使用styled-components并不能发挥出色,配合传统CSS写法,应该可以相得益彰。
29 |
30 | ## 🔑Problem and Solution
31 | - **ref**: 对于ref的感觉一直是又爱又恨,毕竟在react之前,dom操作被我们玩的飞起,而react官方的态度一直是不建议使用。 在这次的项目中,markdown editor处的textarea我便采用了Uncontrolled的形式,使用ref保存dom引用。 初衷是为了对频繁的内容变动进行debounce处理,当编辑暂停时才触发一次内容state更新。 随着组件的增加,在一个嵌套达到3层的modal组件里,需要对textarea的value进行重置操作,好了,这下我得从父组件一层层的把这个ref传进去。 那感觉简直不能再糟.... 一刹那感觉官方文档就像和蔼的老司机,句句肺腑之言啊! 不过在你真的遇到这个坑前,是不会有多深的感受的。 要解决这个恶心的传递,只有采用controlled形式,onChange监听,value直接链接state.
32 |
33 | - **Perf**: 作为性能测量的利器,测试结果让我发现styled-components的消耗是可观的,尤其是更新到v2.0版本后。在其他方面,由于本项目里的newsFeed可能会涉及频繁点击切换路径的情况,为了防止无谓的重复渲染,给所有presentational components都设置为PureComponent, 接着在一些只需要更新一次的组件里手写`shouldComponentUpdate`, 还是强调一点: 必须十分清楚传入的参数,以及其结构,并考虑这个结构是否在生产环境中有变化的可能导致判断失效。 还有个值得注意的问题是react-router-v4里的NavLink检测location渲染当前激活地址的link(activeClassName属性)时,注意组件是否是PureComponent, 如果是,必须在父组件传入location,否则PureComponent的`shouldComponentUpdate`将会判定参数无变化,从而block掉link的动态渲染。参照: [Dealing with Update Blocking](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/blocked-updates.md)
34 |
35 | - **Server Side**: 由于是使用leancloud部署,用node环境,为了解决v2ex api的跨域问题,自己写了一套请求转发,但是问题来了: 单页APP里为保证刷新后不出现cannot get等问题,必须写上一条`app.get('*', (req, res) => {res.sendFile('index.html的路径')} )`, 这就很麻烦了,后来经过请教,用正则过滤了请求转发涉及的路径,就避免了路径全局拦截。但是! 这样刷新后,又会遇到cannot get的问题了。 因为再次刷新时url已经变化,浏览器会去请求这个地址,而后台并没有提供此路径的响应。 最好的办法是使用nginx部署环境。(express难道就没办法? 还是我服务端知识短浅啊...要恶补) 另外一个问题: 生产环境和部署环境下由于使用了不同的请求地址,返回的数据的结构存在微小差异,以本项目为例,请求v2ex topic在生产环境下数据是`res.data`,而到了部署环境下由于使用了自己设置的请求地址,返回的数据成了`res.data[0]`,找了很久才发现问题,值得注意。
36 |
37 | - **Cancellation**: 在newsfeed里频繁切换页面时还有一个问题: 也许下一个页面呈现时,上一个页面中触发的fetch操作还没执行完毕。举个例子: 我进入了v2ex的页面,此时组件拉取新闻信息,接着我几乎不等待便切换至github,此时对于v2ex的拉取还在进行。这就是一种浪费了。 为了解决它,起初我尝试用saga结合react-router-redux里提供的`LOCATION_CHANGE` action来作为判定取消之前未完成fetch的标志。 测试发现就算我点击的是同一个link,依然会触发`LOCATION_CHANGE`,(真坑啊,完全不符合直觉好么!?)那么有这么一个场景: 当你进入hackerNews等待数据返回,由于时间较久,你不耐烦的**再次点击**了hackerNews的Link,**Boom~~! `LOCATION_CHANGE` dispatched. 于是乎你的fetch被取消了...**,所以用`LOCATION_CHANGE`作为判定标志在首次拉取这个场景下是不可行的(论corner case重要性..), 后来想出来的解决办法是在三块新闻组件的`componentDidMount`的顶部dispatch一个`STOP_FETCH` action,然后将判定取消的标志改为:`STOP_FETCH`,算是解决了,可总感觉有点暴力,因为一旦组件变多,将要手动。接下来将继续思考更优雅的解决方案,如果大神们有答案,请告知。
38 |
39 |
40 | ## 🏅Gain
41 | - 最大的收获: 主动找上问题,而不是问题找上你。 不折腾,不踩坑,进步颇微。
42 | - 当container变多时,直接将container component作为单位,单独设立目录,然后放置对应的components/styled-components/reducer/action. 这就是按feature组织目录的方法。 细致的拆分,解耦性更好,以container component为单位进行修改时,大大降低误伤率的同时,隔离无关的信息。
43 | - 大概总结出一个Learn by doing的心路历程:
44 | 1. 被未尝试的技术吸引,并且有了下一个project的idea
45 | 2. 尝试拆分所需技能,分成组块(裂墙推荐知乎[金旭亮老师](https://www.zhihu.com/people/jin-xu-liang)组块学习论)
46 | 3. 漫长的学习过程: 读文档,找样例,写小demo倒腾API。由于组块积累未完全,所以无法对project全面下手,自然会很烦躁,并且踏出了舒适区,接收更多的信息。
47 | 4. 组块知识积累完毕,project开始施工: 从最简功能需求开始,不断增加新feature: problem -> google -> resolve.
48 | 6. Project成型,评估,修正,改进,more problem come in.
49 | 7. 项目总结。然后享受一下独立完成project的成就感。同时也会深刻理解自己的不足,为自己的技术精进之路指明了方向。
50 | 8. 以project为单位,循环以上步骤。
51 |
52 | - 最后的领悟: 我早几年干什么去了... 捶胸泪目ing。
53 |
54 | ## ⛳️More Feature?
55 | 未来可能会补上的:
56 | - 白噪音组合播放
57 | - 番茄钟
58 | - 音乐部分(哈哈哈偷懒了时间不多了,赶紧找工作。)
59 |
60 | 作为一名新人,还请大家多多指教。同样无耻的求star,2333。
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/containers/Editor/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | /** Actions && Selectors */
4 | import * as editorActions from "./action";
5 | import {
6 | makeSelectTextValue,
7 | makeSelectIsPreview,
8 | makeSelectIsSaving,
9 | makeSelectIsBrowsing,
10 | makeSelectSavedFiles
11 | } from "./selector";
12 | /** Tool */
13 | import debounce from "lodash/debounce";
14 | import marked from "marked";
15 | import PropTypes from "prop-types";
16 | import classNames from "classnames";
17 | /** Styled Components */
18 | import AutoSizeTextarea from "./styled/Textarea";
19 | import Wrapper from "./styled/Wrapper";
20 | /** Child Components */
21 | import EditorPanel from "./components/EditorPanel";
22 | import SaveFileModal from "./components/SaveFileModal";
23 | import BrowseFileModal from "./components/BrowseFileModal";
24 | /** Material Components */
25 | import Preview from "material-ui/svg-icons/action/visibility";
26 | import Save from "material-ui/svg-icons/content/archive";
27 |
28 | class Editor extends Component {
29 | constructor(props) {
30 | super(props);
31 | this.onChange = debounce(this.onChange, 500);
32 | }
33 |
34 | componentWillMount() {
35 | // this._initHighLight();
36 | this.props.loadLocalFiles();
37 | }
38 |
39 | componentDidMount() {
40 | this.fillTextFromLocal();
41 | // Forced synchronization between state&LocalStorage
42 | this.props.editMarkdown(this.textarea.value);
43 | }
44 |
45 | /**
46 | * Doesn't work completely.
47 | * Cannot find out proper method yet.
48 | * @TODO: I want syntax highlighting !
49 | * So just ignore it.. fold these shit...
50 | */
51 |
52 | // _initHighLight() {
53 | // const hlScript = document.createElement("script");
54 | // hlScript.type = "text/javascript";
55 | // hlScript.src =
56 | // "//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/highlight.min.js";
57 | // document.getElementsByTagName("head")[0].appendChild(hlScript);
58 | // hlScript.onload = function() {
59 | // window.hljs.initHighlightingOnLoad();
60 | // console.log(
61 | // "%c HilghtJS initiaized",
62 | // "color: #8bc34a; font-weight: bold;"
63 | // );
64 | // };
65 | // }
66 |
67 | loadLocal = () => {
68 | return localStorage.getItem("currentText");
69 | };
70 |
71 | fillTextFromLocal = () => {
72 | this.textarea.value = this.loadLocal()
73 | ? this.loadLocal()
74 | : this.props.textValue;
75 | };
76 |
77 | setTextFromFileName = filename => {
78 | this.textarea.value = localStorage.getItem(filename);
79 | localStorage.setItem("currentText", this.textarea.value);
80 | this.props.editMarkdown(this.textarea.value);
81 | };
82 |
83 | onChange = () => {
84 | const result = this.textarea.value;
85 | localStorage.setItem("currentText", result);
86 | this.props.editMarkdown(result);
87 | };
88 |
89 | mockSave = e => {
90 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 83) {
91 | e.preventDefault();
92 | this.props.toggleSaveFile();
93 | }
94 | };
95 |
96 | render() {
97 | const {
98 | isPreview,
99 | isSaving,
100 | savedFiles,
101 | removeFile,
102 | textValue,
103 | isBrowsing,
104 | editMarkdown,
105 | toggleSaveFile,
106 | loadLocalFiles,
107 | togglePreview,
108 | toggleBrowse,
109 | saveNewFile
110 | } = this.props;
111 |
112 | const markdownCls = classNames({
113 | "hidden-toggle": isPreview || isBrowsing,
114 | markdown: true
115 | });
116 |
117 | const previewCls = classNames({
118 | "hidden-toggle": !isPreview,
119 | markdown: true
120 | });
121 |
122 | const browseCls = classNames({
123 | "hidden-toggle": !isBrowsing
124 | });
125 | return (
126 |
127 | {/* Markdown -Editor */}
128 | (this.textarea = node)}
131 | onChange={this.onChange}
132 | onKeyDown={this.mockSave}
133 | />
134 | {/* Preview -Overlay*/}
135 |
141 |
142 | {/* Editor tools panel - Aside */}
143 |
151 | {/* Modal: enter filename */}
152 |
158 |
165 |
166 | );
167 | }
168 | }
169 |
170 | Editor.propTypes = {
171 | textValue: PropTypes.string,
172 | togglePreview: PropTypes.func,
173 | isPreview: PropTypes.bool,
174 | isSaving: PropTypes.bool,
175 | isBrowsing: PropTypes.bool,
176 | editMarkdown: PropTypes.func,
177 | toggleSaveFile: PropTypes.func,
178 | loadLocalFiles: PropTypes.func,
179 | toggleBrowse: PropTypes.func,
180 | saveNewFile: PropTypes.func,
181 | removeFile: PropTypes.func,
182 | savedFiles: PropTypes.object
183 | };
184 |
185 | const mapStateToProps = state => ({
186 | textValue: makeSelectTextValue(state),
187 | isPreview: makeSelectIsPreview(state),
188 | isSaving: makeSelectIsSaving(state),
189 | isBrowsing: makeSelectIsBrowsing(state),
190 | savedFiles: makeSelectSavedFiles(state)
191 | });
192 |
193 | export default connect(mapStateToProps, editorActions)(Editor);
194 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/SvgIcons/RadioSVG.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import StyledSvg from './StyledSvg'
3 |
4 | const RadioSVG = () => (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 |
30 | export default RadioSVG
31 |
--------------------------------------------------------------------------------
/src/containers/DashBoard/SvgIcons/IdeaSVG.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import StyledSvg from './StyledSvg'
3 |
4 | const IdeaSVG = () => (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 |
27 | export default IdeaSVG
28 |
--------------------------------------------------------------------------------
/src/containers/NewsFeed/saga.js:
--------------------------------------------------------------------------------
1 | import { call, fork, put, take, takeLatest, cancel } from 'redux-saga/effects'
2 | import axios from 'axios'
3 | import moment from 'moment'
4 |
5 | const HACKERNEWS_BASE_URL = 'https://hacker-news.firebaseio.com/v0'
6 | const wrapApiKey = 'vZpCx0QXD65gAcUD4Q7gAL6y0GQB1pgT'
7 | const GITHUB_BASE_URL =
8 | 'https://wrapapi.com/use/sunnysingh/github/trending/0.0.4?wrapAPIKey=' +
9 | wrapApiKey
10 | // const V2EX_BASE_URL = "https://www.v2ex.com/api";
11 | const V2EX_BASE_URL = '/news/v2ex'
12 |
13 | /**
14 | * 30 top stories from hackerNews
15 | */
16 | const fetchHackerNews = () => {
17 | const data = []
18 | return axios
19 | .get(HACKERNEWS_BASE_URL + '/topstories.json')
20 | .then(res =>
21 | res.data
22 | .slice(0, 30)
23 | .map(storyId =>
24 | axios.get(HACKERNEWS_BASE_URL + '/item/' + storyId + '.json')
25 | )
26 | )
27 | .then(res => {
28 | return Promise.all(res).then(res => {
29 | res.forEach(item => {
30 | data.push({
31 | id: item.data.id,
32 | title: item.data.title,
33 | by: item.data.by,
34 | url: item.data.url,
35 | points: item.data.score,
36 | commentCount: item.data.descendants,
37 | ago: moment.unix(item.data.time).fromNow()
38 | })
39 | })
40 | })
41 | })
42 | .then(() => data)
43 | }
44 |
45 | function * loadHackerNewsData () {
46 | try {
47 | let stories = yield call(fetchHackerNews)
48 | yield put({ type: 'FETCH_HACKERNEWS_SUCCESS', payload: stories })
49 | } catch (error) {
50 | yield put({ type: 'FETCH_FAILED', error })
51 | }
52 | }
53 |
54 | const fetchGithub = () => {
55 | const data = []
56 | return axios
57 | .get(GITHUB_BASE_URL)
58 | .then(res => {
59 | res.data.data.repositories.map(repo => {
60 | data.push({
61 | url: 'https://github.com' + repo.url,
62 | user: repo.url.split('/')[1],
63 | name: repo.url.split('/')[2],
64 | description: repo.description ? repo.description.trim() : null,
65 | stars: parseInt(repo.stars),
66 | language: repo.language ? repo.language.trim() : null
67 | })
68 | })
69 | })
70 | .then(() => data)
71 | }
72 |
73 | function * loadGithub () {
74 | try {
75 | let repos = yield call(fetchGithub)
76 | yield put({ type: 'FETCH_GITHUB_SUCCESS', payload: repos })
77 | } catch (error) {
78 | yield put({ type: 'FETCH_FAILED', error })
79 | }
80 | }
81 |
82 | const fetchV2exTopic = id => {
83 | const ret = { topicInfo: {}, replies: [] }
84 | let url = `${V2EX_BASE_URL}/topics/show.json?id=${id}`
85 | let repliesUrl = `${V2EX_BASE_URL}/replies/show.json?topic_id=${id}`
86 | return axios
87 | .get(url)
88 | .then(res => {
89 | /**
90 | * WATCH OUT!!!!
91 | * When in dev environment we use v2ex api directly
92 | * So the data's sturcture will be different
93 | */
94 | if (V2EX_BASE_URL === '/news/v2ex') {
95 | ret.topicInfo = res.data
96 | } else if (V2EX_BASE_URL === 'https://www.v2ex.com/api') {
97 | ret.topicInfo = res.data[0]
98 | }
99 | return axios.get(repliesUrl)
100 | })
101 | .then(res => {
102 | ret.replies = res.data
103 | })
104 | .then(() => {
105 | return ret
106 | })
107 | }
108 |
109 | function * loadV2exTopic (id) {
110 | try {
111 | let topic = yield call(fetchV2exTopic, id)
112 | yield put({ type: 'FETCH_V2EX_TOPIC_SUCCESS', payload: topic })
113 | } catch (error) {
114 | yield put({ type: 'FETCH_FAILED', error })
115 | }
116 | }
117 |
118 | const fetchV2exTopics = contentType => {
119 | let url = ''
120 | switch (contentType) {
121 | case 'latest':
122 | url = `${V2EX_BASE_URL}/topics/latest.json`
123 | break
124 | default:
125 | url = `${V2EX_BASE_URL}/topics/show.json?node_name=${contentType}`
126 | break
127 | }
128 | return axios.get(url).then(res => res.data)
129 | }
130 |
131 | function * loadV2exTopics (contentType) {
132 | try {
133 | let topics = yield call(fetchV2exTopics, contentType)
134 | yield put({
135 | type: 'FETCH_V2EX_TOPICS_SUCCESS',
136 | payload: { topics, contentType }
137 | })
138 | } catch (error) {
139 | yield put({ type: 'FETCH_FAILED', error })
140 | }
141 | }
142 |
143 | const fetchV2exHot = () => {
144 | let url = `${V2EX_BASE_URL}/topics/hot.json`
145 | return axios.get(url).then(res => res.data)
146 | }
147 |
148 | function * loadV2exHot () {
149 | try {
150 | let topics = yield call(fetchV2exHot)
151 | yield put({
152 | type: 'FETCH_V2EX_HOT_SUCCESS',
153 | payload: { topics }
154 | })
155 | } catch (error) {
156 | yield put({ type: 'FETCH_FAILED', error })
157 | }
158 | }
159 |
160 | /**
161 | * Older Version Watchers : cannot prevent fetch waste
162 | */
163 | export function * watchLoadV2exTopic () {
164 | const v2exTopicTask = yield takeLatest('LOAD_V2EX_TOPIC', loadV2exTopic)
165 | }
166 |
167 | export function * watchLoadV2exTopics () {
168 | const v2exTopicsTask = yield takeLatest('LOAD_V2EX_TOPICS', loadV2exTopics)
169 | }
170 |
171 | export function * watchLoadV2exHot () {
172 | const v2exHotTask = yield takeLatest('LOAD_V2EX_HOT', loadV2exHot)
173 | }
174 |
175 | export function * watchLoadGitHub () {
176 | const githubTask = yield takeLatest('LOAD_GITHUB', loadGithub)
177 | }
178 |
179 | /**
180 | * Newest Version Watchers: Effectively eliminate waste of fetch
181 | */
182 | export function * watchLoadHackerNews () {
183 | const hackerNewsTask = yield takeLatest(
184 | 'LOAD_HACKERNEWS',
185 | loadHackerNewsData
186 | )
187 | }
188 |
189 | export function * watchV2exTopic () {
190 | let action
191 | while ((action = yield take('LOAD_V2EX_TOPIC'))) {
192 | const v2exTopicTask = yield fork(loadV2exTopic, action.id)
193 | yield take('STOP_FETCH')
194 | yield cancel(v2exTopicTask)
195 | }
196 | }
197 |
198 | export function * watchV2exTopics () {
199 | let action // we need the 'contentType' from action as argument passed to worker.
200 | while ((action = yield take('LOAD_V2EX_TOPICS'))) {
201 | const v2exTopicsTask = yield fork(loadV2exTopics, action.contentType)
202 | yield take('STOP_FETCH')
203 | yield cancel(v2exTopicsTask)
204 | }
205 | }
206 |
207 | export function * watchV2exHot () {
208 | while (yield take('LOAD_V2EX_HOT')) {
209 | const v2exHotTask = yield fork(loadV2exHot)
210 | yield take('STOP_FETCH')
211 | yield cancel(v2exHotTask)
212 | }
213 | }
214 |
215 | export function * watchHackerNews () {
216 | while (yield take('LOAD_HACKERNEWS')) {
217 | const hackNewsTask = yield fork(loadHackerNewsData)
218 | yield take('STOP_FETCH')
219 | yield cancel(hackNewsTask)
220 | }
221 | }
222 |
223 | export function * watchGithub () {
224 | while (yield take('LOAD_GITHUB')) {
225 | const githubTask = yield fork(loadGithub)
226 | yield take('STOP_FETCH')
227 | yield cancel(githubTask)
228 | }
229 | }
230 |
--------------------------------------------------------------------------------