├── 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 | 
19 | * 文章写作页面
20 | 
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 |
130 | {tags.length === 0 ? "(EMPTY)":
131 | tags.map((t, i) =>
132 | - {t}
)}
133 |
134 |
135 |
138 |
139 |
140 | {allTags.length === 0 ? "(EMPTY)" :
141 | allTags.map((e, i) =>
142 | - {e}
)}
143 |
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 |
52 | {as.map((a, j) =>
53 | - {`${j+1}. ${a.title}`}
54 | )}
55 |
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 | : }
34 |
35 | {c.comment}
36 |
37 | )
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
Comments:
44 |
45 | {this.renderAComment()}
46 |
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 |
68 | {a.tags.length == 0 ? "" :
69 | a.tags.map((t, j) => - {t}
)
70 | }
71 |
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 |
97 | {tags.length === 0 ? "" :
98 | tags.map((t, i) => - {t}
)}
99 |
100 |
101 |
102 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
114 |
115 |
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 |
49 | {tags.map((t, i) =>
50 | - {t}
52 | )}
53 |
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 |
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 | }
--------------------------------------------------------------------------------