├── NO_README.md ├── app ├── actions │ ├── actions.js │ └── index.js ├── client.js ├── components │ ├── About │ │ ├── About.css │ │ └── About.js │ ├── AddComment │ │ ├── AddComment.css │ │ └── AddComment.js │ ├── AddTags │ │ ├── AddTags.css │ │ └── AddTags.js │ ├── Archive │ │ ├── Archive.css │ │ └── Archive.js │ ├── Body │ │ ├── Body.css │ │ └── Body.js │ ├── Comments │ │ ├── Comments.css │ │ └── Comments.js │ ├── Edit │ │ └── Edit.js │ ├── Footer │ │ ├── Footer.css │ │ └── Footer.js │ ├── Header │ │ ├── Header.css │ │ └── Header.js │ ├── Home │ │ ├── Home.css │ │ └── Home.js │ ├── InputArea │ │ ├── InputArea.css │ │ └── InputArea.js │ ├── NotFound │ │ ├── NotFound.css │ │ └── NotFound.js │ ├── SearchResult │ │ └── SearchResult.js │ ├── Sidebar │ │ ├── Sidebar.css │ │ └── Sidebar.js │ ├── Single │ │ ├── Single.css │ │ └── Single.js │ ├── TagsResult │ │ ├── TagsResult.css │ │ └── TagsResult.js │ └── Write │ │ └── Write.js ├── constants │ └── actionTypes.js ├── containers │ ├── App.css │ └── App.js ├── helper │ └── auth.js ├── reducers │ ├── auth.js │ ├── check.js │ ├── comment.js │ ├── edit.js │ ├── getTitles.js │ ├── index.js │ ├── remove.js │ ├── single.js │ ├── tags.js │ └── upload.js ├── routes.js ├── store │ └── configureStore.js └── utils.js ├── data └── user.js ├── dist ├── bundle.js └── vendor.bundle.js ├── images ├── screen1.png └── screen2.png ├── index.html ├── model ├── comment.js ├── db.js └── post.js ├── package.json ├── routes └── index.js ├── server.js ├── settings.js ├── webpack.config.dev.js └── webpack.config.js /NO_README.md: -------------------------------------------------------------------------------- 1 | # React-Express-SPA-blog 2 | 3 | 这是一个简单的单页个人博客应用.主要的功能有展示文章列表, 通过标签寻找文章, 通过搜索查找文章, 评论功能, 文章写作, 编辑, 删除功能. 4 | 5 | 搭建地址为: [harryfyodor.tk] 6 | [harryfyodor.tk]: https://harryfyodor.tk 7 | 8 | ### 技术栈如下: 9 | * React 10 | * Redux 11 | * React-Router 12 | * Express 13 | * Webpack 14 | * mongodb 15 | 16 | ### 图片如下: 17 | * home主页面如下 18 | ![picture1](/images/screen1.png) 19 | * 文章写作页面 20 | ![picture2](/images/screen2.png) 21 | 22 | ### 操作: 23 | 把代码下载下来之后, 在命令行里输入 24 | ``` 25 | npm install 26 | ``` 27 | 在本地的时候设置环境为development: 28 | ``` 29 | export NODE_ENV=development 30 | ``` 31 | 搭建的时候设置为production: 32 | ``` 33 | export NODE_ENV=production 34 | ``` 35 | 密码在data/user.js文件中可以设置. 36 | 37 | ---------- 38 | ### 主要参考: 39 | * [使用 Express + MongoDB 搭建多人博客] 40 | [使用 Express + MongoDB 搭建多人博客]: https://github.com/harryfyodor/N-blog 41 | * [react-redux-jwt-auth-example] 42 | [react-redux-jwt-auth-example]: https://github.com/joshgeller/react-redux-jwt-auth-example 43 | * [redux官网] 44 | [redux官网]: http://cn.redux.js.org/ 45 | -------------------------------------------------------------------------------- /app/actions/actions.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harryfyodor/React-Express-SPA-blog/f1068d2ee0d1bd978cd405b306ac217a994dba99/app/actions/actions.js -------------------------------------------------------------------------------- /app/actions/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import { 3 | LOGIN_USER_SUCCESS, 4 | LOGIN_USER_REQUEST, 5 | LOGIN_USER_FAILURE, 6 | LOGOUT_USER, 7 | CHECK_AUTHENTICATED_REQUEST, 8 | CHECK_AUTHENTICATED_MATCHED, 9 | TITLES_GET_REQUEST, 10 | TITLES_GET_SUCCESS, 11 | TITLES_GET_FAILURE, 12 | TAGS_REQUEST, 13 | TAGS_SUCCESS, 14 | TAGS_FAILURE, 15 | POST_ARTICLE_REQUEST, 16 | POST_ARTICLE_SUCCESS, 17 | POST_ARTICLE_FAILURE, 18 | SINGLE_REQUEST, 19 | SINGLE_SUCCESS, 20 | SINGLE_FAILURE, 21 | EDIT_ARTICLE_REQUEST, 22 | EDIT_ARTICLE_SUCCESS, 23 | EDIT_ARTICLE_FAILURE, 24 | DELETE_ARTICLE_REQUEST, 25 | DELETE_ARTICLE_SUCCESS, 26 | DELETE_ARTICLE_FAILURE, 27 | ADD_COMMENT_REQUEST, 28 | ADD_COMMENT_SUCCESS, 29 | ADD_COMMENT_FAILURE, 30 | HOME, 31 | ARCHIVE, 32 | TAGS, 33 | SEARCH 34 | } from '../constants/actionTypes.js' 35 | import { pushState } from 'redux-router' 36 | import { checkHttpStatus } from '../utils.js' 37 | import jwtDecode from 'jwt-decode' 38 | 39 | export const loginUserRequest = () => { 40 | return { 41 | type: LOGIN_USER_REQUEST 42 | } 43 | } 44 | 45 | export const loginUserSuccess = (token) => { 46 | localStorage.setItem('token', token) 47 | return { 48 | type: LOGIN_USER_SUCCESS, 49 | payload: { 50 | token: token 51 | } 52 | } 53 | } 54 | 55 | export const loginUserFailure = (error) => { 56 | localStorage.removeItem('token') 57 | return { 58 | type: LOGIN_USER_FAILURE, 59 | payload: { 60 | status: error.res.state, 61 | statusText: error.res.statusText 62 | } 63 | } 64 | } 65 | 66 | export const logout = () => { 67 | localStorage.removeItem('token') 68 | return { 69 | type: LOGOUT_USER 70 | } 71 | } 72 | 73 | export const logoutAndRedirect = () => { 74 | return (dispatch, state) => { 75 | dispatch(logout()) 76 | } 77 | } 78 | 79 | export const loginUser = (password) => { 80 | return dispatch => { 81 | dispatch(loginUserRequest()) 82 | // the return here 83 | return fetch('/api/login', { 84 | method: 'POST', 85 | headers: { 86 | 'Accept': 'application/json', 87 | 'Content-Type': 'application/json' 88 | }, 89 | body: JSON.stringify({ password: password }) 90 | }) 91 | .then(checkHttpStatus) 92 | .then(res => res.json()) 93 | .then(res => { 94 | try { 95 | // throw error if it is invalid 96 | let decoded = jwtDecode(res.token) 97 | dispatch(loginUserSuccess(res.token)) 98 | // dispatch(pushState(null, '/')) 99 | } catch (e) { 100 | // get the token which is invalid 101 | dispatch(loginUserFailure({ 102 | res: { 103 | status: 403, 104 | statusText: 'Invalid token' 105 | } 106 | })) 107 | } 108 | }) 109 | .catch(error => { 110 | // fail to get the jwt token 111 | dispatch(loginUserFailure(error)) 112 | }) 113 | } 114 | } 115 | 116 | export const checkAuthenticatedRequest = () => { 117 | return { 118 | type: CHECK_AUTHENTICATED_REQUEST 119 | } 120 | } 121 | 122 | export const checkAuthenticatedMatched = (matched) => { 123 | return { 124 | type: CHECK_AUTHENTICATED_MATCHED, 125 | isAuthenticated: matched 126 | } 127 | } 128 | 129 | export const checkAuth = () => { 130 | return dispatch => { 131 | dispatch(checkAuthenticatedRequest()) 132 | return fetch('/api/check', { 133 | method: "POST", 134 | headers : { 135 | "Content-Type": "application/json", 136 | "Accept": "application/json" 137 | }, 138 | body: JSON.stringify({ token: localStorage.token }) 139 | }) 140 | .then(checkHttpStatus) 141 | .then(res => res.json()) 142 | .then(res => { 143 | if(res.match) { 144 | dispatch(checkAuthenticatedMatched(true)) 145 | } else { 146 | dispatch(checkAuthenticatedMatched(false)) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | export const titlesGetRequest = () => { 153 | return { 154 | type: TITLES_GET_REQUEST 155 | } 156 | } 157 | 158 | export const titlesGetFailure = (err) => { 159 | return { 160 | type: TITLES_GET_FAILURE, 161 | err: err 162 | } 163 | } 164 | 165 | // Archive has title,id,date 166 | // Home(need page) has id,title,tags,description,date 167 | // Tags(need tagName) has id,title 168 | // Search(need searchString) has id,title 169 | export const titlesGetSuccess = (articles, count) => { 170 | return { 171 | type: TITLES_GET_SUCCESS, 172 | articles: articles, 173 | count: count 174 | } 175 | } 176 | 177 | export const getTitles = ({ 178 | type: type, 179 | page: page, 180 | tagName: tagName, 181 | searchString: searchString 182 | }) => { 183 | return dispatch => { 184 | dispatch(titlesGetRequest()) 185 | return fetch('/api/titles', { 186 | method: "POST", 187 | headers: { 188 | "Content-Type": "application/json", 189 | "Accept": "application/json" 190 | }, 191 | body: JSON.stringify({ 192 | type: type, 193 | page: page, 194 | tagName: tagName, 195 | searchString: searchString 196 | }) 197 | }) 198 | .then(checkHttpStatus) 199 | .then(res => res.json()) 200 | .then(res => { 201 | if(res.ok) { 202 | dispatch(titlesGetSuccess(res.articles, res.count)) 203 | } else { 204 | dispatch(titlesGetFailure(res.err)) 205 | } 206 | }) 207 | } 208 | } 209 | 210 | export const tagsRequest = () => { 211 | return { 212 | type: TAGS_REQUEST 213 | } 214 | } 215 | 216 | export const tagsSuccess = (tags) => { 217 | return { 218 | type: TAGS_SUCCESS, 219 | tags: tags 220 | } 221 | } 222 | 223 | export const tagsFailure = (err) => { 224 | return { 225 | type: TAGS_FAILURE, 226 | err: err 227 | } 228 | } 229 | 230 | export const getTags = () => { 231 | //setTimeout(() => { 232 | return dispatch => { 233 | dispatch(tagsRequest()) 234 | return fetch('/api/tags', { 235 | method: "POST", 236 | header: { 237 | "dataType": "json" 238 | } 239 | }) 240 | .then(checkHttpStatus) 241 | .then(res => res.json()) 242 | .then(res => { 243 | if(res.ok) { 244 | dispatch(tagsSuccess(res.tags)) 245 | } else { 246 | dispatch(tagsFailure(res.err)) 247 | } 248 | }) 249 | } 250 | // }, 1000) 251 | } 252 | 253 | export const postArticleRequest = () => { 254 | return { 255 | type: POST_ARTICLE_REQUEST 256 | } 257 | } 258 | 259 | export const postArticleSuccess = () => { 260 | return { 261 | type: POST_ARTICLE_SUCCESS 262 | } 263 | } 264 | 265 | export const postArticleFailure = (err) => { 266 | return { 267 | type: POST_ARTICLE_FAILURE, 268 | err: err 269 | } 270 | } 271 | 272 | export const postArticle = (type, article) => { 273 | const token = localStorage.token 274 | return dispatch => { 275 | dispatch(postArticleRequest()) 276 | return fetch('/api/upload', { 277 | method: "POST", 278 | headers: { 279 | "Content-Type": "application/json", 280 | "Accept": "application/json", 281 | "Authorization": `Bearer ${token}` 282 | }, 283 | body: JSON.stringify({type: type, article: article}) 284 | }) 285 | .then(checkHttpStatus) 286 | .then(res => res.json()) 287 | .then(res => { 288 | if(res.ok) { 289 | dispatch(postArticleSuccess()) 290 | } else { 291 | dispatch(postArticleFailure(res.err)) 292 | } 293 | }) 294 | } 295 | } 296 | 297 | export const singleRequest = () => { 298 | return { 299 | type: SINGLE_REQUEST 300 | } 301 | } 302 | 303 | export const singleSuccess = (article) => { 304 | return { 305 | type: SINGLE_SUCCESS, 306 | article: article 307 | } 308 | } 309 | 310 | export const singleFailure = () => { 311 | return { 312 | type: SINGLE_FAILURE 313 | } 314 | } 315 | 316 | export const getSingle = (day, title) => { 317 | return dispatch => { 318 | dispatch(singleRequest()) 319 | return fetch('/api/single', { 320 | method: "POST", 321 | headers: { 322 | "Content-Type": "application/json", 323 | "Accept": "application/json" 324 | }, 325 | body: JSON.stringify({ 326 | day: day, 327 | title: title 328 | }) 329 | }) 330 | .then(checkHttpStatus) 331 | .then(res => res.json()) 332 | .then(res => { 333 | if(res.ok) { 334 | dispatch(singleSuccess(res.article)) 335 | } else { 336 | dispatch(singleFailure()) 337 | } 338 | }) 339 | } 340 | } 341 | 342 | export const editArticleRequest = () => { 343 | return { 344 | type: EDIT_ARTICLE_REQUEST 345 | } 346 | } 347 | 348 | export const editArticleSuccess = () => { 349 | return { 350 | type: EDIT_ARTICLE_SUCCESS 351 | } 352 | } 353 | 354 | export const editArticleFailure = () => { 355 | return { 356 | type: EDIT_ARTICLE_FAILURE 357 | } 358 | } 359 | 360 | export const editArticle = (oldDay, oldTitle, article) => { 361 | var token = localStorage.token 362 | return dispatch => { 363 | dispatch(editArticleRequest()) 364 | return fetch('/api/edit', { 365 | method: "POST", 366 | headers: { 367 | "Content-Type": "application/json", 368 | "Accept": "application/json", 369 | "Authorization": `Bearer ${token}` 370 | }, 371 | body: JSON.stringify({ 372 | oldDay: oldDay, 373 | oldTitle: oldTitle, 374 | article: article 375 | }) 376 | }) 377 | .then(checkHttpStatus) 378 | .then(res => res.json()) 379 | .then(res => { 380 | if(res.ok) { 381 | dispatch(editArticleSuccess()) 382 | } else { 383 | dispatch(editArticleFailure()) 384 | } 385 | }) 386 | } 387 | } 388 | 389 | export const deleteArticleRequest = () => { 390 | return { 391 | type: DELETE_ARTICLE_REQUEST 392 | } 393 | } 394 | 395 | export const deleteArticleSuccess = () => { 396 | return { 397 | type: DELETE_ARTICLE_SUCCESS 398 | } 399 | } 400 | 401 | export const deleteArticleFailure = () => { 402 | return { 403 | type: DELETE_ARTICLE_FAILURE 404 | } 405 | } 406 | 407 | export const deleteArticle = (day, title) => { 408 | var token = localStorage.token 409 | return dispatch => { 410 | dispatch(deleteArticleRequest()) 411 | return fetch('/api/remove', { 412 | method: "POST", 413 | headers: { 414 | "Content-Type": "application/json", 415 | "Accept": "application/json", 416 | "Authorization": `Bearer ${token}` 417 | }, 418 | body: JSON.stringify({ 419 | day: day, 420 | title: title 421 | }) 422 | }) 423 | .then(checkHttpStatus) 424 | .then(res => res.json()) 425 | .then(res => { 426 | if(res.ok) { 427 | dispatch(deleteArticleSuccess()) 428 | } else { 429 | dispatch(deleteArticleFailure()) 430 | } 431 | }) 432 | } 433 | } 434 | 435 | export const addCommentRequest = () => { 436 | return { 437 | type: ADD_COMMENT_REQUEST 438 | } 439 | } 440 | 441 | export const addCommentSuccess = () => { 442 | return { 443 | type: ADD_COMMENT_SUCCESS 444 | } 445 | } 446 | 447 | export const addCommentFailure = (err) => { 448 | return { 449 | type: ADD_COMMENT_FAILURE 450 | } 451 | } 452 | 453 | export const addComment = (articleTitle, articleDay, comment) => { 454 | return dispatch => { 455 | dispatch(addCommentRequest()) 456 | return fetch('/api/comment', { 457 | method: "POST", 458 | headers: { 459 | "Content-Type": "application/json", 460 | "Accept": "application/json" 461 | }, 462 | body: JSON.stringify({ 463 | articleDay: articleDay, 464 | articleTitle: articleTitle, 465 | comment: comment 466 | }) 467 | }) 468 | .then(checkHttpStatus) 469 | .then(res => res.json()) 470 | .then(res => { 471 | if(res.ok) { 472 | dispatch(addCommentSuccess()) 473 | } else { 474 | dispatch(addCommentFailure()) 475 | } 476 | }) 477 | } 478 | } -------------------------------------------------------------------------------- /app/client.js: -------------------------------------------------------------------------------- 1 | // import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | 5 | // import router 6 | import router from './routes.js' 7 | 8 | render ( 9 | router, 10 | document.getElementById('root') 11 | ) -------------------------------------------------------------------------------- /app/components/About/About.css: -------------------------------------------------------------------------------- 1 | .about { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | background-color: rgba(172,194,204,0.6); 6 | text-align: left; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | .about div { 13 | width: 50%; 14 | background-color: rgba(172,194,204,0.9); 15 | padding: 0.5rem 0.5rem 0.5rem 0.5rem; 16 | box-shadow: 0 0 5px #009a94; 17 | border-radius: 3px; 18 | } 19 | 20 | .about div>h2 { 21 | text-align: center; 22 | text-decoration: line-through; 23 | } 24 | 25 | .about button { 26 | text-decoration: none; 27 | color: #344653; 28 | line-height: 1.5; 29 | width: 4rem; 30 | height: 1.4rem; 31 | display: block; 32 | line-height: 1.4rem; 33 | margin: 0.5rem auto 34 | } 35 | 36 | 37 | .about button:hover { 38 | background-color: #009a94; 39 | border: 1px solid white; 40 | color: white; 41 | } -------------------------------------------------------------------------------- /app/components/About/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import style from './About.css' 4 | 5 | class About extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.displayName = 'About' 9 | } 10 | 11 | onQuit() { 12 | document.getElementsByTagName("body")[0].style.overflow = "" 13 | this.props.callback(); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 |
20 |

