├── 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 | 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 | <a href={url}>{user}/{name}</a> 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 |
      35 | 36 | 37 | 38 |
    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 |
    33 | {node.title} 34 | • 35 | 36 | {member.username} 37 | 38 | • 39 | {date(created)} 40 |
    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 | 60 | {Object.keys(savedFiles).length 61 | ? 67 | : You haven't saved any file yet :)} 68 | 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 |
    41 | 46 | {title} 47 | 48 |
    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 | 66 | 72 | 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 |
    37 | 38 | 39 | KYLEWH 40 | 41 |
    42 |
    43 | 44 | 11 45 | 49 |
    50 | 51 | 26 52 | 56 | 64 |
    65 |
    66 | 70 | 创作新主题 71 |
    72 |
    73 | 74 |
    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 |
    75 | 76 | {node.title} 77 | 78 | • 79 | 80 | {member.username} 81 | 82 | • 83 | {date(created)} 84 |
    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 | ![](http://om8hmotom.bkt.clouddn.com/2017-06-07-project3.gif) 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 | ![](http://om8hmotom.bkt.clouddn.com/2017-06-07-project4.gif) 15 | 2. **NewsFeed:** 集成v2ex,HackerNews-Top Stories, Github-Trending 16 | ![](http://om8hmotom.bkt.clouddn.com/2017-06-07-project5.gif) 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 | --------------------------------------------------------------------------------