├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── assets │ └── duck.jpg ├── components │ ├── Button │ │ └── index.js │ ├── Grid │ │ ├── Row.js │ │ ├── index.js │ │ └── Col.js │ ├── ScrollToTop │ │ └── index.js │ ├── PointOutContent │ │ └── index.js │ ├── Footer │ │ └── index.js │ ├── ArticleDirectory │ │ ├── Styled.js │ │ └── index.js │ ├── Blog │ │ ├── Styled.js │ │ ├── index.js │ │ └── markdown.css │ ├── Container │ │ └── index.js │ ├── Loader │ │ └── index.js │ ├── ContentLoader │ │ └── index.js │ ├── BlogList │ │ ├── Styled.js │ │ └── index.js │ ├── Tag │ │ └── index.js │ ├── ResumeContent │ │ ├── index.js │ │ └── markdown.css │ ├── TopNav │ │ └── index.js │ └── ProfileCard │ │ └── index.js ├── reducers │ ├── index.js │ ├── blog.js │ ├── resume.js │ ├── profile.js │ └── blogList.js ├── index.js ├── reboot.css ├── store.js ├── constants │ └── index.js ├── App.js ├── utils │ └── index.js └── registerServiceWorker.js ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── paths.js ├── env.js ├── webpackDevServer.config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .gitignore ├── scripts ├── test.js ├── start.js └── build.js ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaleGZL/zalegzl-website/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/duck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaleGZL/zalegzl-website/HEAD/src/assets/duck.jpg -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Button = styled.button` 5 | background-color: red; 6 | ` 7 | 8 | export default Button 9 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import blogList from './blogList' 3 | import blog from './blog' 4 | import resume from './resume' 5 | import profile from './profile' 6 | 7 | export default combineReducers({ 8 | blogList, 9 | blog, 10 | resume, 11 | profile 12 | }) 13 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/components/Grid/Row.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Row = styled.div` 4 | /* 清除浮动 */ 5 | &::after { 6 | content: ''; 7 | clear: both; 8 | display: table; 9 | } 10 | 11 | /* 是网格布局具有相同的高度 */ 12 | display: -webkit-box; 13 | display: -webkit-flex; 14 | display: -ms-flexbox; 15 | display: flex; 16 | ` 17 | 18 | export default Row 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ZALE_BLOG", 3 | "name": "guozeling's blog", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/Grid/index.js: -------------------------------------------------------------------------------- 1 | import Row from './Row' 2 | import Col from './Col' 3 | 4 | const Grid = { 5 | Row, 6 | Col 7 | } 8 | 9 | // 网格布局 API 10 | // Row (行) 11 | 12 | // Col (列) 元素所占用12格中的多少格 13 | // xs: < 576px or 适配所有屏幕宽度 14 | // sm: >= 576px 15 | // md: >= 768px 16 | // lg: >= 992px 17 | // xl: >= 1200px 18 | // gutter: left and right padding (default 15px) 19 | 20 | export default Grid 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { BrowserRouter as Router } from 'react-router-dom' 5 | import store from './store' 6 | import App from './App' 7 | // import registerServiceWorker from './registerServiceWorker' 8 | // registerServiceWorker() 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ) 18 | -------------------------------------------------------------------------------- /src/components/ScrollToTop/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Styled from 'styled-components' 3 | import ScrollToTop from 'react-scroll-up' 4 | const StyledScrollUp = Styled.div` 5 | font-size: 30px; 6 | color: #1c93e0; 7 | &:hover { 8 | opacity: 0.6; 9 | } 10 | ` 11 | 12 | const ScrollToTopCustom = () => ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | 20 | export default ScrollToTopCustom -------------------------------------------------------------------------------- /src/components/PointOutContent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Styled from 'styled-components' 4 | 5 | const PointOutContentContainer = Styled.div` 6 | margin-top: 20px; 7 | text-align: center; 8 | font-size: 1.4rem; 9 | color: rgba(0, 0, 0, 0.65); 10 | ` 11 | 12 | const PointOutContent = ({ text }) => ( 13 | {text} 14 | ) 15 | 16 | PointOutContent.proptypes = { 17 | text: PropTypes.string 18 | } 19 | 20 | export default PointOutContent 21 | -------------------------------------------------------------------------------- /src/reboot.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 12px; 3 | font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; 4 | word-wrap: break-word; 5 | color: #333; 6 | background-color: #F4F5F5; 7 | position: relative; 8 | min-height: 100%; 9 | } 10 | 11 | *, 12 | *::before, 13 | *::after { 14 | box-sizing: border-box; 15 | } 16 | 17 | body { 18 | margin: 0 0 100px; 19 | padding-top: 50px; 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | cursor: pointer; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Styled from 'styled-components' 3 | 4 | const StyledFooter = Styled.div` 5 | padding: 60px 0 10px; 6 | text-align: center; 7 | font-size: 1.3rem; 8 | font-weight: 500; 9 | line-height: 2; 10 | color: #3c3b3b; 11 | 12 | position: absolute; 13 | left: 0; 14 | bottom: 0; 15 | height: 100px; 16 | width: 100%; 17 | ` 18 | 19 | const Footer = () => ( 20 | 21 | @2018   ZALE 22 | 23 | ) 24 | 25 | export default Footer 26 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import rootReducer from './reducers' 4 | 5 | const initialState = {} 6 | const middlewares = [thunk] 7 | const enhancers = [] 8 | 9 | if (process.env.NODE_ENV === 'development') { 10 | const devToolsExtension = window.devToolsExtension 11 | 12 | if (typeof devToolsExtension === 'function') { 13 | enhancers.push(devToolsExtension()) 14 | } 15 | } 16 | 17 | const composedEnhancers = compose(applyMiddleware(...middlewares), ...enhancers) 18 | 19 | export default createStore(rootReducer, initialState, composedEnhancers) 20 | -------------------------------------------------------------------------------- /src/components/ArticleDirectory/Styled.js: -------------------------------------------------------------------------------- 1 | import Styled from 'styled-components' 2 | 3 | export const ArticleDirectoryContainer = Styled.div` 4 | background-color: #fff; 5 | box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.2); 6 | color: #636363; 7 | font-weight: 300; 8 | 9 | position: -webkit-sticky; 10 | position: sticky; 11 | top: 0; 12 | ` 13 | 14 | export const Title = Styled.div` 15 | font-size: 1.25rem; 16 | font-weight: 400; 17 | text-align: center; 18 | border-bottom: 1px solid #e8e8e8; 19 | padding: 10px 0; 20 | ` 21 | 22 | export const Content = Styled.div` 23 | padding: 10px 0; 24 | font-size: 1.3rem; 25 | padding-right: 15px; 26 | 27 | & a { 28 | color: #636363; 29 | } 30 | & a:hover, & a:active { 31 | color: #303030; 32 | font-weight: 400; 33 | } 34 | ` 35 | -------------------------------------------------------------------------------- /src/components/ArticleDirectory/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import * as Styled from './Styled' 4 | 5 | class ArticleDirectory extends Component { 6 | render() { 7 | const toc = this.props.toc 8 | return ( 9 | 10 | 文章目录 11 | {toc ? ( 12 | 13 |
18 | 19 | ) : null} 20 | 21 | ) 22 | } 23 | } 24 | 25 | const mapStateToProps = state => ({ 26 | toc: state.blog.data.toc 27 | }) 28 | 29 | export default connect(mapStateToProps)(ArticleDirectory) 30 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /src/components/Blog/Styled.js: -------------------------------------------------------------------------------- 1 | import Styled from 'styled-components' 2 | 3 | export const BlogContainer = Styled.div` 4 | background-color: #fff; 5 | margin-bottom: 1rem; 6 | box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.2); 7 | 8 | color: rgba(51, 51, 51, 0.9); 9 | ` 10 | 11 | export const InfoContainer = Styled.div` 12 | padding: 1.1rem 1.3rem 1rem 1.3rem; 13 | /* border-bottom: 1px solid #e8e8e8; */ 14 | text-align: center; 15 | ` 16 | 17 | export const Info = Styled.span` 18 | padding-right: 1.6rem; 19 | padding-bottom: 0.3rem; 20 | 21 | font-size: 1.1rem; 22 | font-weight: 400; 23 | line-height: 1.5; 24 | color: rgba(0, 0, 0, 0.45); 25 | ` 26 | 27 | export const Title = Styled.div` 28 | font-size: 1.8rem; 29 | padding-bottom: 1.3rem; 30 | ` 31 | 32 | export const BlogContent = Styled.div` 33 | padding: 1rem 1.3rem 2rem 1.3rem; 34 | ` 35 | -------------------------------------------------------------------------------- /src/components/Container/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import styled, { css } from 'styled-components' 3 | import { getPixelString } from '../../utils' 4 | 5 | const getMaxWidth = css` 6 | @media only screen and (min-width: 576px) { 7 | max-width: 540px; 8 | } 9 | 10 | @media only screen and (min-width: 768px) { 11 | max-width: 720px; 12 | } 13 | 14 | @media only screen and (min-width: 992px) { 15 | max-width: 960px; 16 | } 17 | 18 | @media only screen and (min-width: 1200px) { 19 | max-width: 1140px; 20 | } 21 | ` 22 | 23 | const Container = styled.div` 24 | width: 100%; 25 | padding: 0 ${({ lrPadding }) => getPixelString(lrPadding, 15)}; 26 | margin: 0 auto; 27 | 28 | ${({ fluid }) => (fluid ? undefined : getMaxWidth)}; 29 | ` 30 | 31 | Container.propTypes = { 32 | lrPadding: PropTypes.number, 33 | fluid: PropTypes.bool 34 | } 35 | 36 | export default Container 37 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | // 博客列表每页的数目 2 | const blogListPerPageCount = 5 3 | const activePaginationClassName = 'active-pagination-link' 4 | const blogListQs = 'fields=_id,title,summary,createTime,viewTimes,tags,category' 5 | const blogQs = 6 | 'fields=title,summary,createTime,viewTimes,tags,category,htmlContent,toc' 7 | const apiPrefix = '/api' 8 | 9 | const navList = [ 10 | { 11 | key: 'home', 12 | to: '/blogs', 13 | name: '首页', 14 | icon: 'fas fa-home', 15 | exact: false 16 | }, 17 | { 18 | key: 'categories', 19 | to: '/categories', 20 | name: '分类', 21 | icon: 'fas fa-th', 22 | exact: false 23 | }, 24 | { 25 | key: 'tags', 26 | to: '/tags', 27 | name: '标签', 28 | icon: 'fas fa-tags', 29 | exact: false 30 | }, 31 | { 32 | key: 'about', 33 | to: '/about', 34 | name: '关于', 35 | icon: 'fas fa-user', 36 | exact: false 37 | } 38 | ] 39 | 40 | export { 41 | blogListPerPageCount, 42 | activePaginationClassName, 43 | blogListQs, 44 | navList, 45 | apiPrefix, 46 | blogQs 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Grid/Col.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import PropTypes from 'prop-types' 3 | import { getPixelString } from '../../utils' 4 | 5 | const getWidthString = span => { 6 | if (span === undefined) { 7 | return undefined 8 | } else if (span === 0) { 9 | return 'display: none;' 10 | } 11 | 12 | const width = span / 12 * 100 13 | return `width: ${width}%;` 14 | } 15 | 16 | const Col = styled.div` 17 | float: left; 18 | padding: 0 ${({ gutter }) => getPixelString(gutter, 15)}; 19 | 20 | @media only screen and (min-width: 1px) { 21 | display: block; 22 | width: 100%; 23 | ${({ xs }) => getWidthString(xs)}; 24 | } 25 | 26 | @media only screen and (min-width: 576px) { 27 | display: block; 28 | ${({ sm }) => getWidthString(sm)}; 29 | } 30 | 31 | @media only screen and (min-width: 768px) { 32 | display: block; 33 | ${({ md }) => getWidthString(md)}; 34 | } 35 | 36 | @media only screen and (min-width: 992px) { 37 | display: block; 38 | ${({ lg }) => getWidthString(lg)}; 39 | } 40 | 41 | @media only screen and (min-width: 1200px) { 42 | display: block; 43 | ${({ xl }) => getWidthString(xl)}; 44 | } 45 | ` 46 | 47 | Col.propTypes = { 48 | xs: PropTypes.number, 49 | sm: PropTypes.number, 50 | md: PropTypes.number, 51 | lg: PropTypes.number, 52 | xl: PropTypes.number, 53 | gutter: PropTypes.number 54 | } 55 | 56 | export default Col 57 | -------------------------------------------------------------------------------- /src/reducers/blog.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { apiPrefix } from '../constants' 3 | 4 | const types = { 5 | // 获取博客信息 6 | START_GET_BLOG: 'blog/START_GET_BLOG', 7 | SUCCESS_GET_BLOG: 'blog/SUCCESS_GET_BLOG', 8 | FAILURE_GET_BLOG: 'blog/FAILURE_GET_BLOG' 9 | } 10 | 11 | const initState = { 12 | status: '', 13 | data: {} 14 | } 15 | 16 | export default (state = initState, action) => { 17 | switch (action.type) { 18 | case types.START_GET_BLOG: 19 | return { 20 | ...state, 21 | status: 'pending' 22 | } 23 | case types.SUCCESS_GET_BLOG: 24 | return { 25 | status: 'success', 26 | data: action.data 27 | } 28 | case types.FAILURE_GET_BLOG: 29 | return { 30 | ...state, 31 | status: 'failure' 32 | } 33 | default: 34 | return state 35 | } 36 | } 37 | 38 | const startGetBlog = () => ({ 39 | type: types.START_GET_BLOG 40 | }) 41 | 42 | const successGetBlog = blog => ({ 43 | type: types.SUCCESS_GET_BLOG, 44 | data: blog 45 | }) 46 | 47 | const failureGetBlog = () => ({ 48 | type: types.FAILURE_GET_BLOG 49 | }) 50 | 51 | const requestGetBlog = id => dispatch => { 52 | dispatch(startGetBlog()) 53 | 54 | // 发起请求 55 | return axios 56 | .get(`${apiPrefix}/blogs/${id}`) 57 | .then(response => { 58 | const blog = response.data.data 59 | dispatch(successGetBlog(blog)) 60 | return 'success' 61 | }) 62 | .catch(error => { 63 | console.log(error.response) 64 | dispatch(failureGetBlog()) 65 | return 'failure' 66 | }) 67 | } 68 | 69 | export { requestGetBlog } 70 | -------------------------------------------------------------------------------- /src/reducers/resume.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { apiPrefix } from '../constants' 3 | 4 | const types = { 5 | START_GET_RESUME: 'resume/START_GET_RESUME', 6 | SUCCESS_GET_RESUME: 'resume/SUCCESS_GET_RESUME', 7 | FAILURE_GET_RESUME: 'resume/FAILURE_GET_RESUME' 8 | } 9 | 10 | const initState = { 11 | status: '', 12 | htmlContent: '' 13 | } 14 | 15 | export default (state = initState, action) => { 16 | switch (action.type) { 17 | case types.START_GET_RESUME: 18 | return { 19 | ...state, 20 | status: 'pending' 21 | } 22 | case types.SUCCESS_GET_RESUME: 23 | return { 24 | htmlContent: action.data, 25 | status: 'success' 26 | } 27 | case types.FAILURE_GET_RESUME: 28 | return { 29 | ...state, 30 | status: 'failure' 31 | } 32 | default: 33 | return state 34 | } 35 | } 36 | 37 | const startGetResume = () => ({ 38 | type: types.START_GET_RESUME 39 | }) 40 | 41 | const successGetResume = htmlContent => ({ 42 | type: types.SUCCESS_GET_RESUME, 43 | data: htmlContent 44 | }) 45 | 46 | const failureGetResume = () => ({ 47 | type: types.FAILURE_GET_RESUME 48 | }) 49 | 50 | const requestGetResume = () => dispatch => { 51 | dispatch(startGetResume()) 52 | 53 | return axios 54 | .get(`${apiPrefix}/resumes`) 55 | .then(response => { 56 | const { htmlContent } = response.data.data 57 | dispatch(successGetResume(htmlContent)) 58 | return 'success' 59 | }) 60 | .catch(error => { 61 | console.log(error.response) 62 | dispatch(failureGetResume()) 63 | return 'failure' 64 | }) 65 | } 66 | 67 | export { requestGetResume } 68 | -------------------------------------------------------------------------------- /src/components/Loader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | import { getPixelString } from '../../utils' 5 | 6 | const StyledLoaderContainer = styled.div` 7 | width: 100%; 8 | min-height: ${({ height }) => getPixelString(height, null)}; 9 | padding-top: ${({ paddingTB }) => getPixelString(paddingTB, null)}; 10 | padding-bottom: ${({ paddingTB }) => getPixelString(paddingTB, null)}; 11 | ` 12 | 13 | const StyledLoader = styled.div` 14 | border: 10px solid #fff; 15 | border-radius: 50%; 16 | border-top: 10px solid #3498db; 17 | border-bottom: 10px solid #3498db; 18 | margin: 0 auto; 19 | width: ${({ size }) => getPixelString(size, null)}; 20 | height: ${({ size }) => getPixelString(size, null)}; 21 | -webkit-animation: spin 2s linear infinite; /* Safari */ 22 | animation: spin 2s linear infinite; 23 | 24 | /* Safari */ 25 | @-webkit-keyframes spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | } 29 | 100% { 30 | -webkit-transform: rotate(360deg); 31 | } 32 | } 33 | 34 | @keyframes spin { 35 | 0% { 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | transform: rotate(360deg); 40 | } 41 | } 42 | ` 43 | 44 | const Loader = ({ minHeight, size }) => { 45 | const paddingTB = (minHeight - size) / 2 46 | return ( 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | Loader.propTypes = { 54 | minHeight: PropTypes.number.isRequired, 55 | size: PropTypes.number.isRequired 56 | } 57 | 58 | export default Loader 59 | -------------------------------------------------------------------------------- /src/components/ContentLoader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Styled from 'styled-components' 4 | 5 | const StyledContentLoaderContaner = Styled.div` 6 | background-color: #fff; 7 | height: ${({ height }) => height + 'px'}; 8 | border: 1px solid #fff; 9 | padding: 0 20px; 10 | margin-bottom: 20px; 11 | overflow: hidden; 12 | ` 13 | 14 | const StyledContentContainer = Styled.div` 15 | box-sizing: border-box; 16 | margin-top: 20px; 17 | margin-bottom: 20px; 18 | width: ${({ width }) => width + '%'}; 19 | height: 25px; 20 | ` 21 | 22 | const StyledContent = Styled.div` 23 | width: 100%; 24 | height: 100%; 25 | background-color: #f1f1f1; 26 | ` 27 | 28 | const getWidthArrayByHeight = height => { 29 | const number = Math.floor(height / 50) 30 | const arr = [] 31 | for (let i = 0; i < number; i++) { 32 | arr.push(Math.floor(Math.random() * 90 + 10)) 33 | } 34 | return arr 35 | } 36 | 37 | class ContentLoader extends React.Component { 38 | constructor(props) { 39 | super(props) 40 | 41 | const height = this.props.height 42 | this.state = { 43 | height, 44 | WidthArrayByHeight: getWidthArrayByHeight(height) 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 | {this.state.WidthArrayByHeight.map((width, index) => ( 51 | 52 | 53 | 54 | ))} 55 | ) 56 | } 57 | } 58 | 59 | 60 | ContentLoader.propTypes = { 61 | height: PropTypes.number 62 | } 63 | 64 | export default ContentLoader -------------------------------------------------------------------------------- /src/reducers/profile.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { apiPrefix } from '../constants' 3 | 4 | const types = { 5 | START_GET_PROFILE_INFO: 'profile/START_GET_PROFILE_INFO', 6 | SUCCESS_GET_PROFILE_INFO: 'profile/SUCCESS_GET_PROFILE_INFO', 7 | FAILURE_GET_PROFILE_INFO: 'profile/FAILURE_GET_PROFILE_INFO' 8 | } 9 | 10 | const initState = { 11 | status: '', 12 | blogCount: 0, 13 | tagCount: 0, 14 | categoryCount: 0 15 | } 16 | 17 | export default (state = initState, action) => { 18 | switch (action.type) { 19 | case types.START_GET_PROFILE_INFO: 20 | return { 21 | ...state, 22 | status: 'pending' 23 | } 24 | case types.SUCCESS_GET_PROFILE_INFO: 25 | return { 26 | status: 'success', 27 | ...action.data 28 | } 29 | case types.FAILURE_GET_PROFILE_INFO: 30 | return { 31 | ...state, 32 | status: 'failure' 33 | } 34 | default: 35 | return state 36 | } 37 | } 38 | 39 | const startGetProfileInfo = () => ({ 40 | type: types.START_GET_PROFILE_INFO 41 | }) 42 | 43 | const successGetProfileInfo = data => ({ 44 | type: types.SUCCESS_GET_PROFILE_INFO, 45 | data 46 | }) 47 | 48 | const failureGetProfileInfo = () => ({ 49 | type: types.FAILURE_GET_PROFILE_INFO 50 | }) 51 | 52 | const requestGetProfileInfo = () => dispatch => { 53 | dispatch(startGetProfileInfo()) 54 | 55 | return axios 56 | .get(`${apiPrefix}/profiles`) 57 | .then(response => { 58 | const data = response.data.data 59 | dispatch(successGetProfileInfo(data)) 60 | return 'success' 61 | }) 62 | .catch(error => { 63 | console.log(error.response) 64 | dispatch(failureGetProfileInfo()) 65 | return 'failure' 66 | }) 67 | } 68 | 69 | export { requestGetProfileInfo } 70 | -------------------------------------------------------------------------------- /src/components/BlogList/Styled.js: -------------------------------------------------------------------------------- 1 | import Styled from 'styled-components' 2 | import { activePaginationClassName } from '../../constants' 3 | 4 | export const Container = Styled.div` 5 | background-color: #fff; 6 | padding: 1.1rem 1.3rem; 7 | margin-bottom: 1rem; 8 | 9 | box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.2); 10 | ` 11 | 12 | export const Title = Styled.h4` 13 | margin: 0.3rem 0 0.7rem 0; 14 | overflow: hidden; 15 | 16 | font-size: 1.4rem; 17 | font-weight: 500; 18 | line-height: 1.2; 19 | 20 | & a:hover, 21 | & a:active { 22 | color: #1890ff; 23 | } 24 | 25 | & a { 26 | color: rgba(0, 0, 0, 0.65) 27 | } 28 | ` 29 | 30 | export const Summary = Styled.div` 31 | font-size: 1.15rem; 32 | font-weight: 400; 33 | line-height: 1.6; 34 | color: rgba(0, 0, 0, 0.45); 35 | margin-bottom: 0.5rem; 36 | ` 37 | 38 | export const InfoContainer = Styled.div` 39 | /* 清除浮动 */ 40 | &::after { 41 | content: ''; 42 | clear: both; 43 | display: table; 44 | } 45 | ` 46 | 47 | export const Info = Styled.div` 48 | float: left; 49 | display: block; 50 | padding-right: 1.6rem; 51 | padding-bottom: 0.3rem; 52 | 53 | font-size: 1.1rem; 54 | font-weight: 400; 55 | line-height: 1.5; 56 | color: rgba(0, 0, 0, 0.45); 57 | ` 58 | 59 | export const Pagination = Styled.div` 60 | padding: 10px 0; 61 | text-align: center; 62 | 63 | & a { 64 | color: black; 65 | display: inline-block; 66 | padding: 8px 16px; 67 | text-decoration: none; 68 | transition: background-color 0.3s; 69 | border: 1px solid #ddd; 70 | margin: 0 1px; 71 | } 72 | 73 | & a:hover:not(.${activePaginationClassName}) { 74 | background-color: #ddd; 75 | } 76 | 77 | & a.${activePaginationClassName} { 78 | color: white; 79 | background-color: #1e90ff; 80 | } 81 | ` 82 | -------------------------------------------------------------------------------- /src/reducers/blogList.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { blogListQs, apiPrefix } from '../constants' 3 | 4 | const types = { 5 | // 获取博客列表 6 | START_GET_BLOG_LIST: 'blogList/START_GET_BLOG_LIST', 7 | SUCCESS_GET_BLOG_LIST: 'blogList/SUCCESS_GET_BLOG_LIST', 8 | FAILURE_GET_BLOG_LIST: 'blogList/FAILURE_GET_BLOG_LIST' 9 | } 10 | 11 | const initState = { 12 | status: '', 13 | list: [], 14 | amount: 0 15 | } 16 | 17 | export default (state = initState, action) => { 18 | switch (action.type) { 19 | case types.START_GET_BLOG_LIST: 20 | return { 21 | ...state, 22 | status: 'pending' 23 | } 24 | case types.SUCCESS_GET_BLOG_LIST: 25 | return { 26 | status: 'success', 27 | list: action.data.list, 28 | amount: action.data.amount 29 | } 30 | case types.FAILURE_GET_BLOG_LIST: 31 | return { 32 | ...state, 33 | status: 'failure' 34 | } 35 | default: 36 | return state 37 | } 38 | } 39 | 40 | const startGetBlogList = () => ({ 41 | type: types.START_GET_BLOG_LIST 42 | }) 43 | 44 | const successGetBlogList = (list, amount) => ({ 45 | type: types.SUCCESS_GET_BLOG_LIST, 46 | data: { 47 | list, 48 | amount 49 | } 50 | }) 51 | 52 | const failureGetBlogList = () => ({ 53 | type: types.FAILURE_GET_BLOG_LIST 54 | }) 55 | 56 | // 请求获取博客列表 57 | const requestGetBlogList = () => dispatch => { 58 | dispatch(startGetBlogList()) 59 | 60 | return axios 61 | .get(`${apiPrefix}/blogs?${blogListQs}`) 62 | .then(response => { 63 | const { blogs, count } = response.data.data 64 | dispatch(successGetBlogList(blogs, count)) 65 | return 'success' 66 | }) 67 | .catch(error => { 68 | console.log(error.response) 69 | dispatch(failureGetBlogList()) 70 | return 'failure' 71 | }) 72 | } 73 | 74 | export { requestGetBlogList } 75 | -------------------------------------------------------------------------------- /src/components/Tag/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | import { colorStyle, colorName } from '../../utils' 5 | 6 | const StyledTag = styled.div` 7 | display: inline-block; 8 | padding: 0 0.5833rem; 9 | margin-right: 0.6667rem; 10 | font-size: 1rem; 11 | line-height: 1.5; 12 | height: 1.833rem; 13 | border-radius: 4px; 14 | border: 1px solid #d9d9d9; 15 | 16 | /* 标签的默认颜色样式 */ 17 | background-color: #fafafa; 18 | & a { 19 | color: rgba(0, 0, 0, 0.65); 20 | } 21 | 22 | /* 根据color的值来设置颜色样式 */ 23 | & a { 24 | color: ${({ color }) => color && colorStyle[color]['color']}; 25 | } 26 | color: ${({ color }) => color && colorStyle[color]['color']}; 27 | 28 | background-color: ${({ color }) => 29 | color && colorStyle[color]['background-color']}; 30 | border-color: ${({ color }) => color && colorStyle[color]['border-color']}; 31 | ` 32 | 33 | class Tag extends Component { 34 | constructor(props) { 35 | super(props) 36 | 37 | let color = undefined 38 | if (props.random) { 39 | color = colorName[Math.floor((Math.random() * 100) % 11)] 40 | } else if (props.color) { 41 | color = props.color 42 | } 43 | this.state = { color } 44 | } 45 | 46 | render() { 47 | const { to, children, isLink, onClick } = this.props 48 | return ( 49 | 50 | {isLink ? ( 51 | 52 | {children} 53 | 54 | ) : ( 55 | {children} 56 | )} 57 | 58 | ) 59 | } 60 | } 61 | Tag.propTypes = { 62 | color: PropTypes.string, 63 | random: PropTypes.bool, 64 | to: PropTypes.string, 65 | children: PropTypes.node, 66 | isLink: PropTypes.bool, 67 | onClick: PropTypes.func 68 | } 69 | 70 | export default Tag 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | ZALE的博客 30 | 31 | 32 | 33 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/ResumeContent/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators } from 'redux' 5 | import { requestGetResume } from '../../reducers/resume' 6 | import Styled from 'styled-components' 7 | import ContentLoader from '../ContentLoader' 8 | import PointOutContent from '../PointOutContent' 9 | import './markdown.css' 10 | 11 | export const BlogContainer = Styled.div` 12 | background-color: #fff; 13 | margin-bottom: 1rem; 14 | box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.2); 15 | 16 | color: rgba(51, 51, 51, 0.9); 17 | ` 18 | 19 | export const BlogContent = Styled.div` 20 | padding: 2rem 1.3rem 2rem 1.3rem; 21 | ` 22 | 23 | class Blog extends Component { 24 | componentDidMount() { 25 | this.props.requestGetResume() 26 | } 27 | 28 | render() { 29 | const { status, htmlContent } = this.props 30 | 31 | // 检查数据是否处于加载阶段或加载失败 32 | if (status === '' || status === 'pending') { 33 | return 34 | } else if (status === 'failure') { 35 | return 36 | } 37 | 38 | return ( 39 | 40 | 41 |
46 | 47 | 48 | ) 49 | } 50 | } 51 | 52 | const mapStateToProps = state => ({ 53 | status: state.resume.status, 54 | htmlContent: state.resume.htmlContent 55 | }) 56 | 57 | const mapDispatchToProps = dispatch => 58 | bindActionCreators( 59 | { 60 | requestGetResume 61 | }, 62 | dispatch 63 | ) 64 | 65 | Blog.propTypes = { 66 | status: PropTypes.string, 67 | htmlContent: PropTypes.string 68 | } 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)(Blog) 71 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' 3 | import TopNav from './components/TopNav' 4 | import Container from './components/Container' 5 | import ProfileCard from './components/ProfileCard' 6 | import Grid from './components/Grid' 7 | import Footer from './components/Footer' 8 | import Blog from './components/Blog' 9 | import ArticleDirectory from './components/ArticleDirectory' 10 | import './reboot.css' 11 | import BlogList from './components/BlogList' 12 | import { navList } from './constants' 13 | import ScrollToTop from './components/ScrollToTop' 14 | import PointOutContent from './components/PointOutContent' 15 | import ResumeContent from './components/ResumeContent' 16 | 17 | const OnDevelopingContent = () => 18 | const NotFoundContent = () => 19 | 20 | const App = () => ( 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 |
47 | ) 48 | 49 | export default App 50 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right