harryfyodor

21 |

weibo: harryfyodor

22 |

github: harryfyodor

23 |

email: wuhaiweideyouxiang@163.com

24 |

This is a SPA blog built by express and react. 25 | For more detail, view the code here. 26 |
(Do not forget to star if you like it~ )

27 | 28 |
29 |
30 | ) 31 | } 32 | } 33 | 34 | About.propTypes = { 35 | callback: React.PropTypes.func.isRequired 36 | } 37 | 38 | export default About 39 | -------------------------------------------------------------------------------- /app/components/AddComment/AddComment.css: -------------------------------------------------------------------------------- 1 | .addComment { 2 | display: flex; 3 | flex-direction: column; 4 | margin-top: 1rem; 5 | } 6 | 7 | .addComment input { 8 | margin-bottom: 0.5rem; 9 | height: 1.4rem; 10 | padding: 0.2rem; 11 | } 12 | 13 | .addComment textarea { 14 | height: 5rem; 15 | padding: 0.2rem; 16 | } 17 | 18 | .addComment button { 19 | width: 4.2rem; 20 | margin-top: 0.5rem; 21 | } 22 | 23 | .addComment button:a hover { 24 | background-color: #009a94; 25 | color: white; 26 | } -------------------------------------------------------------------------------- /app/components/AddComment/AddComment.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from "react-router" 3 | 4 | import style from './AddComment.css' 5 | 6 | class AddComment extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.displayName = 'AddComment' 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | // when the comment posted, refresh comment 14 | if(nextProps.posted) { 15 | const { title, day } = this.props.params 16 | this.props.actions.getSingle(title, day) 17 | } 18 | } 19 | 20 | onPostComment(e) { 21 | if(window.confirm("Are you sure to post this comment?")) { 22 | const { title, day } = this.props.params 23 | const comment = { 24 | name: this.refs.name.value, 25 | email: this.refs.email.value, 26 | website: this.refs.website.value, 27 | comment: this.refs.comment.value, 28 | admin: !!this.props.allowed 29 | } 30 | if(comment.name != "" && 31 | this.onEamil(comment.email) && 32 | comment.website != "" && 33 | comment.comment != "") { 34 | this.refs.name.value = "" 35 | this.refs.email.value = "" 36 | this.refs.website.value = "" 37 | this.refs.comment.value = "" 38 | // pass new comments to parent component 39 | this.props.postComment(title, day, comment) 40 | comment.minute = "just now" 41 | this.props.callback([ ...this.props.comments, comment]) 42 | } else { 43 | alert("Do not leave blank! Email must be in correct form.") 44 | } 45 | } 46 | } 47 | 48 | onEamil(email) { 49 | if(/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/.test(email)) { 50 | return true 51 | } 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 | 59 | 62 | 64 | 66 | 69 |
70 | ) 71 | } 72 | } 73 | 74 | AddComment.propTypes = { 75 | postComment: React.PropTypes.func.isRequired, 76 | comments: React.PropTypes.array.isRequired, 77 | allowed: React.PropTypes.bool.isRequired, 78 | callback: React.PropTypes.func.isRequired, 79 | params: React.PropTypes.object.isRequired, 80 | posted: React.PropTypes.bool.isRequired, 81 | getSingle: React.PropTypes.func.isRequired 82 | } 83 | 84 | export default AddComment 85 | -------------------------------------------------------------------------------- /app/components/AddTags/AddTags.css: -------------------------------------------------------------------------------- 1 | /* 2 | .addTagsPop { 3 | position: absolute; 4 | width: 100%; 5 | height: 100%; 6 | z-index: 10000; 7 | background-color: rgba(172,194,204,0.6); 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .addTagsPop>div { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | width: 20rem; 18 | background-color: red; 19 | margin: 5rem auto; 20 | padding: 1rem; 21 | background-color: rgba(172,194,204,0.9); 22 | line-height: 1.5; 23 | box-shadow: 0 0 2px black; 24 | } 25 | 26 | .addTagsPop>div input { 27 | width: 10rem; 28 | } 29 | 30 | .addTagsPop>div li { 31 | margin: 0.5rem 0.5rem 0.5rem 0rem; 32 | color: #344653; 33 | border: 1px solid black; 34 | line-height: 1.4; 35 | padding: 0 0.2rem; 36 | } 37 | 38 | .addTagsPop>div li:hover { 39 | cursor: default; 40 | box-shadow: 0 0 2px #344653; 41 | } 42 | 43 | .addTagsPop>div div { 44 | display: flex; 45 | justify-content: center; 46 | } 47 | 48 | .addTagsPop>div div button { 49 | margin: 0 0.5rem; 50 | } 51 | 52 | .isAdded li:hover { 53 | background-color: red; 54 | color: white; 55 | } 56 | */ 57 | .addTagsPop { 58 | position: absolute; 59 | width: 100%; 60 | height: 100%; 61 | z-index: 10000; 62 | background-color: rgba(238,238,238,0.6); 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | } 67 | 68 | .addTagsPop>div { 69 | display: flex; 70 | flex-direction: column; 71 | justify-content: center; 72 | width: 20rem; 73 | background-color: red; 74 | margin: 5rem auto; 75 | padding: 1rem; 76 | background-color: rgba(238,238,238,0.9); 77 | line-height: 1.5; 78 | box-shadow: 0 0 2px black; 79 | } 80 | 81 | .addTagsPop>div input { 82 | width: 10rem; 83 | border: 1px solid #009a94; 84 | padding-left: 0.5rem; 85 | } 86 | 87 | .addTagsPop>div li { 88 | margin: 0.5rem 0.5rem 0.5rem 0rem; 89 | color: #344653; 90 | border: 1px solid #009a94; 91 | line-height: 1.4; 92 | padding: 0 0.2rem; 93 | } 94 | 95 | .addTagsPop>div li:hover { 96 | cursor: default; 97 | box-shadow: 0 0 2px #009a94; 98 | text-decoration: line-through; 99 | } 100 | 101 | .addTagsPop>div div { 102 | display: flex; 103 | justify-content: center; 104 | } 105 | 106 | .addTagsPop>div div button { 107 | margin: 0 0.5rem; 108 | } 109 | 110 | .isAdded li:hover { 111 | background-color: red; 112 | color: white; 113 | } -------------------------------------------------------------------------------- /app/components/AddTags/AddTags.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import style from './AddTags.css' 4 | 5 | const notInArray = (array, item) => { 6 | for(let i = 0; i < array.length; i++) 7 | if(item === array[i]) return false 8 | return true 9 | } 10 | 11 | const findIndex = (array, item) => { 12 | for(let i = 0; i < array.length; i++) 13 | if(item === array[i]) return i 14 | return -1 15 | } 16 | 17 | class AddTags extends React.Component { 18 | constructor(props) { 19 | super(props) 20 | this.displayName = 'AddTags' 21 | this.state = { 22 | left : "0px", 23 | top : "0px", 24 | display : "none", // flex 25 | overflow: "hidden", 26 | marginTop: "0px", 27 | tags: this.props.currentTags || [], 28 | text: "", 29 | allTags: this.props.allTags || [] 30 | } 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | // important!!! 35 | if(nextProps.popup) { 36 | this.setState({ 37 | display: "flex", 38 | tags: nextProps.currentTags, 39 | allTags: nextProps.allTags, 40 | marginTop: document.documentElement.offsetTop || document.body.scrollTop + "px" 41 | }) 42 | document.getElementsByTagName("body")[0].style.overflow = "hidden" 43 | } 44 | } 45 | 46 | onQuitClick() { 47 | this.setState({ 48 | tags: [], 49 | display: "none", 50 | text: "" 51 | }) 52 | document.getElementsByTagName("body")[0].style.overflow = "" 53 | } 54 | 55 | onFinishClick() { 56 | document.getElementsByTagName("body")[0].style.overflow = "" 57 | this.setState({ 58 | display: "none" 59 | }) 60 | this.props.callbackParent([ ...this.state.tags ]) 61 | } 62 | 63 | onSubmitHandler(e) { 64 | // press Enter 65 | if(e.which === 13) { 66 | e.preventDefault() 67 | let tags = [ ...this.state.tags] 68 | const text = this.state.text 69 | if (tags.length === 5) { 70 | alert("At most 5 tags!") 71 | return 72 | } else if (text.trim() != "" && 73 | notInArray(tags, text) && 74 | text.trim().length <= 8) { 75 | tags.push(text) 76 | this.setState({ 77 | tags: tags, 78 | text: "" 79 | }) 80 | } else { 81 | alert('Not allow to repeat, leave blank and be too long!'); 82 | } 83 | } 84 | } 85 | 86 | onChangeHandler(e) { 87 | this.setState({ text: e.target.value }) 88 | } 89 | 90 | // delete tags 91 | onDeleteHandler(e) { 92 | // very important!!! 93 | let tags = [ ...this.state.tags] 94 | const item = e.target.innerHTML 95 | const index = findIndex(tags, item) 96 | tags.splice(index, 1) 97 | this.setState({ 98 | tags: tags 99 | }) 100 | } 101 | 102 | // copy the old tags 103 | onRepeat(e) { 104 | const item = e.target.innerHTML 105 | // very important!!! 106 | let tags = [ ...this.state.tags] 107 | if (notInArray(tags, item) && tags.length < 5) { 108 | tags.push(item) 109 | this.setState({ 110 | tags: tags 111 | }) 112 | } 113 | } 114 | 115 | render() { 116 | const inStyle = { 117 | left: this.state.left, 118 | top: this.state.top, 119 | display: this.state.display, 120 | overflow: this.state.overflow, 121 | marginTop: this.state.marginTop 122 | } 123 | let tags = this.state.tags 124 | let allTags = this.state.allTags 125 | return ( 126 |
127 |
128 | 129 | 134 | 135 | 138 | 139 | 144 |
145 | 146 | 147 |
148 |
149 |
150 | ) 151 | } 152 | } 153 | 154 | AddTags.propTypes = { 155 | popup: React.PropTypes.bool.isRequired, 156 | currentTags: React.PropTypes.array.isRequired, 157 | callbackParent: React.PropTypes.func.isRequired, 158 | allTags: React.PropTypes.array.isRequired 159 | } 160 | 161 | export default AddTags -------------------------------------------------------------------------------- /app/components/Archive/Archive.css: -------------------------------------------------------------------------------- 1 | /* 2 | .archive { 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .archive>div { 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .archive>div li { 12 | display: block; 13 | } 14 | 15 | .archive>div a { 16 | color: #344653; 17 | text-decoration: none; 18 | line-height: 1; 19 | } 20 | 21 | .archive>div a:hover { 22 | text-decoration: underline; 23 | color: black; 24 | } 25 | */ 26 | 27 | .archive>div li{ 28 | display: block; 29 | } 30 | 31 | .archive>div li:hover { 32 | text-decoration: underline; 33 | } -------------------------------------------------------------------------------- /app/components/Archive/Archive.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | import style from './Archive.css'; 5 | 6 | class Archive extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.displayName = 'Archive'; 10 | this.state = { 11 | articles: [] 12 | } 13 | } 14 | 15 | componentDidMount() { 16 | this.props.actions.getTitles({ 17 | type: "ARCHIVE" 18 | }) 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | this.setState({ 23 | articles: nextProps.getTitles.articles 24 | }) 25 | } 26 | 27 | renderMonth() { 28 | let articles = [] 29 | let group = [] 30 | let index = 0 31 | // toArray 32 | if (this.state.articles.length !== 0) { 33 | let lastMonth = this.state.articles[0].time.month 34 | this.state.articles.forEach(function(article, i) { 35 | if(lastMonth == article.time.month) { 36 | group.push(article) 37 | } else { 38 | articles[index] = group 39 | lastMonth = article.time.month 40 | group = [] 41 | group.push(article) 42 | index++ 43 | } 44 | if(i === articles.length) { 45 | articles[index] = group 46 | } 47 | }) 48 | return articles.map((as, i) => 49 |
50 |

{as[0].time.month}

51 | 56 |
57 | ) 58 | } 59 | 60 | return "NO ARCHIVE" 61 | } 62 | 63 | render() { 64 | return ( 65 |
66 | {this.renderMonth()} 67 |
68 | ) 69 | } 70 | } 71 | 72 | export default Archive 73 | -------------------------------------------------------------------------------- /app/components/Body/Body.css: -------------------------------------------------------------------------------- 1 | /* 2 | .main { 3 | display: flex; 4 | align-items: flex-start; 5 | width: 960px; 6 | margin: 6rem auto 5rem auto; 7 | padding: 1.5rem 1rem 1.5rem 2rem; 8 | background-color: rgba(172,194,204,0.5); 9 | box-shadow: 0 0 2px black; 10 | align-items: stretch; 11 | justify-content: space-around; 12 | } 13 | 14 | .content { 15 | flex-grow: 2; 16 | flex-shrink: 1; 17 | } 18 | */ 19 | 20 | .main { 21 | display: flex; 22 | align-items: flex-start; 23 | align-items: stretch; 24 | justify-content: space-around; 25 | margin: 0 auto; 26 | width: 60%; 27 | flex: 1; 28 | } 29 | 30 | .content { 31 | width: 40rem; 32 | padding-right: 1rem; 33 | } -------------------------------------------------------------------------------- /app/components/Body/Body.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Sidebar from '../Sidebar/Sidebar.js' 4 | 5 | import style from './Body.css'; 6 | 7 | class Body extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | this.displayName = 'Body' 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 |
17 | {React.cloneElement(this.props.children, this.props)} 18 |
19 | 27 |
28 | ) 29 | } 30 | } 31 | 32 | export default Body 33 | -------------------------------------------------------------------------------- /app/components/Comments/Comments.css: -------------------------------------------------------------------------------- 1 | .comments ul { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .comments li{ 7 | margin-top: 1rem; 8 | margin-bottom: 0; 9 | border-bottom: 1px solid #009a94; 10 | } 11 | 12 | .comments p { 13 | margin-top: 0.3rem; 14 | } 15 | 16 | .comments h3 { 17 | margin-top: 1rem; 18 | } 19 | 20 | .comments h4 { 21 | display: inline; 22 | font-weight: 100; 23 | margin-right: 1rem; 24 | } -------------------------------------------------------------------------------- /app/components/Comments/Comments.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import style from './Comments.css' 4 | 5 | class Comment extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.displayName = 'Comment' 9 | this.state = { 10 | comments: this.props.comments || [] 11 | } 12 | } 13 | 14 | componentWillReceiveProps(nextProps) { 15 | this.setState({ 16 | comments: nextProps.comments || [] 17 | }) 18 | } 19 | 20 | renderAComment() { 21 | const linethrough = { 22 | textDecoration : "line-through", 23 | fontSize: "0 0 3px black" 24 | } 25 | if(!this.state.comments) return
  • Loading...
  • 26 | if(this.state.comments.length === 0) return
  • No Comments
  • 27 | return this.state.comments.map((c, i) => 28 |
  • 29 | {c.admin ? 30 |

    {c.name}

    33 | :

    {c.name}

    } 34 | 35 |

    {c.comment}

    36 |
  • 37 | ) 38 | } 39 | 40 | render() { 41 | return ( 42 |
    43 |

    Comments:

    44 | 47 |
    48 | ) 49 | } 50 | } 51 | 52 | Comment.propTypes = { 53 | comments: React.PropTypes.array.isRequired 54 | } 55 | 56 | export default Comment 57 | -------------------------------------------------------------------------------- /app/components/Edit/Edit.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import InputArea from '../InputArea/InputArea.js' 4 | 5 | class Edit extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.displayName = 'Edit' 9 | } 10 | render() { 11 | if(this.props.single.article.time) 12 | return ( 13 |
    14 | 22 |
    23 | ) 24 | return ( 25 |
    26 |

    Error!

    27 |

    Please fetch the aritcle before edit it.

    28 |
    29 | ) 30 | } 31 | } 32 | 33 | export default Edit -------------------------------------------------------------------------------- /app/components/Footer/Footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex; 3 | justify-content: center; 4 | background-color: #009a94; 5 | flex-direction: column; 6 | height: 7rem; 7 | margin-top: 1rem; 8 | background: -webkit-linear-gradient(#eeeeee, #009a94); /* Safari 5.1 - 6.0 */ 9 | background: -o-linear-gradient(#eeeeee, #009a94); /* Opera 11.1 - 12.0 */ 10 | background: -moz-linear-gradient(#eeeeee, #009a94); /* Firefox 3.6 - 15 */ 11 | background: linear-gradient(#eeeeee, #009a94); /* 标准的语法 */ 12 | font-size: 0.8rem; 13 | text-align: center; 14 | } 15 | 16 | .footer p{ 17 | display: block; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import style from './Footer.css' 4 | 5 | class Footer extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.displayName = 'Footer'; 9 | } 10 | render() { 11 | return ( 12 |
    13 |

    © 2016 harryfyodor

    14 |

    Hosted on DigitalOcean

    15 |

    Powered by Express and 16 | React

    17 |
    18 | ); 19 | } 20 | } 21 | 22 | export default Footer; 23 | -------------------------------------------------------------------------------- /app/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | /* 2 | .heading { 3 | width: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: 5rem; 7 | position: fixed; 8 | z-index: 9999; 9 | } 10 | 11 | .header { 12 | background-color: #BDD1DB; 13 | display: inline-flex; 14 | width: 960px; 15 | height: 3rem; 16 | margin: 0 auto; 17 | justify-content: space-between; 18 | box-shadow: 0 0 2px black; 19 | } 20 | 21 | .header li { 22 | display: inline-block; 23 | font-size: 1rem; 24 | line-height: 3rem; 25 | color: #344653; 26 | text-decoration: none; 27 | } 28 | 29 | .header li a{ 30 | text-decoration: none; 31 | color: #344653; 32 | padding: 1rem 0.5rem 1rem 0.5rem; 33 | } 34 | 35 | .header li a:hover { 36 | background-color: #ABB7B7; 37 | cursor: pointer; 38 | } 39 | 40 | .header h1 { 41 | display: block; 42 | margin-left: 2rem; 43 | } 44 | 45 | .header h1 a { 46 | color: #344653; 47 | text-decoration: none; 48 | } 49 | 50 | .header h1:hover { 51 | cursor: pointer; 52 | } 53 | 54 | .login { 55 | display: flex; 56 | width: 960px; 57 | flex-direction: column; 58 | justify-content: space-between; 59 | align-items: flex-end; 60 | margin: 0 auto; 61 | } 62 | 63 | .login input { 64 | width: 10rem; 65 | } 66 | */ 67 | 68 | .heading { 69 | display: flex; 70 | flex-direction: column; 71 | content-justify: center; 72 | margin-bottom: 3rem; 73 | } 74 | 75 | .header h1 { 76 | width: 11rem; 77 | margin: 2rem auto; 78 | text-decoration: line-through; 79 | text-shadow: 0px 0px 1px black; 80 | border: none; 81 | } 82 | 83 | .header h1:hover { 84 | text-decoration: none; 85 | } 86 | 87 | .header ul { 88 | width: 50%; 89 | margin: 0 auto; 90 | display: flex; 91 | justify-content: space-between; 92 | } 93 | 94 | .header li, 95 | .header a { 96 | color: black; 97 | } 98 | 99 | .header h1:hover, 100 | .header li:hover, 101 | .header a:hover { 102 | cursor: pointer; 103 | color: #009A94; 104 | } 105 | 106 | .login { 107 | display: flex; 108 | width: 50%; 109 | flex-direction: column; 110 | justify-content: space-between; 111 | align-items: flex-end; 112 | margin: 0 auto; 113 | margin-bottom: -2.55rem; 114 | } 115 | 116 | .login input { 117 | background-color: #eeeeee; 118 | border: 1px solid #009A94; 119 | font-size: 0.8rem; 120 | height: 1.2rem; 121 | line-height: 1.2rem; 122 | padding-left: 0.2rem; 123 | width: 10rem; 124 | box-shadow: 0 0 2px gray; 125 | } 126 | 127 | .login input:focus { 128 | box-shadow: 0 0 1px black; 129 | border: 1px solid #eeeeee; 130 | } 131 | 132 | -------------------------------------------------------------------------------- /app/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | // import css 5 | import style from './Header.css' 6 | 7 | import About from '../About/About.js' 8 | 9 | class Header extends React.Component { 10 | constructor(props) { 11 | super(props) 12 | this.displayName = 'Header' 13 | this.state = { 14 | text: "", 15 | isShown: false, 16 | show: { 17 | display: "none" 18 | }, 19 | isAuthenticated: this.props.auth.isAuthenticated, 20 | shouldBeLogout: false, 21 | about: false 22 | } 23 | } 24 | 25 | // if there is token, check it before rendering 26 | componentDidMount() { 27 | if(!!localStorage.token) { 28 | this.props.checkAuth() 29 | } 30 | } 31 | 32 | componentWillReceiveProps(nextProps) { 33 | // when refresh or logout happen 34 | this.setState({ 35 | isAuthenticated: nextProps.check.isAuthenticated 36 | }) 37 | // login for the first time, change header btns immediately 38 | if(nextProps.auth.isAuthenticated) { 39 | this.setState({ 40 | isAuthenticated: true 41 | }) 42 | } 43 | } 44 | 45 | onLoginHandler(e) { 46 | e.preventDefault(); 47 | if(this.state.isShown) { 48 | this.setState({ 49 | isShown: false, 50 | show: { 51 | display: "none" 52 | } 53 | }) 54 | } else { 55 | this.setState({ 56 | isShown: true, 57 | show: { 58 | display: "flex" 59 | } 60 | }) 61 | } 62 | } 63 | 64 | onChangeHandler(e) { 65 | this.setState({ text: e.target.value }) 66 | } 67 | 68 | onPostHandler(e) { 69 | if(e.which === 13 && this.state.text != "") { 70 | const password = this.state.text 71 | this.props.login(password) 72 | this.setState({ 73 | text: "", 74 | isShown: false, 75 | show: { 76 | display: "none" 77 | } 78 | }) 79 | } 80 | } 81 | 82 | onLogoutHandler(e) { 83 | this.setState({ 84 | isAuthenticated: false, 85 | shouldBeLogout: true 86 | }) 87 | // important!!change the props!! 88 | this.props.checkAuthenticatedMatched(false) 89 | this.props.logoutAndRedirect() 90 | } 91 | 92 | onAbout() { 93 | document.getElementsByTagName("body")[0].style.overflow = "hidden" 94 | this.setState({ 95 | about: true 96 | }) 97 | } 98 | 99 | onAboutChild() { 100 | this.setState({ 101 | about: false 102 | }) 103 | } 104 | 105 | onBack() { 106 | if(this.props.location !== "/page/1") { 107 | /* 108 | this.props.actions.getTitles({ 109 | type: "HOME", 110 | page: 1 111 | }) 112 | */ 113 | } 114 | } 115 | 116 | render() { 117 | return ( 118 |
    119 | {this.state.about ? 120 | 121 | : ""} 122 |
    123 |

    harryfyodor

    124 |
      125 |
    • home
    • 126 |
    • archive
    • 127 | {this.state.isAuthenticated ? 128 |
    • write
    • : 129 |
    • about
    • 130 | } 131 | {this.state.isAuthenticated ? 132 |
    • logout
    • : 133 |
    • login
    • 134 | } 135 |
    136 |
    137 |
    138 | 139 | 144 |
    145 |
    146 | ) 147 | } 148 | } 149 | 150 | Header.propTypes = { 151 | check: React.PropTypes.object.isRequired, 152 | login: React.PropTypes.func.isRequired, 153 | logoutAndRedirect: React.PropTypes.func.isRequired, 154 | checkAuth: React.PropTypes.func.isRequired, 155 | checkAuthenticatedMatched: React.PropTypes.func.isRequired, 156 | location: React.PropTypes.string.isRequired, 157 | } 158 | 159 | export default Header 160 | -------------------------------------------------------------------------------- /app/components/Home/Home.css: -------------------------------------------------------------------------------- 1 | /* 2 | .home { 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .home>div { 8 | margin: 1rem 0rem 2rem 0rem; 9 | } 10 | 11 | .home li { 12 | display: inline-block; 13 | margin-right: 1rem; 14 | color: #3c4e5a; 15 | cursor: pointer; 16 | } 17 | 18 | .home button { 19 | margin-right: 2rem; 20 | } 21 | 22 | .home button a { 23 | text-decoration: none; 24 | color: #344653; 25 | line-height: 1.5; 26 | width: 4rem; 27 | height: 1.4rem; 28 | display: block; 29 | line-height: 1.4rem; 30 | }*/ 31 | 32 | .home>div { 33 | margin: 1rem 0rem 2rem 0rem; 34 | } 35 | 36 | .home>div time { 37 | font-size: 0.8rem; 38 | } 39 | 40 | .home>div li { 41 | margin-right: 1rem; 42 | color: #009A94; 43 | } 44 | 45 | .home button { 46 | margin-right: 2rem; 47 | } 48 | -------------------------------------------------------------------------------- /app/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import style from './Home.css' 5 | 6 | class Home extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.displayName = 'Home' 10 | this.state = { 11 | page : parseInt(this.props.params.pageId, 10) || 1, 12 | length : (this.props.getTitles.count/10 + 1) || 1, 13 | articles : [] 14 | } 15 | } 16 | 17 | componentDidMount() { 18 | this.props.actions.getTitles({ 19 | type: "HOME", 20 | page: this.state.page 21 | }) 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | this.setState({ 26 | length: nextProps.getTitles.count/10 + 1, 27 | articles: nextProps.getTitles.articles 28 | }) 29 | } 30 | 31 | onPreClick() { 32 | if(this.state.page >= 2) { 33 | // get posts of previous page 34 | this.actions.getTitles({ 35 | type: "HOME", 36 | page: this.state.page - 1 37 | }) 38 | this.setState({ 39 | page: this.state.page - 1 40 | }) 41 | } 42 | } 43 | 44 | onNextClick() { 45 | if(this.state.page <= this.state.length - 1) { 46 | // get posts of next page 47 | this.actions.getTitles({ 48 | type: "HOME", 49 | page: this.state.page + 1 50 | }) 51 | this.setState({ 52 | page: this.state.page + 1 53 | }) 54 | } 55 | } 56 | 57 | onRenderSingle() { 58 | // const articles = this.state.articles 59 | // to solve the problem when came from other titles 60 | if(this.state.articles === []) return "" 61 | if(this.state.articles[0] == undefined) return "" 62 | if(this.state.articles[0].tags == undefined) return "" 63 | return this.state.articles.map((a, i) => 64 |
    65 |

    {a.title}

    66 | 67 | 72 |

    {a.description}

    73 |
    74 | ) 75 | } 76 | 77 | render() { 78 | // get next and pre 79 | let next = (this.state.page <= this.state.length - 1) ? 80 | this.state.page + 1 : this.state.page 81 | let pre = (this.state.page >= 2) ? 82 | this.state.page - 1 : this.state.page 83 | return ( 84 |
    85 | {this.onRenderSingle()} 86 |
    87 | 89 | 91 |
    92 |
    93 | ) 94 | } 95 | } 96 | 97 | export default Home 98 | -------------------------------------------------------------------------------- /app/components/InputArea/InputArea.css: -------------------------------------------------------------------------------- 1 | /* 2 | .inputArea { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: flex-end; 6 | padding-right: 12%; 7 | } 8 | 9 | .inputArea label { 10 | margin-right: 0.5rem; 11 | } 12 | 13 | .inputArea input{ 14 | width: 28rem; 15 | height: 1.5rem; 16 | margin-bottom: 0.5rem; 17 | } 18 | 19 | .inputArea textarea { 20 | width: 28rem; 21 | height: 15rem; 22 | padding: 0.5rem; 23 | } 24 | 25 | .inputArea>div { 26 | display: flex; 27 | align-items: flex-start; 28 | } 29 | 30 | .inputArea button { 31 | align-self: flex-start; 32 | margin: 0.5rem 0rem 0.5rem 5.6rem; 33 | } 34 | 35 | .addTags { 36 | align-self: flex-start; 37 | display: flex; 38 | } 39 | 40 | .addTags button { 41 | margin-left: 0.5rem; 42 | } 43 | 44 | .addTags li { 45 | margin: 0.5rem 0.5rem 0.5rem 0rem; 46 | color: #344653; 47 | border: 1px solid black; 48 | line-height: 1.4; 49 | padding: 0 0.2rem; 50 | } 51 | 52 | .addTags li:hover { 53 | cursor: default; 54 | box-shadow: 0 0 2px #344653; 55 | } 56 | 57 | .addTags ul { 58 | align-self: flex-start; 59 | margin-left: 5.6rem; 60 | } 61 | 62 | .inputArea span { 63 | align-self: flex-start; 64 | margin-left: 5.6rem; 65 | } 66 | 67 | .md { 68 | margin-top: 0.5rem; 69 | } 70 | 71 | .md>div { 72 | width: 28rem; 73 | border: 1px solid black; 74 | height: 10rem; 75 | box-shadow: 0 0 2px #344653; 76 | overflow:auto; 77 | padding:0.5rem; 78 | font-size: 0.8rem; 79 | line-height: 1.5; 80 | } 81 | 82 | .md>div h1, h2, h3, h4, h5, h6 { 83 | line-height: 2; 84 | } 85 | 86 | .md>div code { 87 | font-size: 0.7rem; 88 | } 89 | 90 | .md>div li { 91 | margin: 0; 92 | border: none; 93 | display: block; 94 | padding-left: 0.5rem; 95 | } 96 | 97 | .md>div ul { 98 | margin: 0; 99 | } 100 | 101 | .md>div li:hover { 102 | box-shadow: 0 0 0; 103 | } 104 | 105 | .md>div a { 106 | font-size: 0.9rem; 107 | } 108 | 109 | .md>div a:hover { 110 | text-decoration: underline; 111 | 112 | color: black; 113 | } 114 | 115 | .md>div { 116 | 117 | }*/ 118 | 119 | .inputArea { 120 | display: flex; 121 | flex-direction: column; 122 | align-items: flex-end; 123 | padding-right: 1rem; 124 | } 125 | 126 | .inputArea label { 127 | margin-right: 0.5rem; 128 | } 129 | 130 | .inputArea input{ 131 | width: 25rem; 132 | height: 1.5rem; 133 | margin-bottom: 0.5rem; 134 | } 135 | 136 | .inputArea textarea { 137 | width: 25rem; 138 | height: 15rem; 139 | padding: 0.5rem; 140 | } 141 | 142 | .inputArea>div { 143 | display: flex; 144 | } 145 | 146 | .inputArea button { 147 | margin: 0.5rem 0rem 0.5rem 5.6rem; 148 | } 149 | 150 | .addTags { 151 | display: flex; 152 | } 153 | 154 | .addTags button { 155 | margin-left: 0.5rem; 156 | } 157 | 158 | .addTags li { 159 | margin: 0.5rem 0.5rem 0.5rem 0rem; 160 | color: #344653; 161 | border: 1px solid #009a94; 162 | line-height: 1.4; 163 | padding: 0 0.2rem; 164 | } 165 | 166 | .addTags li:hover { 167 | cursor: default; 168 | box-shadow: 0 0 2px #009a94; 169 | } 170 | 171 | .addTags ul { 172 | margin-left: 5.6rem; 173 | } 174 | 175 | .inputArea span { 176 | margin-left: 5.6rem; 177 | } 178 | 179 | .inputArea button { 180 | text-decoration: none; 181 | color: #344653; 182 | line-height: 1.5; 183 | width: 4rem; 184 | height: 1.4rem; 185 | display: block; 186 | line-height: 1.4rem; 187 | } 188 | 189 | .inputArea button:hover { 190 | background-color: #009A94; 191 | color: white; 192 | } 193 | 194 | /*markdown*/ 195 | .md { 196 | font-size: 0.8rem; 197 | margin-top: 1rem; 198 | } 199 | 200 | .md>div { 201 | width: 25rem; 202 | border: 1px solid black; 203 | height: 10rem; 204 | box-shadow: 0 0 2px #344653; 205 | overflow:auto; 206 | padding:0.5rem; 207 | font-size: 0.8rem; 208 | line-height: 1.5; 209 | } 210 | 211 | .md h1, 212 | .md h2, 213 | .md h3, 214 | .md h4, 215 | .md h5, 216 | .md h6 { 217 | margin-bottom: 0.5rem; 218 | margin-top: 0.5rem; 219 | } 220 | 221 | .md li { 222 | display: list-item; 223 | margin: 0; 224 | padding: 0; 225 | } 226 | 227 | .md p { 228 | line-height: 1rem; 229 | } 230 | 231 | .md ul { 232 | list-style: square; 233 | padding-left:1.2rem; 234 | } 235 | 236 | .md ol { 237 | padding-left:1.2rem; 238 | } 239 | 240 | .md pre { 241 | background-color: rgba(0,154,148,0.7); 242 | color: white; 243 | padding: 8px 10px; 244 | border-radius: 0.4em; 245 | -moz-border-radius: 0.4em; 246 | -webkit-border-radius: 0.4em; 247 | overflow-x: hidden; 248 | } 249 | 250 | .md code { 251 | background-color: #47b3af; 252 | color: white; 253 | } 254 | 255 | .md pre code { 256 | font-size: 10pt; 257 | font-family: "Consolas", "Menlo", "Monaco", monospace, serif; 258 | } 259 | 260 | .md blockquote { 261 | font-style: italic; 262 | margin: 1rem 1rem 1rem; 263 | padding-left: 1rem; 264 | border-left: 3px solid #47b3af; 265 | } 266 | 267 | .md a { 268 | font-size: 0.9rem; 269 | } 270 | 271 | .md a:hover { 272 | text-decoration: underline; 273 | } 274 | 275 | .md img { 276 | width: 100%; 277 | box-shadow: 0 0 10px #009a94; 278 | } 279 | -------------------------------------------------------------------------------- /app/components/InputArea/InputArea.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import md from 'marked' 3 | import { Link } from 'react-router' 4 | 5 | import style from './InputArea.css' 6 | 7 | import AddTags from '../AddTags/AddTags.js' 8 | 9 | class InputArea extends React.Component { 10 | constructor(props) { 11 | super(props) 12 | this.displayName = 'InputArea' 13 | this.state = { 14 | currentTags: this.props.type === "EDIT" ? this.props.oldArticle.tags : [], 15 | title: this.props.type === "EDIT" ? this.props.oldArticle.title : "", 16 | description: this.props.type === "EDIT" ? this.props.oldArticle.description : "", 17 | content: this.props.type === "EDIT" ? this.props.oldArticle.content : "", 18 | day: this.props.day, 19 | allowed: this.props.allowed, 20 | type: this.props.type, // new build or edit 21 | allTags: this.props.allTags, 22 | popup: false // popup when true 23 | } 24 | } 25 | 26 | onContentChange(e) { 27 | this.setState({ content: e.target.value }) 28 | } 29 | 30 | onAddTags() { 31 | this.setState({ 32 | popup: true 33 | }) 34 | } 35 | 36 | onChildChange(nextState) { 37 | this.setState({ 38 | currentTags: nextState, 39 | popup: false 40 | }) 41 | } 42 | 43 | componentDidMount() { 44 | if(this.state.allowed) { 45 | this.refs.title.value = this.state.title 46 | this.refs.content.value = this.state.content 47 | this.refs.description.value = this.state.description 48 | } 49 | } 50 | 51 | componentWillReceiveProps(nextProps) { 52 | this.setState({ 53 | allowed: nextProps.allowed, 54 | allTags: nextProps.allTags 55 | }) 56 | } 57 | 58 | onFinishHandler() { 59 | const newArticle = { 60 | title: this.refs.title.value.trim(), 61 | description: this.refs.description.value.trim(), 62 | content: this.refs.content.value.trim(), 63 | tags: this.state.currentTags 64 | } 65 | if(newArticle.title !== "" && 66 | newArticle.description !== "" && 67 | newArticle.content !== "" && 68 | newArticle.tags.length != 0) { 69 | // not empty 70 | if(this.state.type === "WRITE") { 71 | this.props.upload(this.state.type, newArticle) 72 | alert("SUCCESS!") 73 | this.props.history.pushState(null, '/') 74 | } else if (this.state.type === "EDIT") { 75 | this.props.editArticle(this.props.params.day, this.props.params.title, newArticle) 76 | alert("SUCCESS!") 77 | this.props.history.pushState(null, '/') 78 | } 79 | } else { 80 | // empty 81 | alert("FAIL TO POST! POSSIBLY THERE IS A BLANK!") 82 | } 83 | } 84 | 85 | onShowInput() { 86 | const tags = this.state.currentTags 87 | if(this.state.allowed) { 88 | return ( 89 |
    90 |

    91 | 92 | 93 |

    94 | 2016-7-14 95 |
    96 | 100 | 101 |
    102 | 106 |

    107 | 108 | 109 |

    110 |
    111 | 112 | 114 |
    115 |
    116 | 117 |
    118 |
    119 | 120 |
    121 | ) 122 | } else { 123 | return

    You are now allowed to write!
    Login first.

    124 | } 125 | } 126 | 127 | render() { 128 | return ( 129 |
    130 | {this.onShowInput()} 131 |
    132 | ) 133 | } 134 | } 135 | 136 | InputArea.propTypes = { 137 | allowed: React.PropTypes.bool.isRequired, 138 | type: React.PropTypes.string.isRequired, 139 | history: React.PropTypes.object.isRequired, 140 | day: React.PropTypes.string.isRequired, 141 | allTags: React.PropTypes.array.isRequired 142 | } 143 | 144 | export default InputArea 145 | -------------------------------------------------------------------------------- /app/components/NotFound/NotFound.css: -------------------------------------------------------------------------------- 1 | .notFound { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: column; 6 | height: 100vh; 7 | } 8 | 9 | .notFound h1 { 10 | color: #009a94; 11 | } 12 | 13 | .notFound iframe { 14 | border: none; 15 | height: 50px; 16 | width: 200px; 17 | } -------------------------------------------------------------------------------- /app/components/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import style from './NotFound.css' 4 | 5 | class NotFound extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.displayName = 'NotFound' 9 | } 10 | render() { 11 | return ( 12 |
    13 |

    404

    14 |

    Not Found

    15 | 17 |

    Only Music ... Was Left

    18 |
    19 | ) 20 | } 21 | } 22 | 23 | export default NotFound 24 | -------------------------------------------------------------------------------- /app/components/SearchResult/SearchResult.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from '../TagsResult/TagsResult.css' 3 | import { Link } from 'react-router' 4 | 5 | class SearchResult extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.displayName = 'SearchResult' 9 | this.state = { 10 | articles: [] 11 | } 12 | } 13 | 14 | componentDidMount() { 15 | this.props.actions.getTitles({ 16 | type: "SEARCH", 17 | searchString: this.props.params.word 18 | }) 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | this.setState({ 23 | articles: nextProps.getTitles.articles 24 | }) 25 | } 26 | 27 | renderTitle() { 28 | if(this.state.articles.length === 0) return
  • Could not found anything...
  • 29 | return this.state.articles.map((a, i) => 30 |
  • 31 | {a.title} 32 | 33 |
  • 34 | ) 35 | } 36 | 37 | render() { 38 | return ( 39 |
    40 |

    SEARCH: {this.props.params.word}

    41 |
      42 | {this.renderTitle()} 43 |
    44 |
    45 | ) 46 | } 47 | } 48 | 49 | export default SearchResult 50 | -------------------------------------------------------------------------------- /app/components/Sidebar/Sidebar.css: -------------------------------------------------------------------------------- 1 | /* 2 | .sidebar { 3 | width: 10rem; 4 | display: flex; 5 | border-left: 1px solid #3b4c56; 6 | flex-direction: column; 7 | padding-left: 1rem; 8 | } 9 | 10 | .music { 11 | border: 0; 12 | margin: 0; 13 | width: 18rem; 14 | } 15 | 16 | .sidebar div { 17 | color: #344653; 18 | width: 9rem; 19 | height: 4rem; 20 | } 21 | 22 | .sidebar input { 23 | width: 9rem; 24 | background-color: transparent; 25 | border: 1px solid #344653; 26 | box-shadow: 0 0 1px black; 27 | height: 1.5rem; 28 | line-height: 1.5rem; 29 | padding-left: 0.3rem; 30 | font-size: 1rem; 31 | color: #344653; 32 | } 33 | 34 | .sidebar ul { 35 | margin-top: 0.5rem; 36 | } 37 | 38 | .sidebar li { 39 | margin-right: 0.5rem; 40 | } 41 | 42 | .sidebar li a:hover { 43 | text-decoration: underline; 44 | color: black; 45 | }*/ 46 | 47 | .sidebar { 48 | display: flex; 49 | flex-direction: column; 50 | width: 10rem; 51 | border-left: 1px solid #009a94; 52 | padding-left: 1rem; 53 | } 54 | 55 | .sidebar ul { 56 | margin-left: 0.2rem; 57 | } 58 | 59 | .sidebar input { 60 | border: 1px solid #009A9A; 61 | border-radius: 2px; 62 | height: 1.2rem; 63 | font-size: 0.8rem; 64 | margin: 0.5rem 0 0.5rem 0.5rem; 65 | width: 9.4rem; 66 | padding-left: 0.2rem; 67 | box-shadow: 0 0 2px gray; 68 | } 69 | 70 | .sidebar li a{ 71 | margin-right: 1rem; 72 | } 73 | 74 | .sidebar li a:hover { 75 | text-decoration: underline; 76 | } 77 | 78 | .sidebar iframe { 79 | width: 10.3rem; 80 | border: none; 81 | } 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/components/Sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import style from './Sidebar.css' 5 | 6 | class Sidebar extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.displayName = 'Sidebar' 10 | this.state = { 11 | text: "", 12 | tags: [], 13 | titlesFetched: false 14 | } 15 | this.titlesFetched = false 16 | } 17 | 18 | componentDidMount() { 19 | // this.props.getTags() 20 | } 21 | 22 | componentWillReceiveProps(nextProps) { 23 | // the first time 24 | if(nextProps.titlesFetched && !this.titlesFetched) { 25 | this.titlesFetched = true 26 | this.props.getTags() 27 | } 28 | // got tags by ajax 29 | if(this.titlesFetched) { 30 | // this.titlesFetched = false 31 | this.setState({ 32 | tags: nextProps.tags 33 | }) 34 | } 35 | } 36 | 37 | // when click a tag 38 | onTagsHandler(e) { 39 | this.props.getTitles({ 40 | type: "TAGS", 41 | tagName: e.target.innerHTML 42 | }) 43 | } 44 | 45 | onRenderTags(tags) { 46 | if(tags.length !== 0) { 47 | return ( 48 | 54 | ) 55 | } 56 | } 57 | 58 | onChangeHandler(e) { 59 | this.setState({ 60 | text: e.target.value 61 | }) 62 | } 63 | 64 | onSearch(e) { 65 | const text = this.state.text 66 | if(e.which === 13 && text !== "") { 67 | 68 | this.props.getTitles({ 69 | type: "SEARCH", 70 | searchString: text 71 | }) 72 | 73 | this.props.history.pushState(null, "/searchResult/" + text) 74 | } 75 | } 76 | 77 | render() { 78 | const tags = this.state.tags 79 | return ( 80 |
    81 | TAGS: 82 |
      83 | {this.onRenderTags(tags)} 84 |
    85 | 89 | 92 |
    93 | ) 94 | } 95 | } 96 | 97 | Sidebar.propTypes = { 98 | getTags: React.PropTypes.func.isRequired, 99 | tags: React.PropTypes.array.isRequired, 100 | isFetched: React.PropTypes.bool.isRequired, 101 | isFetching: React.PropTypes.bool.isRequired, 102 | titlesFetched: React.PropTypes.bool.isRequired, 103 | history: React.PropTypes.object.isRequired, 104 | getTitles: React.PropTypes.func.isRequired 105 | } 106 | 107 | export default Sidebar 108 | -------------------------------------------------------------------------------- /app/components/Single/Single.css: -------------------------------------------------------------------------------- 1 | /* 2 | .single h1 { 3 | line-height: 2; 4 | color: #344653; 5 | } 6 | 7 | .single li { 8 | display: inline-block; 9 | } 10 | 11 | .single li a { 12 | margin-right: 1rem; 13 | text-decoration: none; 14 | color: #344653; 15 | } 16 | 17 | .single button { 18 | margin-top: 5rem; 19 | margin-right: 1rem; 20 | }*/ 21 | 22 | .single { 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: space-between; 26 | } 27 | 28 | .single li { 29 | margin-right: 0.5rem; 30 | margin-bottom: 1rem; 31 | } 32 | 33 | .single button { 34 | margin-right: 1rem; 35 | margin-top: 3rem; 36 | } 37 | 38 | .single button a:hover { 39 | background-color: #009A94; 40 | color: white; 41 | } 42 | 43 | /*markdowm*/ 44 | 45 | .md { 46 | font-size: 0.8rem; 47 | } 48 | 49 | .md h1, 50 | .md h2, 51 | .md h3, 52 | .md h4, 53 | .md h5, 54 | .md h6 { 55 | margin-bottom: 0.5rem; 56 | margin-top: 0.5rem; 57 | } 58 | 59 | .md li { 60 | display: list-item; 61 | margin: 0; 62 | padding: 0; 63 | } 64 | 65 | .md p { 66 | line-height: 1rem; 67 | } 68 | 69 | .md ul { 70 | list-style: square; 71 | padding-left:1.2rem; 72 | } 73 | 74 | .md ol { 75 | padding-left:1.2rem; 76 | } 77 | 78 | .md pre { 79 | background-color: rgba(0,154,148,0.7); 80 | color: white; 81 | padding: 8px 10px; 82 | border-radius: 0.4em; 83 | -moz-border-radius: 0.4em; 84 | -webkit-border-radius: 0.4em; 85 | overflow-x: hidden; 86 | } 87 | 88 | .md code { 89 | background-color: #47b3af; 90 | color: white; 91 | } 92 | 93 | .md pre code { 94 | font-size: 10pt; 95 | font-family: "Consolas", "Menlo", "Monaco", monospace, serif; 96 | } 97 | 98 | .md blockquote { 99 | font-style: italic; 100 | margin: 1rem 1rem 1rem; 101 | padding-left: 1rem; 102 | border-left: 3px solid #47b3af; 103 | } 104 | 105 | .md a { 106 | font-size: 0.9rem; 107 | } 108 | 109 | .md a:hover { 110 | text-decoration: underline; 111 | } 112 | 113 | .md img { 114 | width: 100%; 115 | box-shadow: 0 0 10px #009a94; 116 | } -------------------------------------------------------------------------------- /app/components/Single/Single.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router' 3 | import md from 'marked' 4 | 5 | import style from './Single.css' 6 | 7 | import Comment from '../Comments/Comments.js' 8 | import AddComment from '../AddComment/AddComment.js' 9 | 10 | class Single extends React.Component { 11 | constructor(props) { 12 | super(props) 13 | this.displayName = 'Single' 14 | this.state = { 15 | article: {}, 16 | comments: [], 17 | firstTime: true 18 | } 19 | } 20 | 21 | componentDidMount() { 22 | this.props.actions.getSingle(this.props.params.day, this.props.params.title) 23 | } 24 | 25 | componentWillReceiveProps(nextProps) { 26 | if(this.state.firstTime && nextProps.single.isFetched) { 27 | this.setState({ 28 | article: nextProps.single.article, 29 | comments: nextProps.single.article.comments, 30 | firstTime: false 31 | }) 32 | } 33 | } 34 | 35 | onDeleteHandler() { 36 | if(window.confirm("Are you sure to delete this post?")) { 37 | this.props.actions.deleteArticle(this.props.params.day, this.props.params.title) 38 | } 39 | } 40 | 41 | onChildComment(nextState) { 42 | this.setState({ 43 | comments: [ ...nextState ] 44 | }) 45 | } 46 | 47 | render() { 48 | const { 49 | title, 50 | time, 51 | content, 52 | tags 53 | } = this.state.article 54 | const comments = this.state.comments 55 | const allowed = (this.props.check.isAuthenticated || this.props.auth.isAuthenticated) 56 | return ( 57 |
    58 | {this.state.article.content ? 59 |
    60 |

    {title}

    61 | 62 |
      63 | {this.state.article.tags.map((t, i) => 64 |
    • {t}
    • )} 65 |
    66 |
    67 |
    68 |
    69 | {allowed? 70 | 71 | : ""} 72 | {allowed? 73 | 74 | : ""} 75 |
    76 | :
    Error
    } 77 | 78 | 86 |
    87 | ); 88 | } 89 | } 90 | 91 | export default Single; 92 | -------------------------------------------------------------------------------- /app/components/TagsResult/TagsResult.css: -------------------------------------------------------------------------------- 1 | /* 2 | .tagResult { 3 | 4 | } 5 | 6 | .tagResult li { 7 | display: block; 8 | } 9 | 10 | .tagResult a { 11 | margin-right: 1rem; 12 | } 13 | 14 | .tagResult a:hover { 15 | color: black; 16 | text-decoration: underline; 17 | } 18 | */ 19 | .tagResult li { 20 | display: block; 21 | } 22 | 23 | .tagResult a { 24 | margin-right: 1rem; 25 | } 26 | 27 | .tagResult a:hover { 28 | text-decoration: underline; 29 | } -------------------------------------------------------------------------------- /app/components/TagsResult/TagsResult.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './TagsResult.css' 3 | import { Link } from 'react-router' 4 | 5 | class List extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.displayName = 'List' 9 | this.state = { 10 | articles: [], 11 | tagName: this.props.params.tag 12 | } 13 | } 14 | 15 | componentDidMount() { 16 | this.props.actions.getTitles({ 17 | type: "TAGS", 18 | tagName: this.props.params.tag 19 | }) 20 | } 21 | 22 | componentWillReceiveProps(nextProps) { 23 | this.setState({ 24 | articles: nextProps.getTitles.articles 25 | }) 26 | } 27 | 28 | renderTitle() { 29 | return this.state.articles.map((a, i) => 30 |
  • 31 | {a.title} 32 | 33 |
  • 34 | ) 35 | } 36 | 37 | render() { 38 | return ( 39 |
    40 |

    TAGS: {this.props.params.tag}

    41 |
      42 | {this.renderTitle()} 43 |
    44 |
    45 | ) 46 | } 47 | } 48 | 49 | export default List; 50 | -------------------------------------------------------------------------------- /app/components/Write/Write.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import InputArea from '../InputArea/InputArea.js' 4 | 5 | class Write extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.displayName = 'Write' 9 | } 10 | render() { 11 | const date = new Date() 12 | const day = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() 13 | return ( 14 |
    15 | 21 |
    22 | ) 23 | } 24 | } 25 | 26 | export default Write 27 | -------------------------------------------------------------------------------- /app/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | /* 2 | import {createConstants} from '../utils.js' 3 | 4 | export createConstants( 5 | 'LOGIN_USER_REQUEST', 6 | 'LOGIN_USER_FAILURE', 7 | 'LOGIN_USER_SUCCESS', 8 | 'LOGOUT_USER' 9 | ) 10 | */ 11 | 12 | // authenticate 13 | export const LOGIN_USER_REQUEST = "LOGIN_USER_REQUEST" 14 | export const LOGIN_USER_SUCCESS = "LOGIN_USER_SUCCESS" 15 | export const LOGIN_USER_FAILURE = "LOGIN_USER_FAILURE" 16 | export const LOGOUT_USER = "LOGOUT_USER" 17 | 18 | // check whether it is authenticated 19 | export const CHECK_AUTHENTICATED_REQUEST = "CHECK_AUTHENTICATED_REQUEST" 20 | export const CHECK_AUTHENTICATED_MATCHED = "CHECK_AUTHENTICATED_MATCHED" 21 | export const CHECK_AUTHENTICATED_FAILURE = "CHECK_AUTHENTICATED_FAILURE" 22 | 23 | // fetch articles from be 24 | export const TITLES_GET_REQUEST = "TITLES_GET_REQUEST" 25 | export const TITLES_GET_SUCCESS = "TITLES_GET_SUCCESS" 26 | export const TITLES_GET_FAILURE = "TITLES_GET_FAILURE" 27 | 28 | // fetch tags 29 | export const TAGS_REQUEST = "TAGS_REQUEST" 30 | export const TAGS_SUCCESS = "TAGS_SUCCESS" 31 | export const TAGS_FAILURE = "TAGS_FAILURE" 32 | 33 | // upload post 34 | export const POST_ARTICLE_REQUEST = "POST_ARTICLE_REQUEST" 35 | export const POST_ARTICLE_SUCCESS = "POST_ARTICLE_SUCCESS" 36 | export const POST_ARTICLE_FAILURE = "POST_ARTICLE_FAILURE" 37 | 38 | // single 39 | export const SINGLE_REQUEST = "SINGLE_REQUEST" 40 | export const SINGLE_SUCCESS = "SINGLE_SUCCESS" 41 | export const SINGLE_FAILURE = "SINGLE_FAILURE" 42 | 43 | // edit article 44 | export const EDIT_ARTICLE_REQUEST = "EDIT_ARTICLE_REQUEST" 45 | export const EDIT_ARTICLE_SUCCESS = "EDIT_ARTICLE_SUCCESS" 46 | export const EDIT_ARTICLE_FAILURE = "EDIT_ARTICLE_FAILURE" 47 | 48 | // delete article 49 | export const DELETE_ARTICLE_REQUEST = "DELETE_ARTICLE_REQUEST" 50 | export const DELETE_ARTICLE_SUCCESS = "DELETE_ARTICLE_SUCCESS" 51 | export const DELETE_ARTICLE_FAILURE = "DELETE_ARTICLE_FAILURE" 52 | 53 | // add comment 54 | export const ADD_COMMENT_REQUEST = "ADD_COMMENT_REQUEST" 55 | export const ADD_COMMENT_SUCCESS = "ADD_COMMENT_SUCCESS" 56 | export const ADD_COMMENT_FAILURE = "ADD_COMMENT_FAILURE" 57 | 58 | // different types of request 59 | export const HOME = "HOME" // req page 60 | export const ARCHIVE = "ARCHIVE" // res date 61 | export const TAGS = "TAGS" // res string 62 | export const SEARCH = "SEARCH" // req string -------------------------------------------------------------------------------- /app/containers/App.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Vollkorn:400,400italic,700,700italic&subset=latin); 2 | 3 | * { 4 | padding : 0; 5 | margin: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | font-size: 120%; 11 | } 12 | 13 | li { 14 | display: inline-block; 15 | } 16 | 17 | a { 18 | color: #009A94; 19 | text-decoration: none; 20 | } 21 | 22 | body { 23 | font: 1rem "Vollkorn", "Microsoft YaHei", Palatino,Times; 24 | color: #333; 25 | line-height: 1.4; 26 | text-align: justify; 27 | background-color: #f2f1f0; 28 | } 29 | 30 | blockquote { 31 | margin-left: 1rem; 32 | padding-left: 1rem; 33 | border-left: 1px solid #ddd; 34 | } 35 | 36 | button:hover { 37 | cursor: pointer; 38 | } 39 | 40 | button a { 41 | text-decoration: none; 42 | color: #344653; 43 | line-height: 1.3; 44 | width: 4rem; 45 | height: 1.4rem; 46 | display: block; 47 | line-height: 1.4rem; 48 | display: inline-block; 49 | } 50 | 51 | button a:hover { 52 | background-color: #009A94; 53 | color: white; 54 | animation: btnAnimation 0.5s; 55 | -moz-animation: btnAnimation 0.5s; 56 | -webkit-animation: btnAnimation 0.5s; 57 | -o-animation: btnAnimation 0.5s; 58 | } 59 | 60 | h1, h2, h3{ 61 | font-weight: normal; 62 | } 63 | 64 | p { 65 | word-wrap: break-word; 66 | -webkit-hypens:auto; 67 | -moz-hypens:auto; 68 | hyphens:auto; 69 | } 70 | 71 | .container { 72 | display: flex; 73 | flex-direction: column; 74 | justify-content: space-between; 75 | min-height: 110vh; 76 | } 77 | 78 | 79 | /*button animation*/ 80 | 81 | @keyframes btnAnimation { 82 | from 83 | { 84 | background-color: #f2f1f0; 85 | color: black; 86 | } 87 | to 88 | { 89 | background-color: #009A94; 90 | color: white; 91 | } 92 | } 93 | 94 | @-moz-keyframes btnAnimation { 95 | from 96 | { 97 | background-color: #f2f1f0; 98 | color: black; 99 | } 100 | to 101 | { 102 | background-color: #009A94; 103 | color: white; 104 | } 105 | } 106 | 107 | @-webkit-keyframes btnAnimation { 108 | from 109 | { 110 | background-color: #f2f1f0; 111 | color: black; 112 | } 113 | to 114 | { 115 | background-color: #009A94; 116 | color: white; 117 | } 118 | } 119 | 120 | @-o-keyframes btnAnimation { 121 | from 122 | { 123 | background-color: #f2f1f0; 124 | color: black; 125 | } 126 | to 127 | { 128 | background-color: #009A94; 129 | color: white; 130 | } 131 | } -------------------------------------------------------------------------------- /app/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | // import components 7 | import Header from '../components/Header/Header.js' 8 | import Body from '../components/Body/Body.js' 9 | import Footer from '../components/Footer/Footer.js' 10 | import Sidebar from '../components/Sidebar/Sidebar.js' 11 | 12 | // import actions 13 | import * as actions from '../actions/index.js' 14 | 15 | // import css 16 | import style from './App.css' 17 | 18 | class App extends React.Component { 19 | render() { 20 | return ( 21 |
    22 |
    30 | 31 |
    32 |
    33 | ) 34 | } 35 | } 36 | 37 | const mapStateToProps = (state) => { 38 | return { 39 | auth: { 40 | token: state.auth.token, 41 | isAuthenticated: state.auth.isAuthenticated, 42 | isAuthenticating: state.auth.isAuthenticating, 43 | status: state.auth.status, 44 | statusText: state.auth.statusText, 45 | }, 46 | check: { 47 | isAuthenticating: state.check.isAuthenticating, 48 | isAuthenticated: state.check.isAuthenticated 49 | }, 50 | getTitles: { 51 | isFetched: state.getTitles.isFetched, 52 | isFetching: state.getTitles.isFetching, 53 | articles: state.getTitles.articles, 54 | fetchFailure: state.getTitles.fetchFailure, 55 | count: state.getTitles.count, 56 | err: state.getTitles.err 57 | }, 58 | tags: { 59 | isFetching: state.tags.isFetching, 60 | isFetched: state.tags.isFetched, 61 | err: state.tags.err, 62 | tags: state.tags.tags 63 | }, 64 | single: { 65 | isFetching: state.single.isFetching, 66 | isFetched: state.single.isFetched, 67 | article: state.single.article 68 | }, 69 | edit: { 70 | isPosting: state.edit.isPosting, 71 | isPosted: state.edit.isPosted 72 | }, 73 | remove: { 74 | isRemoving: state.remove.isRemoving, 75 | isRemoved: state.remove.isRemoved 76 | }, 77 | comment: { 78 | isPosting: state.edit.isPosting, 79 | isPosted: state.edit.isPosted 80 | } 81 | } 82 | } 83 | 84 | const mapDispatchToProps = (dispatch) => { 85 | return { 86 | actions: bindActionCreators(actions, dispatch) 87 | } 88 | } 89 | 90 | export default connect( 91 | mapStateToProps, 92 | mapDispatchToProps 93 | )(App) 94 | 95 | //{React.cloneElement(this.props.children, this.props)} -------------------------------------------------------------------------------- /app/helper/auth.js: -------------------------------------------------------------------------------- 1 | module.export = { 2 | login(password, cb) { 3 | cb = arguments[arguments.length - 1] 4 | if(localStorage.token) { 5 | if(cb) cb(true) 6 | return 7 | } 8 | 9 | fetch('/login', { 10 | method: "POST", 11 | header: { 12 | "Content-Type": "application/json", 13 | "Accept": "application/json", 14 | }, 15 | body: JSON.stringify({ password: password }) 16 | }).then(res => { 17 | if(res.ok) { 18 | console.log(res) 19 | } 20 | }, e => { 21 | alert("POST ERROR"); 22 | }) 23 | /* 24 | postRequset(password, (res) => { 25 | if(res.body.authenticated) { 26 | localStorage.token = res.token 27 | if (cb) cb(true) 28 | } else { 29 | if (cb) cb(false) 30 | } 31 | */ 32 | }) 33 | }, 34 | 35 | getToken() { 36 | return localStorage.token 37 | }, 38 | 39 | logout(cb) { 40 | delete localStorage.token 41 | if (cb) cb() 42 | }, 43 | 44 | loggedIn() { 45 | return !!localStorage.token 46 | } 47 | } -------------------------------------------------------------------------------- /app/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_USER_REQUEST, 3 | LOGIN_USER_SUCCESS, 4 | LOGIN_USER_FAILURE, 5 | LOGOUT_USER 6 | } from '../constants/actionTypes.js' 7 | import { pushState } from 'redux-router' 8 | 9 | const initialState = { 10 | token: null, 11 | isAuthenticated: false, 12 | isAuthenticating: false, 13 | status: null, 14 | statusText: null 15 | } 16 | 17 | const auth = (state = initialState, action) => { 18 | switch(action.type) { 19 | 20 | case LOGIN_USER_REQUEST: 21 | return Object.assign({}, state, { 22 | 'isAuthenticating': true, 23 | 'statusText': null 24 | }) 25 | 26 | case LOGIN_USER_SUCCESS: 27 | return Object.assign({}, state, { 28 | 'isAuthenticating': false, 29 | 'isAuthenticated': true, 30 | 'token': action.payload.token, 31 | 'statueText': 'You have been successfully logged in.' 32 | }) 33 | 34 | case LOGIN_USER_FAILURE: 35 | return Object.assign({}, state, { 36 | 'isAuthenticated': false, 37 | 'isAuthenticating': false, 38 | 'token': null, 39 | 'userName': null, 40 | 'statusText': `Authentication Error: ${action.payload.status} ${action.payload.status}` 41 | }) 42 | 43 | case LOGOUT_USER: 44 | return Object.assign({}, state, { 45 | 'isAuthenticated': false, 46 | 'isAuthenticating': false, 47 | 'token': null, 48 | 'statusText': 'You have been successfully logged out.' 49 | }) 50 | 51 | default: 52 | return Object.assign({}, state, {}) 53 | } 54 | } 55 | 56 | export default auth -------------------------------------------------------------------------------- /app/reducers/check.js: -------------------------------------------------------------------------------- 1 | import { 2 | CHECK_AUTHENTICATED_REQUEST, 3 | CHECK_AUTHENTICATED_MATCHED 4 | } from '../constants/actionTypes.js' 5 | 6 | const initialState = { 7 | isAuthenticating: false, 8 | isAuthenticated: false 9 | } 10 | 11 | const check = (state = initialState, action) => { 12 | switch(action.type) { 13 | case CHECK_AUTHENTICATED_REQUEST: 14 | return Object.assign({}, state, { 15 | isAuthenticating: true 16 | }) 17 | 18 | case CHECK_AUTHENTICATED_MATCHED: 19 | return Object.assign({}, state, { 20 | isAuthenticated: action.isAuthenticated, 21 | isAuthenticating: false 22 | }) 23 | 24 | default: 25 | return Object.assign({}, state, {}) 26 | } 27 | } 28 | 29 | export default check -------------------------------------------------------------------------------- /app/reducers/comment.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_COMMENT_REQUEST, 3 | ADD_COMMENT_SUCCESS, 4 | ADD_COMMENT_FAILURE 5 | } from '../constants/actionTypes.js' 6 | 7 | const initialState = { 8 | isPosting: false, 9 | isPosted: false 10 | } 11 | 12 | const comment = (state=initialState, action) => { 13 | switch(action.type) { 14 | case ADD_COMMENT_REQUEST: 15 | return Object.assign({}, state, { 16 | isPosting: true, 17 | isPosted: false 18 | }) 19 | 20 | case ADD_COMMENT_SUCCESS: 21 | return Object.assign({}, state, { 22 | isPosting: false, 23 | isPosted: true 24 | }) 25 | 26 | case ADD_COMMENT_FAILURE: 27 | return Object.assign({}, state, { 28 | isPosting: false, 29 | isPosted: false 30 | }) 31 | 32 | default: 33 | return Object.assign({}, state, {}) 34 | } 35 | } 36 | 37 | export default comment -------------------------------------------------------------------------------- /app/reducers/edit.js: -------------------------------------------------------------------------------- 1 | import { 2 | EDIT_ARTICLE_REQUEST, 3 | EDIT_ARTICLE_SUCCESS, 4 | EDIT_ARTICLE_FAILURE 5 | } from '../constants/actionTypes.js' 6 | 7 | const initialState = { 8 | isPosting: false, 9 | isPosted: false 10 | } 11 | 12 | const edit = (state=initialState, action) => { 13 | switch(action.type) { 14 | case EDIT_ARTICLE_REQUEST: 15 | return Object.assign({}, state, { 16 | isPosting: true, 17 | isPosted: false 18 | }) 19 | 20 | case EDIT_ARTICLE_SUCCESS: 21 | return Object.assign({}, state, { 22 | isPosting: false, 23 | isPosted: true 24 | }) 25 | 26 | case EDIT_ARTICLE_FAILURE: 27 | return Object.assign({}, state, { 28 | isPosting: false, 29 | isPosted: false 30 | }) 31 | 32 | default: 33 | return Object.assign({}, state, {}) 34 | } 35 | } 36 | 37 | export default edit -------------------------------------------------------------------------------- /app/reducers/getTitles.js: -------------------------------------------------------------------------------- 1 | import { 2 | TITLES_GET_REQUEST, 3 | TITLES_GET_SUCCESS, 4 | TITLES_GET_FAILURE 5 | } from '../constants/actionTypes.js' 6 | 7 | const initialState = { 8 | isFetching: false, 9 | isFetched: false, 10 | fetchFailure: false, 11 | articles: [], 12 | count: 1, 13 | err: null 14 | } 15 | 16 | const getTitles = (state = initialState, action) => { 17 | switch(action.type){ 18 | case TITLES_GET_REQUEST: 19 | return Object.assign({}, state, { 20 | isFetching: true, 21 | isFetched: false, 22 | fetchFailure: false 23 | }) 24 | 25 | case TITLES_GET_FAILURE: 26 | return Object.assign({}, state, { 27 | isFetching: false, 28 | isFetched: false, 29 | fetchFailure: false, 30 | err: action.err 31 | }) 32 | 33 | case TITLES_GET_SUCCESS: 34 | return Object.assign({}, state, { 35 | isFetching: false, 36 | isFetched: true, 37 | fetchFailure: false, 38 | articles: action.articles, 39 | count: action.count 40 | }) 41 | 42 | default: 43 | return Object.assign({}, state, {}) 44 | } 45 | } 46 | 47 | export default getTitles -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux' 2 | import {routerReducer} from 'react-router-redux' 3 | import auth from './auth.js' 4 | import check from './check.js' 5 | import getTitles from './getTitles.js' 6 | import tags from './tags.js' 7 | import single from './single.js' 8 | import edit from './edit.js' 9 | import remove from './remove.js' 10 | 11 | export default combineReducers({ 12 | auth, 13 | check, 14 | getTitles, 15 | tags, 16 | single, 17 | edit, 18 | remove, 19 | routing: routerReducer 20 | }) -------------------------------------------------------------------------------- /app/reducers/remove.js: -------------------------------------------------------------------------------- 1 | import { 2 | DELETE_ARTICLE_REQUEST, 3 | DELETE_ARTICLE_SUCCESS, 4 | DELETE_ARTICLE_FAILURE 5 | } from '../constants/actionTypes.js' 6 | 7 | const initialState = { 8 | isRemoving: false, 9 | isRemoved: false 10 | } 11 | 12 | const remove = (state=initialState, action) => { 13 | switch(action.type) { 14 | case DELETE_ARTICLE_REQUEST: 15 | return Object.assign({}, state, { 16 | isRemoving: true, 17 | isRemoved: false 18 | }) 19 | 20 | case DELETE_ARTICLE_SUCCESS: 21 | return Object.assign({}, state, { 22 | isRemoving: false, 23 | isRemoved: true 24 | }) 25 | 26 | case DELETE_ARTICLE_FAILURE: 27 | return Object.assign({}, state, { 28 | isRemoving: false, 29 | isRemoved: false 30 | }) 31 | 32 | default: 33 | return Object.assign({}, state, {}) 34 | } 35 | } 36 | 37 | export default remove -------------------------------------------------------------------------------- /app/reducers/single.js: -------------------------------------------------------------------------------- 1 | import { 2 | SINGLE_REQUEST, 3 | SINGLE_SUCCESS, 4 | SINGLE_FAILURE 5 | } from '../constants/actionTypes.js' 6 | 7 | const initialState = { 8 | article: {}, 9 | isFetching: false, 10 | isFetched: false 11 | } 12 | 13 | const single = (state=initialState, action) => { 14 | switch(action.type) { 15 | case SINGLE_REQUEST: 16 | return Object.assign({}, state, { 17 | isFetching: true, 18 | isFetched: false 19 | }) 20 | 21 | case SINGLE_SUCCESS: 22 | return Object.assign({}, state, { 23 | isFetching: false, 24 | isFetched: true, 25 | article: action.article 26 | }) 27 | 28 | case SINGLE_FAILURE: 29 | return Object.assign({}, state, { 30 | isFetching: false, 31 | isFetched: false 32 | }) 33 | 34 | default: 35 | return Object.assign({}, state, {}) 36 | } 37 | } 38 | 39 | export default single -------------------------------------------------------------------------------- /app/reducers/tags.js: -------------------------------------------------------------------------------- 1 | import { 2 | TAGS_SUCCESS, 3 | TAGS_FAILURE, 4 | TAGS_REQUEST 5 | } from '../constants/actionTypes.js' 6 | 7 | const initialState = { 8 | tags: [], 9 | isFetching: false, 10 | isFetched: false, 11 | err: null 12 | } 13 | 14 | const tags = (state=initialState, action) => { 15 | switch(action.type) { 16 | case TAGS_REQUEST: 17 | return Object.assign({}, state, { 18 | isFetching: true, 19 | isFetched: false 20 | }) 21 | 22 | case TAGS_SUCCESS: 23 | return Object.assign({}, state, { 24 | isFetching: false, 25 | isFetched: true, 26 | tags: action.tags 27 | }) 28 | 29 | case TAGS_FAILURE: 30 | return Object.assign({}, state, { 31 | isFetching: false, 32 | isFetched: false, 33 | err: action.err 34 | }) 35 | 36 | default: 37 | return Object.assign({}, state, {}) 38 | } 39 | } 40 | 41 | export default tags -------------------------------------------------------------------------------- /app/reducers/upload.js: -------------------------------------------------------------------------------- 1 | import { 2 | POST_ARITCLE_REQUEST, 3 | POST_ARITCLE_SUCCESS, 4 | POST_ARITCLE_FAILURE 5 | } from '../constants/actionTypes.js' 6 | 7 | const initialState = { 8 | isPosting: false, 9 | isPosted: false, 10 | err: null 11 | } 12 | 13 | const upload = (state=initialState, action) => { 14 | switch(action.type) { 15 | case POST_ARITCLE_REQUEST: 16 | return Object.assign({}, state, { 17 | isPosting: true, 18 | isPosted: false 19 | }) 20 | 21 | case POST_ARITCLE_SUCCESS: 22 | return Object.assign({}, state, { 23 | isPosting: false, 24 | isPosted: true 25 | }) 26 | 27 | case POST_ARITCLE_FAILURE: 28 | return Object.assign({}, state, { 29 | isPosting: false, 30 | isPosted: false, 31 | err: action.err 32 | }) 33 | 34 | default: 35 | return Object.assign({}, state, {}) 36 | } 37 | } -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | // import 'babel-polyfill' 2 | import React from 'react' 3 | import { Redirect, Router, Route, IndexRoute, browserHistory } from 'react-router' 4 | import { syncHistoryWithStore } from 'react-router-redux' 5 | import { Provider } from 'react-redux' 6 | 7 | // import components 8 | import App from './containers/App.js' 9 | import About from './components/About/About.js' 10 | import Archive from './components/Archive/Archive.js' 11 | import Edit from './components/Edit/Edit.js' 12 | import Home from './components/Home/Home.js' 13 | import TagsResult from './components/TagsResult/TagsResult.js' 14 | import Single from './components/Single/Single.js' 15 | import SearchResult from './components/SearchResult/SearchResult.js' 16 | import Write from './components/Write/Write.js' 17 | import NotFound from './components/NotFound/NotFound.js' 18 | 19 | import rootReducer from './reducers/index.js' 20 | import configureStore from './store/configureStore.js' 21 | 22 | const store = configureStore() 23 | const history = syncHistoryWithStore(browserHistory, store) 24 | 25 | // route 26 | const router = ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | 44 | export default router 45 | 46 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import rootReducer from '../reducers/index.js' 2 | import routes from '../routes' 3 | import {reduxReactRouter} from 'redux-router' 4 | import {applyMiddleware, compose, createStore} from 'redux' 5 | import createHistory from 'history/lib/createBrowserHistory' 6 | import createLogger from 'redux-logger' 7 | import thunk from 'redux-thunk' 8 | 9 | const configureStore = (initialState) => { 10 | 11 | const createStoreWithMiddleware = compose( 12 | applyMiddleware(thunk, createLogger()), 13 | reduxReactRouter({routes, createHistory}) 14 | ) 15 | 16 | if (module.hot) { 17 | module.hot 18 | .accept('../reducers/index.js', () => { 19 | const nextRootReducer = require('../reducers/index.js') 20 | store.replaceReducer(nextRootReducer) 21 | }) 22 | } 23 | 24 | const store = createStoreWithMiddleware(createStore)(rootReducer, initialState) 25 | 26 | return store 27 | } 28 | 29 | export default configureStore -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | // check http 2 | export const checkHttpStatus = (res) => { 3 | if(res.status >= 200 && res.status < 300) { 4 | return res 5 | } else { 6 | var error = new Error(res.statusText) 7 | error.res = res 8 | throw error 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /data/user.js: -------------------------------------------------------------------------------- 1 | exports.password="123" -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],[function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}var o=n(1),i=(r(o),n(33)),a=n(172),u=r(a);(0,i.render)(u["default"],document.getElementById("root"))},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var o=n(1),i=r(o),a=n(173),u=n(235),l=n(240),s=n(260),c=r(s),f=n(266),d=(r(f),n(301)),p=r(d),h=n(304),m=r(h),y=n(312),g=r(y),b=n(315),E=r(b),v=n(318),_=r(v),T=n(327),S=r(T),R=n(328),w=r(R),O=n(329),A=r(O),C=n(332),P=(r(C),n(340)),k=r(P),j=(0,k["default"])(),x=(0,u.syncHistoryWithStore)(a.browserHistory,j),I=i["default"].createElement(l.Provider,{store:j},i["default"].createElement(a.Router,{history:x},i["default"].createElement(a.Route,{path:"/page/:pageId",component:c["default"]},i["default"].createElement(a.IndexRoute,{component:g["default"]}),i["default"].createElement(a.Route,{path:"/archive",component:p["default"]}),i["default"].createElement(a.Route,{path:"/tagsResult/:tag",component:E["default"]}),i["default"].createElement(a.Route,{path:"/write",component:w["default"]}),i["default"].createElement(a.Route,{path:"/edit/:day/:title",component:m["default"]}),i["default"].createElement(a.Route,{path:"/article/:day/:title",component:_["default"]}),i["default"].createElement(a.Route,{path:"/searchResult/:word",component:S["default"]}),i["default"].createElement(a.Redirect,{from:"/",to:"/page/1"})),i["default"].createElement(a.Route,{path:"*",component:A["default"]})));t["default"]=I},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0}),t.routerMiddleware=t.routerActions=t.goForward=t.goBack=t.go=t.replace=t.push=t.CALL_HISTORY_METHOD=t.routerReducer=t.LOCATION_CHANGE=t.syncHistoryWithStore=void 0;var o=n(236);Object.defineProperty(t,"LOCATION_CHANGE",{enumerable:!0,get:function(){return o.LOCATION_CHANGE}}),Object.defineProperty(t,"routerReducer",{enumerable:!0,get:function(){return o.routerReducer}});var i=n(237);Object.defineProperty(t,"CALL_HISTORY_METHOD",{enumerable:!0,get:function(){return i.CALL_HISTORY_METHOD}}),Object.defineProperty(t,"push",{enumerable:!0,get:function(){return i.push}}),Object.defineProperty(t,"replace",{enumerable:!0,get:function(){return i.replace}}),Object.defineProperty(t,"go",{enumerable:!0,get:function(){return i.go}}),Object.defineProperty(t,"goBack",{enumerable:!0,get:function(){return i.goBack}}),Object.defineProperty(t,"goForward",{enumerable:!0,get:function(){return i.goForward}}),Object.defineProperty(t,"routerActions",{enumerable:!0,get:function(){return i.routerActions}});var a=n(238),u=r(a),l=n(239),s=r(l);t.syncHistoryWithStore=u["default"],t.routerMiddleware=s["default"]},function(e,t){"use strict";function n(){var e=arguments.length<=0||void 0===arguments[0]?i:arguments[0],t=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=t.type,a=t.payload;return n===o?r({},e,{locationBeforeTransitions:a}):e}Object.defineProperty(t,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t=0&&E.splice(t,1)}function u(e){var t=document.createElement("style");return t.type="text/css",i(e,t),t}function l(e){var t=document.createElement("link");return t.rel="stylesheet",i(e,t),t}function s(e,t){var n,r,o;if(t.singleton){var i=b++;n=g||(g=u(t)),r=c.bind(null,n,i,!1),o=c.bind(null,n,i,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=l(t),r=d.bind(null,n),o=function(){a(n),n.href&&URL.revokeObjectURL(n.href)}):(n=u(t),r=f.bind(null,n),o=function(){a(n)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else o()}}function c(e,t,n,r){var o=n?"":r.css;if(e.styleSheet)e.styleSheet.cssText=v(t,o);else{var i=document.createTextNode(o),a=e.childNodes;a[t]&&e.removeChild(a[t]),a.length?e.insertBefore(i,a[t]):e.appendChild(i)}}function f(e,t){var n=t.css,r=t.media;if(r&&e.setAttribute("media",r),e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}function d(e,t){var n=t.css,r=t.sourceMap;r&&(n+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(r))))+" */");var o=new Blob([n],{type:"text/css"}),i=e.href;e.href=URL.createObjectURL(o),i&&URL.revokeObjectURL(i)}var p={},h=function(e){var t;return function(){return"undefined"==typeof t&&(t=e.apply(this,arguments)),t}},m=h(function(){return/msie [6-9]\b/.test(window.navigator.userAgent.toLowerCase())}),y=h(function(){return document.head||document.getElementsByTagName("head")[0]}),g=null,b=0,E=[];e.exports=function(e,t){t=t||{},"undefined"==typeof t.singleton&&(t.singleton=m()),"undefined"==typeof t.insertAt&&(t.insertAt="bottom");var n=o(e);return r(n,t),function(e){for(var i=[],a=0;ah2{text-align:center;text-decoration:line-through}._3qRdtBEXfqdcXq8X1BZ1BJ button{text-decoration:none;color:#344653;line-height:1.5;width:4rem;height:1.4rem;display:block;line-height:1.4rem;margin:.5rem auto}._3qRdtBEXfqdcXq8X1BZ1BJ button:hover{background-color:#009a94;border:1px solid #fff;color:#fff}",""]),t.locals={about:"_3qRdtBEXfqdcXq8X1BZ1BJ"}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;n-1?t:e}function f(e,t){t=t||{};var n=t.body;if(f.prototype.isPrototypeOf(e)){if(e.bodyUsed)throw new TypeError("Already read");this.url=e.url,this.credentials=e.credentials,t.headers||(this.headers=new o(e.headers)),this.method=e.method,this.mode=e.mode,n||(n=e._bodyInit,e.bodyUsed=!0)}else this.url=e;if(this.credentials=t.credentials||this.credentials||"omit",!t.headers&&this.headers||(this.headers=new o(t.headers)),this.method=c(t.method||this.method||"GET"),this.mode=t.mode||this.mode||null,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(n)}function d(e){var t=new FormData;return e.trim().split("&").forEach(function(e){if(e){var n=e.split("="),r=n.shift().replace(/\+/g," "),o=n.join("=").replace(/\+/g," ");t.append(decodeURIComponent(r),decodeURIComponent(o))}}),t}function p(e){var t=new o,n=(e.getAllResponseHeaders()||"").trim().split("\n");return n.forEach(function(e){var n=e.trim().split(":"),r=n.shift().trim(),o=n.join(":").trim();t.append(r,o)}),t}function h(e,t){t||(t={}),this.type="default",this.status=t.status,this.ok=this.status>=200&&this.status<300,this.statusText=t.statusText,this.headers=t.headers instanceof o?t.headers:new o(t.headers),this.url=t.url||"",this._initBody(e)}if(!e.fetch){var m={searchParams:"URLSearchParams"in e,iterable:"Symbol"in e&&"iterator"in Symbol,blob:"FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(e){return!1}}(),formData:"FormData"in e,arrayBuffer:"ArrayBuffer"in e};o.prototype.append=function(e,r){e=t(e),r=n(r);var o=this.map[e];o||(o=[],this.map[e]=o),o.push(r)},o.prototype["delete"]=function(e){delete this.map[t(e)]},o.prototype.get=function(e){var n=this.map[t(e)];return n?n[0]:null},o.prototype.getAll=function(e){return this.map[t(e)]||[]},o.prototype.has=function(e){return this.map.hasOwnProperty(t(e))},o.prototype.set=function(e,r){this.map[t(e)]=[n(r)]},o.prototype.forEach=function(e,t){Object.getOwnPropertyNames(this.map).forEach(function(n){this.map[n].forEach(function(r){e.call(t,r,n,this)},this)},this)},o.prototype.keys=function(){var e=[];return this.forEach(function(t,n){e.push(n)}),r(e)},o.prototype.values=function(){var e=[];return this.forEach(function(t){e.push(t)}),r(e)},o.prototype.entries=function(){var e=[];return this.forEach(function(t,n){e.push([n,t])}),r(e)},m.iterable&&(o.prototype[Symbol.iterator]=o.prototype.entries);var y=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];f.prototype.clone=function(){return new f(this)},s.call(f.prototype),s.call(h.prototype),h.prototype.clone=function(){return new h(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new o(this.headers),url:this.url})},h.error=function(){var e=new h(null,{status:0,statusText:""});return e.type="error",e};var g=[301,302,303,307,308];h.redirect=function(e,t){if(g.indexOf(t)===-1)throw new RangeError("Invalid status code");return new h(null,{status:t,headers:{location:e}})},e.Headers=o,e.Request=f,e.Response=h,e.fetch=function(e,t){return new Promise(function(n,r){function o(){return"responseURL"in a?a.responseURL:/^X-Request-URL:/m.test(a.getAllResponseHeaders())?a.getResponseHeader("X-Request-URL"):void 0}var i;i=f.prototype.isPrototypeOf(e)&&!t?e:new f(e,t);var a=new XMLHttpRequest;a.onload=function(){var e={status:a.status,statusText:a.statusText,headers:p(a),url:o()},t="response"in a?a.response:a.responseText;n(new h(t,e))},a.onerror=function(){r(new TypeError("Network request failed"))},a.ontimeout=function(){r(new TypeError("Network request failed"))},a.open(i.method,i.url,!0),"include"===i.credentials&&(a.withCredentials=!0),"responseType"in a&&m.blob&&(a.responseType="blob"),i.headers.forEach(function(e,t){a.setRequestHeader(t,e)}),a.send("undefined"==typeof i._bodyInit?null:i._bodyInit)})},e.fetch.polyfill=!0}}("undefined"!=typeof self?self:this)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.LOGIN_USER_REQUEST="LOGIN_USER_REQUEST",t.LOGIN_USER_SUCCESS="LOGIN_USER_SUCCESS",t.LOGIN_USER_FAILURE="LOGIN_USER_FAILURE",t.LOGOUT_USER="LOGOUT_USER",t.CHECK_AUTHENTICATED_REQUEST="CHECK_AUTHENTICATED_REQUEST",t.CHECK_AUTHENTICATED_MATCHED="CHECK_AUTHENTICATED_MATCHED",t.CHECK_AUTHENTICATED_FAILURE="CHECK_AUTHENTICATED_FAILURE",t.TITLES_GET_REQUEST="TITLES_GET_REQUEST",t.TITLES_GET_SUCCESS="TITLES_GET_SUCCESS",t.TITLES_GET_FAILURE="TITLES_GET_FAILURE",t.TAGS_REQUEST="TAGS_REQUEST",t.TAGS_SUCCESS="TAGS_SUCCESS",t.TAGS_FAILURE="TAGS_FAILURE",t.POST_ARTICLE_REQUEST="POST_ARTICLE_REQUEST",t.POST_ARTICLE_SUCCESS="POST_ARTICLE_SUCCESS",t.POST_ARTICLE_FAILURE="POST_ARTICLE_FAILURE",t.SINGLE_REQUEST="SINGLE_REQUEST",t.SINGLE_SUCCESS="SINGLE_SUCCESS",t.SINGLE_FAILURE="SINGLE_FAILURE",t.EDIT_ARTICLE_REQUEST="EDIT_ARTICLE_REQUEST",t.EDIT_ARTICLE_SUCCESS="EDIT_ARTICLE_SUCCESS",t.EDIT_ARTICLE_FAILURE="EDIT_ARTICLE_FAILURE",t.DELETE_ARTICLE_REQUEST="DELETE_ARTICLE_REQUEST",t.DELETE_ARTICLE_SUCCESS="DELETE_ARTICLE_SUCCESS",t.DELETE_ARTICLE_FAILURE="DELETE_ARTICLE_FAILURE",t.ADD_COMMENT_REQUEST="ADD_COMMENT_REQUEST",t.ADD_COMMENT_SUCCESS="ADD_COMMENT_SUCCESS",t.ADD_COMMENT_FAILURE="ADD_COMMENT_FAILURE",t.HOME="HOME",t.ARCHIVE="ARCHIVE",t.TAGS="TAGS",t.SEARCH="SEARCH"},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}t.__esModule=!0;var o=n(283),i=r(o);t.routerStateReducer=i["default"];var a=n(285),u=r(a);t.ReduxRouter=u["default"];var l=n(288),s=r(l);t.reduxReactRouter=s["default"];var c=n(294),f=r(c);t.isActive=f["default"];var d=n(287);t.historyAPI=d.historyAPI,t.push=d.push,t.replace=d.replace,t.setState=d.setState,t.go=d.go,t.goBack=d.goBack,t.goForward=d.goForward},function(e,t,n){"use strict";function r(e,t){void 0===e&&(e=null);var n;switch(t.type){case i.ROUTER_DID_CHANGE:return t.payload;case i.REPLACE_ROUTES:return e?o({},e,(n={},n[i.DOES_NEED_REFRESH]=!0,n)):e;default:return e}}t.__esModule=!0;var o=Object.assign||function(e){for(var t=1;t. Make sure you're using a ");var t=e.history,n=e[g.ROUTER_STATE_SELECTOR];if(!t||!n)throw new Error("Redux store not configured properly for . Make sure you're using the reduxReactRouter() store enhancer.");return f["default"].createElement(v,l({history:t,routerStateSelector:a(n),router:this.router},this.props))},t}(c.Component),v=function(e){function t(){o(this,n),e.apply(this,arguments)}i(t,e),t.prototype.render=function(){var e=this.props.location;if(null===e||void 0===e)return null;var t=this.props.RoutingContext||p.RouterContext;return f["default"].createElement(t,this.props)},s(t,null,[{key:"propTypes",value:{location:c.PropTypes.object,RoutingContext:c.PropTypes.element},enumerable:!0}]);var n=t;return t=d.connect(function(e,t){var n=t.routerStateSelector;return n(e)||{}})(t)||t}(c.Component);t["default"]=E,e.exports=t["default"]},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){return!e&&!t||!(e&&!t||!e&&t)&&(!e[u.DOES_NEED_REFRESH]&&!t[u.DOES_NEED_REFRESH]&&(e.location.pathname===t.location.pathname&&e.location.search===t.location.search&&a["default"](e.location.state,t.location.state)))}t.__esModule=!0,t["default"]=o;var i=n(191),a=r(i),u=n(284);e.exports=t["default"]},function(e,t,n){"use strict";function r(e){return{type:u.ROUTER_DID_CHANGE,payload:e}}function o(e){return{type:u.INIT_ROUTES,payload:e}}function i(e){return{type:u.REPLACE_ROUTES,payload:e}}function a(e){return function(){for(var t=arguments.length,n=Array(t),r=0;r=200&&e.status<300)return e;var t=new Error(e.statusText);throw t.res=e,t}},function(e,t,n){"use strict";var r=n(297);e.exports=function(e){if("string"!=typeof e)throw new Error("Invalid token specified");return JSON.parse(r(e.split(".")[1]))}},function(e,t,n){function r(e){return decodeURIComponent(o(e).replace(/(.)/g,function(e,t){var n=t.charCodeAt(0).toString(16).toUpperCase();return n.length<2&&(n="0"+n),"%"+n}))}var o=n(298);e.exports=function(e){var t=e.replace(/-/g,"+").replace(/_/g,"/");switch(t.length%4){case 0:break;case 2:t+="==";break;case 3:t+="=";break;default:throw"Illegal base64url string!"}try{return r(t)}catch(n){return o(t)}}},function(e,t){function n(e){this.message=e}function r(e){var t=String(e).replace(/=+$/,"");if(t.length%4==1)throw new n("'atob' failed: The string to be decoded is not correctly encoded.");for(var r,i,a=0,u=0,l="";i=t.charAt(u++);~i&&(r=a%4?64*r+i:i,a++%4)?l+=String.fromCharCode(255&r>>(-2*a&6)):0)i=o.indexOf(i);return l}var o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";n.prototype=new Error,n.prototype.name="InvalidCharacterError",e.exports="undefined"!=typeof window&&window.atob||r},function(e,t,n){var r=n(300);"string"==typeof r&&(r=[[e.id,r,""]]);n(265)(r,{});r.locals&&(e.exports=r.locals)},function(e,t,n){t=e.exports=n(264)(),t.push([e.id,"@import url(http://fonts.googleapis.com/css?family=Vollkorn:400,400italic,700,700italic&subset=latin);",""]),t.push([e.id,"*{padding:0;margin:0;box-sizing:border-box}html{font-size:120%}li{display:inline-block}a{color:#009a94;text-decoration:none}body{font:1rem Vollkorn,Microsoft YaHei,Palatino,Times;color:#333;line-height:1.4;text-align:justify;background-color:#f2f1f0}blockquote{margin-left:1rem;padding-left:1rem;border-left:1px solid #ddd}button:hover{cursor:pointer}button a{text-decoration:none;color:#344653;line-height:1.3;width:4rem;height:1.4rem;display:block;line-height:1.4rem;display:inline-block}button a:hover{background-color:#009a94;color:#fff;animation:_3MFYfM-0o7NrnVb8wCPZTR .5s;-moz-animation:_3MFYfM-0o7NrnVb8wCPZTR .5s;-webkit-animation:_3MFYfM-0o7NrnVb8wCPZTR .5s;-o-animation:_3MFYfM-0o7NrnVb8wCPZTR .5s}h1,h2,h3{font-weight:400}p{word-wrap:break-word;-webkit-hypens:auto;-moz-hypens:auto;hyphens:auto}._1nkgNk4InNjS0r1mBpNquA{display:flex;flex-direction:column;justify-content:space-between;min-height:110vh}@keyframes _3MFYfM-0o7NrnVb8wCPZTR{0%{background-color:#f2f1f0;color:#000}to{background-color:#009a94;color:#fff}}@-webkit-keyframes _3MFYfM-0o7NrnVb8wCPZTR{0%{background-color:#f2f1f0;color:#000}to{background-color:#009a94;color:#fff}}",""]),t.locals={btnAnimation:"_3MFYfM-0o7NrnVb8wCPZTR",container:"_1nkgNk4InNjS0r1mBpNquA"}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e},l=function(){function e(e,t){for(var n=0;ndiv li{display:block}._3WZbDt2y0hhVm4VtDpdWW3>div li:hover{text-decoration:underline}",""]),t.locals={archive:"_3WZbDt2y0hhVm4VtDpdWW3"}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;ndiv{display:flex}._69ew3bAI0ju8DD3P5mmEN button{margin:.5rem 0 .5rem 5.6rem}._2Q200-CAk3SQMmN3YDcde7{display:flex}._2Q200-CAk3SQMmN3YDcde7 button{margin-left:.5rem}._2Q200-CAk3SQMmN3YDcde7 li{margin:.5rem .5rem .5rem 0;color:#344653;border:1px solid #009a94;line-height:1.4;padding:0 .2rem}._2Q200-CAk3SQMmN3YDcde7 li:hover{cursor:default;box-shadow:0 0 2px #009a94}._2Q200-CAk3SQMmN3YDcde7 ul,._69ew3bAI0ju8DD3P5mmEN span{margin-left:5.6rem}._69ew3bAI0ju8DD3P5mmEN button{text-decoration:none;color:#344653;line-height:1.5;width:4rem;height:1.4rem;display:block;line-height:1.4rem}._69ew3bAI0ju8DD3P5mmEN button:hover{background-color:#009a94;color:#fff}.pi9NauyXPrDzyziIKF802{font-size:.8rem;margin-top:1rem}.pi9NauyXPrDzyziIKF802>div{width:25rem;border:1px solid #000;height:10rem;box-shadow:0 0 2px #344653;overflow:auto;padding:.5rem;font-size:.8rem;line-height:1.5}.pi9NauyXPrDzyziIKF802 h1,.pi9NauyXPrDzyziIKF802 h2,.pi9NauyXPrDzyziIKF802 h3,.pi9NauyXPrDzyziIKF802 h4,.pi9NauyXPrDzyziIKF802 h5,.pi9NauyXPrDzyziIKF802 h6{margin-bottom:.5rem;margin-top:.5rem}.pi9NauyXPrDzyziIKF802 li{display:list-item;margin:0;padding:0}.pi9NauyXPrDzyziIKF802 p{line-height:1rem}.pi9NauyXPrDzyziIKF802 ul{list-style:square;padding-left:1.2rem}.pi9NauyXPrDzyziIKF802 ol{padding-left:1.2rem}.pi9NauyXPrDzyziIKF802 pre{background-color:rgba(0,154,148,.7);color:#fff;padding:8px 10px;border-radius:.4em;-moz-border-radius:.4em;-webkit-border-radius:.4em;overflow-x:hidden}.pi9NauyXPrDzyziIKF802 code{background-color:#47b3af;color:#fff}.pi9NauyXPrDzyziIKF802 pre code{font-size:10pt;font-family:Consolas,Menlo,Monaco,monospace,serif}.pi9NauyXPrDzyziIKF802 blockquote{font-style:italic;margin:1rem;padding-left:1rem;border-left:3px solid #47b3af}.pi9NauyXPrDzyziIKF802 a{font-size:.9rem}.pi9NauyXPrDzyziIKF802 a:hover{text-decoration:underline}.pi9NauyXPrDzyziIKF802 img{width:100%;box-shadow:0 0 10px #009a94}",""]), 3 | t.locals={inputArea:"_69ew3bAI0ju8DD3P5mmEN",addTags:"_2Q200-CAk3SQMmN3YDcde7",md:"pi9NauyXPrDzyziIKF802"}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);tdiv{display:flex;flex-direction:column;justify-content:center;width:20rem;background-color:red;margin:5rem auto;padding:1rem;background-color:hsla(0,0%,93%,.9);line-height:1.5;box-shadow:0 0 2px #000}.qxtboXH9nmxHg5ZZt4-Ux>div input{width:10rem;border:1px solid #009a94;padding-left:.5rem}.qxtboXH9nmxHg5ZZt4-Ux>div li{margin:.5rem .5rem .5rem 0;color:#344653;border:1px solid #009a94;line-height:1.4;padding:0 .2rem}.qxtboXH9nmxHg5ZZt4-Ux>div li:hover{cursor:default;box-shadow:0 0 2px #009a94;text-decoration:line-through}.qxtboXH9nmxHg5ZZt4-Ux>div div{display:flex;justify-content:center}.qxtboXH9nmxHg5ZZt4-Ux>div div button{margin:0 .5rem}._26pMF2muCeToYfHQxfwcpq li:hover{background-color:red;color:#fff}",""]),t.locals={addTagsPop:"qxtboXH9nmxHg5ZZt4-Ux",isAdded:"_26pMF2muCeToYfHQxfwcpq"}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;n=2&&(this.actions.getTitles({type:"HOME",page:this.state.page-1}),this.setState({page:this.state.page-1}))}},{key:"onNextClick",value:function(){this.state.page<=this.state.length-1&&(this.actions.getTitles({type:"HOME",page:this.state.page+1}),this.setState({page:this.state.page+1}))}},{key:"onRenderSingle",value:function(){return this.state.articles===[]?"":void 0==this.state.articles[0]?"":void 0==this.state.articles[0].tags?"":this.state.articles.map(function(e,t){return s["default"].createElement("div",{key:t},s["default"].createElement("h2",null,s["default"].createElement(c.Link,{to:"/article/"+e.time.day+"/"+e.title},e.title)),s["default"].createElement("time",null,e.time.day),s["default"].createElement("ul",null,0==e.tags.length?"":e.tags.map(function(e,t){return s["default"].createElement("li",{key:t},e)})),s["default"].createElement("p",null,e.description))})}},{key:"render",value:function(){var e=this.state.page<=this.state.length-1?this.state.page+1:this.state.page,t=this.state.page>=2?this.state.page-1:this.state.page;return s["default"].createElement("div",{className:d["default"].home},this.onRenderSingle(),s["default"].createElement("div",null,s["default"].createElement("button",null,s["default"].createElement(c.Link,{to:"/page/"+t,onClick:this.onPreClick.bind(this)},"Pre")),s["default"].createElement("button",null,s["default"].createElement(c.Link,{to:"/page/"+e,onClick:this.onNextClick.bind(this)},"Next"))))}}]),t}(s["default"].Component);t["default"]=p},function(e,t,n){var r=n(314);"string"==typeof r&&(r=[[e.id,r,""]]);n(265)(r,{});r.locals&&(e.exports=r.locals)},function(e,t,n){t=e.exports=n(264)(),t.push([e.id,".cQIcNVvO6y5759rQwCpjq>div{margin:1rem 0 2rem}.cQIcNVvO6y5759rQwCpjq>div time{font-size:.8rem}.cQIcNVvO6y5759rQwCpjq>div li{margin-right:1rem;color:#009a94}.cQIcNVvO6y5759rQwCpjq button{margin-right:2rem}",""]),t.locals={home:"cQIcNVvO6y5759rQwCpjq"}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;n 2 | 3 | 4 | 5 | harryfyodor 6 | 7 | 8 |
    9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /model/comment.js: -------------------------------------------------------------------------------- 1 | var mongodb = require('./db') 2 | 3 | function Comment({ 4 | admin: admin, 5 | name: name, 6 | email: email, 7 | comment: comment, 8 | website: website 9 | }) { 10 | this.admin = admin 11 | this.name = name 12 | this.email = email 13 | this.comment = comment 14 | this.website = website 15 | } 16 | 17 | Comment.prototype.save = function(articleTitle, articleDay, callback) { 18 | var date = new Date() 19 | var minute = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + " " + 20 | date.getHours() + ":" + (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) 21 | var comment = { 22 | name: this.name, 23 | email: this.email, 24 | comment: this.comment, 25 | website: this.website, 26 | minute: minute, 27 | admin: this.admin 28 | } 29 | mongodb.open(function(err, db) { 30 | if(err) return callback(err) 31 | db.collection('posts', function(err, collection) { 32 | if(err) { 33 | mongodb.close() 34 | return callback(err) 35 | } 36 | collection.update({ 37 | "title": articleTitle, 38 | "time.day": articleDay 39 | }, { 40 | $push: { "comments":comment } 41 | }, function(err) { 42 | mongodb.close() 43 | if(err) return callback(err) 44 | return callback(null) 45 | }) 46 | }) 47 | }) 48 | } 49 | 50 | module.exports = Comment -------------------------------------------------------------------------------- /model/db.js: -------------------------------------------------------------------------------- 1 | var settings = require('../settings'), 2 | Db = require('mongodb').Db, 3 | Server = require('mongodb').Server; 4 | 5 | module.exports = new Db(settings.db, new Server(settings.host, settings.port), 6 | {safe: true}) -------------------------------------------------------------------------------- /model/post.js: -------------------------------------------------------------------------------- 1 | var mongodb = require('./db.js') 2 | 3 | function Post ({ 4 | title: title, 5 | description: description, 6 | content: content, 7 | tags: tags 8 | }) { 9 | this.title = title 10 | this.description = description 11 | this.content = content 12 | this.tags = tags 13 | } 14 | 15 | Post.prototype = { 16 | // save a post 17 | save: function(callback) { 18 | var date = new Date(); 19 | var time = { 20 | date: date, 21 | year : date.getFullYear(), 22 | month : date.getFullYear() + "-" + (date.getMonth() + 1), 23 | day : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(), 24 | minute : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + " " + 25 | date.getHours() + ":" + (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) 26 | }; 27 | var post = { 28 | title: this.title, 29 | time: time, 30 | tags: this.tags, 31 | description: this.description, 32 | content: this.content, 33 | comments: [] 34 | } 35 | // open db 36 | mongodb.open(function(err, db) { 37 | if(err) return callback(err) 38 | // open posts 39 | db.collection('posts', function(err, collection) { 40 | if(err) { 41 | mongodb.close() 42 | return callback(err) 43 | } 44 | // store the post 45 | collection.insert(post, { 46 | safe: true 47 | }, function(err) { 48 | mongodb.close(); 49 | if(err) return callback(err) 50 | return callback(null) 51 | }) 52 | }) 53 | }) 54 | } 55 | } 56 | 57 | Post.getTen = function(page, callback) { 58 | // open db 59 | mongodb.open(function(err, db) { 60 | if(err) return callback(err) 61 | // open posts 62 | db.collection('posts', function (err, collection) { 63 | if(err) { 64 | mongodb.close() 65 | return callback(err) 66 | } 67 | collection.count({}, function(err, count) { 68 | collection.find({}, { 69 | "title": 1, 70 | "description": 1, 71 | "time": 1, 72 | "tags": 1, 73 | skip: (page - 1)*10, 74 | limit: 10 75 | }).sort({ 76 | time: -1 77 | }).toArray(function(err, docs) { 78 | mongodb.close() 79 | if(err) return callback(err) 80 | return callback(null,docs,count) 81 | }) 82 | }) 83 | }) 84 | }) 85 | } 86 | 87 | Post.getTags = function(callback) { 88 | mongodb.open(function(err, db) { 89 | if (err) return callback(err) 90 | db.collection('posts', function(err, collection) { 91 | if (err) { 92 | mongodb.close() 93 | return callback(err) 94 | } 95 | collection.distinct('tags', function(err, docs) { 96 | mongodb.close() 97 | if (err) return callback(err) 98 | return callback(null, docs) 99 | }) 100 | }) 101 | }) 102 | } 103 | 104 | Post.getArchive = function(callback) { 105 | mongodb.open(function(err, db) { 106 | if(err) return callback(err) 107 | db.collection('posts', function(err, collection) { 108 | if(err) { 109 | mongodb.close() 110 | return callback(err) 111 | } 112 | collection.find({}, { 113 | "time": 1, 114 | "title": 1 115 | }).sort({ 116 | time: -1 117 | }).toArray(function(err, docs) { 118 | mongodb.close() 119 | if(err) return callback(err) 120 | return callback(null, docs) 121 | }) 122 | }) 123 | }) 124 | } 125 | 126 | Post.getTag = function(tagName, callback) { 127 | mongodb.open(function(err, db) { 128 | if(err) return callback(err) 129 | db.collection('posts', function(err, collection) { 130 | if(err) { 131 | mongodb.close() 132 | return callback(err) 133 | } 134 | collection.find({ 135 | "tags": tagName 136 | }, { 137 | "title": 1, 138 | "time": 1 139 | }).sort({ 140 | "time": -1 141 | }).toArray(function(err, docs) { 142 | mongodb.close() 143 | if(err) return callback(err) 144 | return callback(null, docs) 145 | }) 146 | }) 147 | }) 148 | } 149 | 150 | Post.search = function(keyword, callback) { 151 | mongodb.open(function(err, db) { 152 | if(err) return callback(err) 153 | db.collection('posts', function(err, collection) { 154 | if(err) { 155 | mongodb.close() 156 | return callback(err) 157 | } 158 | var pattern = new RegExp(keyword, "i") 159 | collection.find({ 160 | "title": pattern 161 | }, { 162 | "title": 1, 163 | "time": 1 164 | }).sort({ 165 | "time": -1 166 | }).toArray(function(err, docs) { 167 | mongodb.close(); 168 | if(err) return callback(err) 169 | return callback(null, docs) 170 | }) 171 | }) 172 | }) 173 | } 174 | 175 | Post.getOne = function(day, title, callback) { 176 | mongodb.open(function(err, db) { 177 | if(err) return callback(err) 178 | db.collection('posts', function(err, collection){ 179 | if(err) { 180 | mongodb.close() 181 | return callback(err) 182 | } 183 | collection.findOne({ 184 | "time.day": day, 185 | "title": title 186 | }, function(err, doc) { 187 | mongodb.close() 188 | if(err) { 189 | return callback(err) 190 | } 191 | return callback(null, doc) 192 | }) 193 | }) 194 | }) 195 | } 196 | 197 | Post.remove = function(day, title, callback) { 198 | mongodb.open(function(err, db) { 199 | if(err) return callback(err) 200 | db.collection('posts', function(err, collection) { 201 | if(err) { 202 | mongodb.close() 203 | return callback(err) 204 | } 205 | collection.findOne({ 206 | "time.day": day, 207 | "title": title 208 | }, function(err, doc) { 209 | if(err) { 210 | mongodb.close() 211 | return callback(err) 212 | } 213 | collection.remove({ 214 | "title": title, 215 | "time.day": day 216 | }, { 217 | w : 1 218 | }, function(err) { 219 | mongodb.close() 220 | if(err) return callback(err) 221 | return callback(null) 222 | }) 223 | }) 224 | }) 225 | }) 226 | } 227 | 228 | Post.edit = function(day, title, article, callback) { 229 | mongodb.open(function(err, db) { 230 | if(err) return callback(err) 231 | db.collection('posts', function(err, collection) { 232 | if(err) { 233 | mongodb.close() 234 | return callback(err) 235 | } 236 | collection.update({ 237 | "title": title, 238 | "time.day": day 239 | }, { 240 | $set: { 241 | title: article.title, 242 | tags: article.tags, 243 | content: article.content, 244 | description: article.description 245 | } 246 | }, function(err) { 247 | mongodb.close() 248 | if(err) return callback(err) 249 | return callback(null) 250 | }) 251 | }) 252 | }) 253 | } 254 | 255 | module.exports = Post 256 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "harry-blog", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "babel-core": "^6.10.4", 13 | "babel-loader": "^6.2.4", 14 | "babel-preset-es2015": "^6.9.0", 15 | "babel-preset-react": "^6.11.1", 16 | "babel-preset-react-hmre": "^1.1.1", 17 | "body-parser": "^1.15.2", 18 | "file-loader": "^0.9.0", 19 | "jwt-decode": "^2.0.1", 20 | "jwt-simple": "^0.5.0", 21 | "react-router-redux": "^4.0.5", 22 | "redux-router": "^2.0.0", 23 | "redux-thunk": "^2.1.0", 24 | "url-loader": "^0.5.7", 25 | "webpack": "^1.13.1", 26 | "webpack-dev-server": "^1.14.1", 27 | "webpack-hot-middleware": "^2.12.1" 28 | }, 29 | "dependencies": { 30 | "body-parser": "^1.15.2", 31 | "codemirror": "^5.16.0", 32 | "css-loader": "^0.23.1", 33 | "draft-js": "^0.7.0", 34 | "ejs": "^2.4.2", 35 | "express": "^4.14.0", 36 | "express-jwt": "^3.4.0", 37 | "isomorphic-fetch": "^2.2.1", 38 | "json-loader": "^0.5.4", 39 | "marked": "^0.3.5", 40 | "mongodb": "^2.2.2", 41 | "mongoose": "^4.5.3", 42 | "mongoose-timestamp": "^0.6.0", 43 | "react": "^15.2.1", 44 | "react-dom": "^15.2.1", 45 | "react-markdown": "^2.4.2", 46 | "react-redux": "^4.4.5", 47 | "react-router": "^2.5.2", 48 | "redux": "^3.5.2", 49 | "redux-logger": "^2.6.1", 50 | "redux-promise": "^0.5.3", 51 | "style-loader": "^0.13.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var jwt = require('jwt-simple') 2 | var password = require('../data/user.js').password 3 | // var articles = require('../data/articles.js').articles 4 | // var tags = require('../data/articles.js').tags 5 | var Post = require('../model/post.js') 6 | var Comment = require('../model/comment.js') 7 | 8 | var SECRET = "SECRET" 9 | 10 | // login 11 | exports.login = function(req, res) { 12 | req.setMaxListeners(0) 13 | var payload = req.body 14 | if(payload.password === password) { 15 | console.log("Correct password") 16 | var token = jwt.encode(payload, SECRET) 17 | res.json({ token: token }) 18 | } 19 | } 20 | 21 | // check whether it is right 22 | exports.check = function(req, res) { 23 | req.setMaxListeners(0) 24 | var token = req.body.token 25 | var decoded = jwt.decode(token, SECRET) 26 | if(decoded.password === password) { 27 | console.log("matched!") 28 | res.json({ match: true }) 29 | } else { 30 | res.json({ match: false }) 31 | } 32 | } 33 | 34 | function sendTitles(err, posts, res, count){ 35 | if(err) { 36 | posts = [] 37 | } 38 | res.send({ 39 | articles: posts, 40 | ok: true, 41 | count: count 42 | }) 43 | } 44 | 45 | // home, archive, tagsResult, searchResult 46 | exports.titles = function(req, res) { 47 | req.setMaxListeners(0) 48 | switch(req.body.type) { 49 | case "HOME": 50 | Post.getTen(req.body.page, function(err, posts, count) { 51 | sendTitles(err, posts, res, count) 52 | }) 53 | break 54 | case "ARCHIVE": 55 | Post.getArchive(function(err, posts) { 56 | sendTitles(err, posts, res) 57 | }) 58 | break 59 | case "TAGS": 60 | Post.getTag(req.body.tagName, function(err, posts) { 61 | sendTitles(err, posts, res) 62 | }) 63 | break 64 | case "SEARCH": 65 | Post.search(req.body.searchString, function(err, posts) { 66 | sendTitles(err, posts, res) 67 | }) 68 | break 69 | default: 70 | res.send({ 71 | ok: false 72 | }) 73 | break 74 | } 75 | } 76 | 77 | exports.tags = function(req, res) { 78 | req.setMaxListeners(0) 79 | Post.getTags(function(err, docs) { 80 | if(err) { 81 | docs = [] 82 | } else { 83 | res.send({ 84 | ok: true, 85 | tags: docs 86 | }) 87 | } 88 | }) 89 | } 90 | 91 | exports.upload = function(req, res) { 92 | req.setMaxListeners(0) 93 | var token = req.headers['authorization'] 94 | if(!token) { 95 | // have no token 96 | res.sendStatus(401) 97 | } else { 98 | try { 99 | var decoded = jwt.decode(token.replace('Bearer ',''), SECRET) 100 | // the password is correct 101 | if(decoded.password === password) { 102 | // new article 103 | var article = req.body.article 104 | // articles.push(newArticle) 105 | var newArticle = new Post({ 106 | title: article.title, 107 | tags: article.tags, 108 | description: article.description, 109 | content: article.content 110 | }) 111 | newArticle.save(function(err) { 112 | if(err) { 113 | res.json({ok:false}) 114 | } else { 115 | res.json({ok:true}) 116 | } 117 | }) 118 | } 119 | } catch (e) { 120 | res.sendStatus(401) 121 | } 122 | } 123 | } 124 | 125 | exports.edit = function(req, res) { 126 | req.setMaxListeners(0) 127 | var token = req.headers['authorization'] 128 | if(!token) { 129 | // have no token 130 | res.sendStatus(401) 131 | } else { 132 | try { 133 | var decoded = jwt.decode(token.replace('Bearer ',''), SECRET) 134 | // the password is correct 135 | if(decoded.password === password) { 136 | // edit article 137 | var article = req.body.article 138 | // articles.push(newArticle) 139 | var newArticle = { 140 | title: article.title, 141 | tags: article.tags, 142 | description: article.description, 143 | content: article.content 144 | } 145 | Post.edit(req.body.oldDay, req.body.oldTitle, newArticle, function(err) { 146 | if(err) { 147 | res.send({ 148 | ok: false 149 | }) 150 | } else { 151 | res.send({ 152 | ok: true 153 | }) 154 | } 155 | }) 156 | } 157 | } catch (e) { 158 | res.sendStatus(401) 159 | } 160 | } 161 | } 162 | 163 | exports.single = function(req, res) { 164 | req.setMaxListeners(0) 165 | var day = req.body.day 166 | var title = req.body.title 167 | Post.getOne(day, title, function(err, article) { 168 | if(err) { 169 | res.send({ 170 | ok: false 171 | }) 172 | } else { 173 | res.send({ 174 | ok: true, 175 | article: article 176 | }) 177 | } 178 | }) 179 | } 180 | 181 | exports.remove = function(req, res) { 182 | req.setMaxListeners(0) 183 | var day = req.body.day 184 | var title = req.body.title 185 | Post.remove(day, title, function(err) { 186 | if(err) { 187 | res.send({ok: false}) 188 | } else { 189 | res.send({ok: true}) 190 | } 191 | }) 192 | } 193 | 194 | exports.addComment = function(req, res) { 195 | req.setMaxListeners(0) 196 | var articleTitle = req.body.articleTitle 197 | var articleDay = req.body.articleDay 198 | var newComment = new Comment({ 199 | name: req.body.comment.name, 200 | email: req.body.comment.email, 201 | comment: req.body.comment.comment, 202 | website: req.body.comment.website, 203 | admin: req.body.comment.admin 204 | }) 205 | newComment.save(articleTitle, articleDay, function(err) { 206 | if(err) { 207 | res.send({ 208 | ok: false 209 | }) 210 | } else { 211 | res.send({ 212 | ok: true 213 | }) 214 | } 215 | }) 216 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var environment = process.env.NODE_ENV 2 | var webpack = require('webpack') 3 | var bodyParser = require('body-parser') 4 | var path = require('path') 5 | var port = 3000 6 | var app = new (require('express'))() 7 | var router = require('./routes/index') 8 | 9 | var config 10 | 11 | if(environment === 'production') { 12 | config = require('./webpack.config') 13 | } else if (environment === 'development') { 14 | config = require('./webpack.config.dev') 15 | } else { 16 | console.log('Please define NODE_ENV first\nlinux & mac: export NODE_ENV=production(or development)\nwindows: set NODE_ENV=production(or development\n)') 17 | } 18 | 19 | var compiler = webpack(config) 20 | 21 | if(environment === 'development') { 22 | var webpackHotMiddleware = require('webpack-hot-middleware') 23 | app.use(webpackHotMiddleware(compiler)) 24 | } 25 | var webpackDevMiddleware = require('webpack-dev-middleware') 26 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 27 | 28 | // body-parser 29 | // parse application/x-www-form-urlencoded 30 | app.use(bodyParser.urlencoded({ extended: false })) 31 | 32 | // parse application/json 33 | app.use(bodyParser.json()) 34 | 35 | app.get("*", function(req, res) { 36 | res.sendFile(path.join(__dirname, 'index.html')) 37 | }) 38 | 39 | app.post("/api/login", router.login) 40 | app.post("/api/check", router.check) 41 | app.post("/api/titles", router.titles) 42 | app.post("/api/tags", router.tags) 43 | app.post("/api/upload", router.upload) 44 | app.post("/api/single", router.single) 45 | app.post("/api/edit", router.edit) 46 | app.post("/api/remove", router.remove) 47 | app.post("/api/comment", router.addComment) 48 | 49 | app.listen(port, function(error) { 50 | if (error) { 51 | console.error(error) 52 | } else { 53 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 54 | } 55 | }) -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: 'harry', 3 | host: 'localhost', 4 | port: 27017 5 | } -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './app/client' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurrenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders : [ 21 | { 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | }, 27 | { 28 | test: /\.(png|jpg)$/, 29 | loader: 'url-loader?limit=8192' 30 | }, 31 | { 32 | test: /\.css$/, 33 | loader: "style-loader!css-loader?modules" 34 | } 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: './app/client', 7 | vendor: ['react', 8 | 'react-router', 9 | 'redux', 10 | 'react-dom', 11 | 'react-redux', 12 | 'marked' 13 | ] 14 | }, 15 | output: { 16 | path: path.join(__dirname, 'dist'), 17 | filename: 'bundle.js', 18 | publicPath: '/static/' 19 | }, 20 | plugins: [ 21 | new webpack.optimize.UglifyJsPlugin({ 22 | compress: { 23 | warnings: false 24 | } 25 | }), 26 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), 27 | new webpack.DefinePlugin({ 28 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 29 | }) 30 | ], 31 | module: { 32 | loaders : [ 33 | { 34 | test: /\.js$/, 35 | loaders: [ 'babel' ], 36 | exclude: /node_modules/, 37 | include: __dirname 38 | }, 39 | { 40 | test: /\.(png|jpg)$/, 41 | loader: 'url-loader?limit=8192' 42 | }, 43 | { 44 | test: /\.css$/, 45 | loader: "style-loader!css-loader?modules" 46 | } 47 | ] 48 | } 49 | } --------------------------------------------------------------------------------