├── server
├── Procfile
├── README.md
├── .gitignore
├── routers
│ ├── index.js
│ └── api.js
├── models
│ ├── topic_collect.js
│ ├── user.js
│ ├── message.js
│ ├── comment.js
│ └── topic.js
├── db
│ └── connect.js
├── index.js
├── package.json
├── middleware
│ ├── shouldAuth.js
│ └── token.js
└── controllers
│ ├── message_controller.js
│ ├── comment_controller.js
│ ├── user_controller.js
│ └── topic_controller.js
├── web
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── src
│ ├── sources
│ │ ├── 404.jpg
│ │ ├── bg.jpeg
│ │ ├── crow.jpg
│ │ └── dian.svg
│ ├── components
│ │ ├── Header
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── Steps
│ │ │ └── index.js
│ │ ├── NavBar
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── Comment
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── NoMatch
│ │ │ ├── index.js
│ │ │ └── index.scss
│ │ └── TopicItem
│ │ │ ├── index.scss
│ │ │ └── index.js
│ ├── style
│ │ ├── animation.scss
│ │ └── global.scss
│ ├── reducers
│ │ ├── index.js
│ │ ├── auth.js
│ │ ├── comment.js
│ │ ├── message.js
│ │ ├── user.js
│ │ └── topic.js
│ ├── containers
│ │ ├── Register
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── Login
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── Settings
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── Home
│ │ │ └── index.js
│ │ ├── Message
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── UserInfo
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── TopicDetail
│ │ │ ├── index.scss
│ │ │ └── index.js
│ │ ├── CategoryList
│ │ │ └── index.js
│ │ └── CreateTopic
│ │ │ └── index.js
│ ├── stores
│ │ └── index.js
│ ├── index.js
│ ├── server
│ │ └── interceptors.js
│ ├── routers
│ │ └── index.js
│ ├── apis
│ │ └── index.js
│ └── registerServiceWorker.js
├── README.md
├── .gitignore
├── config
│ ├── jest
│ │ ├── fileTransform.js
│ │ └── cssTransform.js
│ ├── polyfills.js
│ ├── paths.js
│ ├── env.js
│ ├── webpackDevServer.config.js
│ ├── webpack.config.dev.js
│ └── webpack.config.prod.js
├── scripts
│ ├── test.js
│ ├── start.js
│ └── build.js
└── package.json
├── .gitignore
├── README.md
└── LICENSE
/server/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
--------------------------------------------------------------------------------
/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shiyangzhaoa/call-club/HEAD/web/public/favicon.ico
--------------------------------------------------------------------------------
/web/src/sources/404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shiyangzhaoa/call-club/HEAD/web/src/sources/404.jpg
--------------------------------------------------------------------------------
/web/src/sources/bg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shiyangzhaoa/call-club/HEAD/web/src/sources/bg.jpeg
--------------------------------------------------------------------------------
/web/src/sources/crow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shiyangzhaoa/call-club/HEAD/web/src/sources/crow.jpg
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | 自己手搭的
2 | ## server端,用的koa2 + mongodb
3 | node index.js启动,占用8080端口;
4 | *****
5 | 推荐使用PM2,或者supervisor
6 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /web/node_modules
5 | /server/node_modules
6 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | 用create-react-app作为脚手架
2 | ## web端,用的react + react router v4 + redux
3 | 默认占用3000端口。开发环境代理8080端口,更改的话请修改生产环境的webpack配置.
4 | ****
5 | 启动:npm start;
6 | 打包:npm run build;
7 |
--------------------------------------------------------------------------------
/server/routers/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 整合所有子路由
3 | */
4 |
5 | const router = require('koa-router')();
6 | const api = require('./api.js');
7 |
8 | router.use('/api', api.routes(), api.allowedMethods());
9 |
10 | module.exports = router;
11 |
--------------------------------------------------------------------------------
/web/src/components/Header/index.scss:
--------------------------------------------------------------------------------
1 | .breadcrumb {
2 | padding: 20px 80px;
3 | background-color: #444;
4 |
5 | .ant-breadcrumb-separator {
6 | color: #fff;
7 | }
8 |
9 | .ant-breadcrumb-link {
10 | color: #ccc;
11 | }
12 |
13 | .ant-card-head {
14 | background-color: #f6f6f6;
15 | }
16 | }
--------------------------------------------------------------------------------
/web/src/style/animation.scss:
--------------------------------------------------------------------------------
1 | .topic-enter {
2 | opacity: 0.01;
3 | }
4 |
5 | .topic-enter.topic-enter-active {
6 | opacity: 1;
7 | transition: opacity 500ms ease-in;
8 | }
9 |
10 | .topic-leave {
11 | opacity: 1;
12 | }
13 |
14 | .topic-leave.topic-leave-active {
15 | opacity: 0.01;
16 | transition: opacity 300ms ease-in;
17 | }
--------------------------------------------------------------------------------
/web/src/reducers/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { combineReducers } from 'redux';
3 |
4 | import topic from './topic';
5 | import user from './user';
6 | import auth from './auth';
7 | import comment from './comment';
8 | import message from './message';
9 |
10 | const combined = combineReducers({ topic, user, auth, comment, message });
11 | export default combined;
12 |
--------------------------------------------------------------------------------
/web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "call club",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/web/src/containers/Register/index.scss:
--------------------------------------------------------------------------------
1 | .register-content {
2 | display: flex;
3 | border-radius: 10px;
4 | width: 60%;
5 | margin: 10% auto;
6 | background-color: rgba(255, 255, 255, .4);
7 | padding: 10px;
8 | }
9 |
10 | .register-form {
11 | flex: 1;
12 | }
13 |
14 | @media (max-width: 400px) {
15 | .register-content {
16 | flex-direction: column;
17 | }
18 | }
--------------------------------------------------------------------------------
/web/.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 |
--------------------------------------------------------------------------------
/web/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/tutorial-webpack.html
7 |
8 | module.exports = {
9 | process(src, filename) {
10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`;
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/web/src/components/Steps/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Timeline } from 'antd';
3 |
4 | const Steps = (props) => (
5 |
6 | {props.steps.map(item => (
7 |
8 | {item.title}
9 |
10 | ))}
11 |
12 | );
13 |
14 | export default Steps;
15 |
--------------------------------------------------------------------------------
/web/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/tutorial-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 |
--------------------------------------------------------------------------------
/server/models/topic_collect.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const topicCollect = new Schema({
6 | author_id: Schema.Types.ObjectId,
7 | topic_id: { type: Schema.Types.ObjectId, required: true },
8 | create_at: { type: Date, default: new Date() },
9 | });
10 |
11 | const TopicCollect = mongoose.model('TopicCollect', topicCollect);
12 |
13 | module.exports = TopicCollect;
--------------------------------------------------------------------------------
/server/db/connect.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | mongoose.Promise = Promise;
4 |
5 | mongoose.connect('mongodb://127.0.0.1:27017/call_club', {
6 | useMongoClient: true,
7 | promiseLibrary: global.Promise
8 | });
9 |
10 | const db = mongoose.connection;
11 |
12 | db.on('error', (error) => {
13 | console.log(`数据库创建失败:${error}`);
14 | });
15 |
16 | db.on('open', () => {
17 | console.log('数据库连接成功');
18 | });
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # call-club
2 | 前端:react、redux和react router,默认占用3000,后端:koa2,数据库:mongodb,要求node8.0以上,默认占用8080;
3 | 后端部署到了heroku,已经对跨域做了处理。接口地址:https://call-club.herokuapp.com/api;
4 | ****
5 | [线上地址](https://shiyangzhaoa.github.io/call-club-web/#/)
6 |
7 | cd web => npm install => npm start
8 | ****
9 | cd server = > npm install => node index
10 | ****
11 | 功能:
12 | - [x] 登陆、注册
13 | - [x] 文章列表、详情、创建和评论
14 | - [x] 对文章的评论和对个人的回复
15 | - [x] 信息数量,标记单个已读和全部已读
16 | - [ ] 管理员功能
17 |
--------------------------------------------------------------------------------
/web/src/components/NavBar/index.scss:
--------------------------------------------------------------------------------
1 | .nav-bar {
2 | height: 100vh;
3 | }
4 | @media (max-width: 1000px) {
5 | .pc {
6 | display: none;
7 | }
8 |
9 | .menu {
10 | line-height: 64px;
11 | }
12 |
13 | .ant-layout.ant-layout-has-sider {
14 | flex-direction: column;
15 | }
16 | }
17 | @media (min-width: 1000px) {
18 | .mobile {
19 | display: none;
20 | }
21 |
22 | .ant-layout.ant-layout-has-sider {
23 | flex-direction: row;
24 | }
25 | }
--------------------------------------------------------------------------------
/web/src/stores/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | applyMiddleware,
4 | compose
5 | } from 'redux';
6 | import thunk from 'redux-thunk';
7 | import reducers from '../reducers';
8 |
9 | function reduxStore(initialState) {
10 | const store = createStore(reducers, initialState, compose(
11 | applyMiddleware(thunk),
12 | window.devToolsExtension ? window.devToolsExtension() : f => f
13 | ));
14 | return store;
15 | }
16 |
17 | export default reduxStore;
18 |
--------------------------------------------------------------------------------
/web/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Breadcrumb } from 'antd';
3 | import { Link } from 'react-router-dom';
4 |
5 | import './index.scss';
6 |
7 | const Header = (props) => (
8 |
9 | {props.back}
10 | {props.now}
11 |
12 | );
13 |
14 | export default Header;
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const user = new Schema({
6 | loginname: { type: String, required: true },
7 | avatar_url: String,
8 | email: { type: String, required: true },
9 | password: { type: String, required: true },
10 | signature: String,
11 | web: String,
12 | creat_time: { type: Date, default: new Date() },
13 | });
14 |
15 | const User = mongoose.model('User', user);
16 |
17 | module.exports = User;
18 |
--------------------------------------------------------------------------------
/web/src/containers/Login/index.scss:
--------------------------------------------------------------------------------
1 | .login-page {
2 | display: flex;
3 | border-radius: 10px;
4 | width: 80%;
5 | margin: 10% auto;
6 | background-color: rgba(255, 255, 255, .4);
7 | padding: 10px;
8 | }
9 |
10 | .login-form {
11 | flex: 1;
12 | margin-left: 20px;
13 |
14 | .form-ctrl {
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | }
19 | }
20 |
21 | @media (max-width: 400px) {
22 | .login-page {
23 | flex-direction: column;
24 | }
25 | }
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const koaBody = require('koa-body');
3 | const logger = require('koa-logger')
4 | //用来处理跨域的,前端直接代理就可以了
5 | //const cors = require('koa-cors2');
6 |
7 | const routers = require('./routers/index');
8 | require('./db/connect');
9 |
10 | const app = new Koa();
11 |
12 | //app.use(cors());
13 | app.use(logger());
14 | app.use(koaBody(({ multipart: true })));
15 | app.use(routers.routes()).use(routers.allowedMethods());
16 |
17 | app.listen(process.env.PORT || 8080);
18 |
19 | console.log('call-club is starting at port 8080');
20 |
--------------------------------------------------------------------------------
/server/models/message.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const message = new Schema({
6 | type: { type: String },
7 | author_id: { type: Schema.Types.ObjectId },
8 | topic_author_id: { type: Schema.Types.ObjectId },
9 | topic_id: { type: Schema.Types.ObjectId },
10 | reply_id: { type: Schema.Types.ObjectId },
11 | has_read: { type: Boolean, default: false },
12 | create_at: { type: Date, default: new Date() },
13 | });
14 |
15 | const Message = mongoose.model('Message', message);
16 |
17 | module.exports = Message;
--------------------------------------------------------------------------------
/web/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { AUTH_REQUSET, AUTH_SUCCESS, AUTH_FAILED, INIT_USER_INFO } from './../apis';
2 |
3 | const initState = {
4 | status: '',
5 | auth: {}
6 | }
7 |
8 | export default function user (state = initState, action) {
9 | switch (action.type) {
10 | case AUTH_REQUSET:
11 | case AUTH_SUCCESS:
12 | case AUTH_FAILED:
13 | return Object.assign({}, state, { status: action.status }, { auth: action.auth || {} });
14 | case INIT_USER_INFO:
15 | return Object.assign({}, state, { auth: {} });
16 | default:
17 | return state;
18 | }
19 | }
--------------------------------------------------------------------------------
/web/src/containers/Settings/index.scss:
--------------------------------------------------------------------------------
1 | .setting-page {
2 |
3 | .content {
4 | width: 80%;
5 | margin: 20px auto;
6 | }
7 |
8 | .setting-card {
9 | margin-bottom: 20px;
10 |
11 | .setting-form {
12 | padding: 20px 0 0;
13 | }
14 | }
15 |
16 | .ant-card-body {
17 | padding: 0;
18 | }
19 |
20 | .ant-card-head {
21 | background: #eaecec;
22 | border-top-left-radius: 0.5em;
23 | border-top-right-radius: 0.5em;
24 | }
25 |
26 | .ant-card-bordered {
27 | border-top-left-radius: 0.5em;
28 | border-top-right-radius: 0.5em;
29 | }
30 | }
--------------------------------------------------------------------------------
/web/src/components/Comment/index.scss:
--------------------------------------------------------------------------------
1 | .comment-item {
2 | display: flex;
3 | padding: 10px;
4 | font-size: 14px;
5 |
6 | &:not(:last-child) {
7 | border-bottom: 1px solid #e1e1e1;
8 | }
9 |
10 | .reply-avatar {
11 | width: 30px;
12 | height: 30px;
13 | margin: 0 10px;
14 | }
15 |
16 | .reply-body {
17 | flex: 1;
18 |
19 | .reply-head {
20 | display: flex;
21 | justify-content: space-between;
22 | }
23 |
24 | .reply-name {
25 | color: #666;
26 | font-size: 12px;
27 | font-weight: 700;
28 | margin-right: 10px;
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/web/src/containers/Home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Route,
4 | Switch,
5 | } from 'react-router-dom';
6 |
7 | import NavBar from './../../components/NavBar';
8 | import CategoryList from './../CategoryList';
9 |
10 | const Home = (pros) => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default Home;
--------------------------------------------------------------------------------
/server/models/comment.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const comment = new Schema({
6 | author_id: Schema.Types.ObjectId,
7 | topic_id: { type: Schema.Types.ObjectId, required: true },
8 | content: String,
9 | create_at: { type: Date, default: new Date() },
10 | ups: [Schema.Types.ObjectId],
11 | reply_id: Schema.Types.ObjectId,
12 | is_uped: { type: Boolean, default: false },
13 | });
14 |
15 | comment.index({topic_id: 1});
16 | comment.index({author_id: 1, create_at: -1});
17 |
18 | const Comment = mongoose.model('Comment', comment);
19 |
20 | module.exports = Comment;
21 |
--------------------------------------------------------------------------------
/web/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 |
--------------------------------------------------------------------------------
/web/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 |
5 | import 'antd/dist/antd.css';
6 | import 'github-markdown-css';
7 | import 'react-quill/dist/quill.snow.css';
8 | import './style/animation.scss';
9 | import './style/global.scss';
10 | import './server/interceptors';
11 | import AppRouter from './routers';
12 | import configureStore from './stores';
13 |
14 | import registerServiceWorker from './registerServiceWorker';
15 |
16 | const store = configureStore();
17 |
18 | ReactDOM.render(
19 |
20 |
21 | ,
22 | document.getElementById('root')
23 | );
24 | registerServiceWorker();
25 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "engines": {
7 | "node": "8.4.0"
8 | },
9 | "scripts": {
10 | "start": "node index.js",
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "keywords": [
14 | "node",
15 | "koa2"
16 | ],
17 | "author": "yangzhao",
18 | "license": "ISC",
19 | "dependencies": {
20 | "gravatar": "^1.6.0",
21 | "jsonwebtoken": "^8.0.0",
22 | "koa": "^2.3.0",
23 | "koa-body": "^2.3.0",
24 | "koa-cors2": "0.0.1",
25 | "koa-logger": "^3.0.1",
26 | "koa-router": "^7.2.1",
27 | "lodash": "^4.17.4",
28 | "mongoose": "^4.11.10",
29 | "qiniu": "^7.0.8"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/web/src/style/global.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-image: url('./../sources/bg.jpeg');
3 | }
4 |
5 | .rdw-editor-wrapper {
6 | border: 2px solid #eee;
7 | width: 75%;
8 | min-width: 600px;
9 | margin: 0 auto;
10 | padding: 10px 10px;
11 | border-radius: 6px;
12 | }
13 |
14 | .rdw-editor-main {
15 | height: 300px !important;
16 | }
17 |
18 | .ql-container {
19 | min-height: 30em;
20 | border-bottom-left-radius: 0.5em;
21 | border-bottom-right-radius: 0.5em;
22 | background: #fefcfc;
23 | }
24 |
25 | .ql-toolbar {
26 | background: #eaecec;
27 | border-top-left-radius: 0.5em;
28 | border-top-right-radius: 0.5em;
29 | }
30 |
31 | a {
32 | color: #333;
33 |
34 | &:hover {
35 | color: #333;
36 | text-decoration: underline;
37 | }
38 | }
--------------------------------------------------------------------------------
/web/src/server/interceptors.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import createHistory from 'history/createHashHistory';
3 |
4 | const history = createHistory()
5 |
6 | //如果有token就在请求头里面带上
7 | axios.interceptors.request.use(function (config) {
8 | const token = localStorage.getItem('login_token');
9 | if (token) {
10 | config.headers['x-access-token'] = token;
11 | }
12 | return config;
13 | }, function (error) {
14 | return Promise.reject(error);
15 | });
16 |
17 | //对登陆过期做处理
18 | axios.interceptors.response.use(function (response) {
19 | return response;
20 | }, function (error) {
21 | const { errCode } = error.response.data;
22 | if (errCode && errCode === 100) {
23 | localStorage.removeItem('login_token');
24 | history.push('/login');
25 | }
26 | return Promise.reject(error);
27 | });
--------------------------------------------------------------------------------
/web/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 |
--------------------------------------------------------------------------------
/web/src/components/NoMatch/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './index.scss';
4 | import NotFound from './../../sources/404.jpg';
5 | import Crow from './../../sources/crow.jpg';
6 | import Point from './../../sources/dian.svg';
7 |
8 | class NoMatch extends React.Component {
9 |
10 | render() {
11 | return (
12 |
13 |
14 |
15 |

16 |
17 |
18 |
404
19 |
page not found
20 |
21 |

22 |
23 | );
24 | }
25 | }
26 |
27 | NoMatch.defaultProps = {
28 | };
29 |
30 | export default NoMatch;
31 |
--------------------------------------------------------------------------------
/web/src/components/NoMatch/index.scss:
--------------------------------------------------------------------------------
1 | .not-found {
2 | width: 100%;
3 | height: 100vh;
4 | background-color: #fff;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 |
9 | .fly-box {
10 | height: 63px;
11 | width: 100vw;
12 | position: fixed;
13 | top: 0;
14 | left: 0;
15 | }
16 |
17 | .crow-box {
18 | width: 100%;
19 | background-color: #fff;
20 | position: fixed;
21 | top: 0;
22 | right: 0;
23 | animation: fly 6s linear infinite;
24 | text-align: right;
25 | }
26 |
27 | .number {
28 | font-size: 100px;
29 | color: #108ee9;
30 | font-weight: bold;
31 | text-align: right;
32 | }
33 |
34 | .warin-text {
35 | font-size: 40px;
36 | color: #108ee9;
37 | }
38 | }
39 | @keyframes fly {
40 | 0% {
41 | right: 0;
42 | }
43 | 100% {
44 | right: calc(100% - 157px);
45 | }
46 | }
--------------------------------------------------------------------------------
/server/models/topic.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const topicSchema = new Schema({
6 | author_id: { type: Schema.Types.ObjectId, required: true },
7 | tab: { type: String, required: true },
8 | content: String,
9 | title: { type: String, required: true },
10 | last_reply: { type: Schema.Types.ObjectId },
11 | last_reply_at: { type: Date, default: new Date() },
12 | good: { type: Boolean, default: false },
13 | top: { type: Boolean, default: false },
14 | reply_count: { type: Number, default: 0 },
15 | visit_count: { type: Number, default: 0 },
16 | create_at: { type: Date, default: new Date() },
17 | author: {
18 | loginname: String,
19 | avatar_url: String,
20 | },
21 | });
22 |
23 |
24 | topicSchema.index({create_at: -1});
25 | topicSchema.index({top: -1, last_reply_at: -1});
26 |
27 | const Topic = mongoose.model('Topics', topicSchema);
28 |
29 | module.exports = Topic;
30 |
--------------------------------------------------------------------------------
/server/middleware/shouldAuth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | const getVerify = function (token) {
4 | return new Promise((resolve, reject) => {
5 | jwt.verify(token, 'app.get(user)', function (err, decoded) {
6 | if (err) {
7 | reject(00);
8 | } else {
9 | resolve(decoded);
10 | }
11 | });
12 | })
13 | }
14 |
15 | const resolveToken = async function (ctx, next) {
16 | let token = ctx.request.body.token || ctx.request.query.token || ctx.request.header['x-access-token'];
17 | if (token) {
18 | try {
19 | const result = await getVerify(token);
20 | ctx.api_user = result;
21 | await next();
22 | } catch(e) {
23 | if (e === 00) {
24 | await next();
25 | } else {
26 | ctx.status = 500;
27 | ctx.body = {
28 | success: false,
29 | message: e.message,
30 | }
31 | }
32 | }
33 | } else {
34 | await next();
35 | }
36 | }
37 |
38 | module.exports = resolveToken;
--------------------------------------------------------------------------------
/web/src/components/TopicItem/index.scss:
--------------------------------------------------------------------------------
1 | .topic-item {
2 | margin-bottom: 20px;
3 |
4 | .topic-content {
5 | display: flex;
6 | }
7 |
8 | .avatar_img {
9 | width: 50px;
10 | height: 50px;
11 | }
12 |
13 | .topic-title {
14 | font-size: 15px;
15 | }
16 |
17 | .topic-info {
18 | display: flex;
19 | justify-content: space-between;
20 | align-items: center;
21 | }
22 |
23 | .message {
24 | background-color: #A9BBDC;
25 | padding: 5px 15px;
26 | border-radius: 40px;
27 | line-height: 15px;
28 | font-size: 15px;
29 | color: #fff;
30 | }
31 |
32 | .topic-msg {
33 | margin-left: 20px;
34 | display: flex;
35 | flex-direction: column;
36 | justify-content: center;
37 |
38 | .topic-count {
39 | font-size: 12px;
40 | color: #bbb;
41 | margin-top: 5px;
42 | }
43 | }
44 |
45 | a {
46 | color: #000;
47 | }
48 |
49 | a:hover {
50 | text-decoration: underline;
51 | }
52 |
53 | .ant-card-head {
54 | background: #79B9DA;
55 | }
56 | }
--------------------------------------------------------------------------------
/web/src/containers/Message/index.scss:
--------------------------------------------------------------------------------
1 | .message-page {
2 |
3 | .mark-btn {
4 | margin-bottom: 20px;
5 | }
6 |
7 | .info-card {
8 | margin-bottom: 20px;
9 |
10 | .info-value {
11 | font-size: 17px;
12 | color: #333;
13 | }
14 |
15 | .info-signature {
16 | font-style: oblique;
17 | }
18 |
19 | .message-item {
20 | border-bottom: 1px solid #ddd;
21 | padding: 10px 15px;
22 |
23 | &:last-child{
24 | border: 0;
25 | }
26 | }
27 |
28 | .topic-link {
29 | color: #08c;
30 |
31 | &:hover {
32 | color: #035d8a;
33 | }
34 | }
35 | }
36 |
37 | .ant-card-wider-padding .ant-card-body {
38 | padding: 0;
39 | }
40 |
41 | .ant-card-body {
42 | padding: 0;
43 | }
44 |
45 | .ant-card-head {
46 | background: #eaecec;
47 | border-top-left-radius: 0.5em;
48 | border-top-right-radius: 0.5em;
49 | }
50 |
51 | .ant-card-bordered {
52 | border-top-left-radius: 0.5em;
53 | border-top-right-radius: 0.5em;
54 | }
55 | }
--------------------------------------------------------------------------------
/web/src/sources/dian.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/src/containers/UserInfo/index.scss:
--------------------------------------------------------------------------------
1 | .user-info {
2 |
3 | .info-card {
4 | margin-bottom: 20px;
5 |
6 | .info-value {
7 | font-size: 17px;
8 | color: #333;
9 | }
10 |
11 | .info-signature {
12 | font-style: oblique;
13 | }
14 |
15 | .user-topic-item {
16 | border-bottom: 1px solid #ddd;
17 | padding: 10px 15px;
18 |
19 | &:last-child{
20 | border: 0;
21 | }
22 | }
23 |
24 | .topic-title {
25 | font-size: 14px;
26 | }
27 |
28 | .topic-count {
29 | font-size: 12px;
30 | color: #bbb;
31 | }
32 | }
33 |
34 | .info-topic {
35 |
36 | .ant-card-wider-padding .ant-card-body {
37 | padding: 0;
38 | }
39 |
40 | .ant-card-body {
41 | padding: 0;
42 | }
43 | }
44 |
45 | .ant-card-head {
46 | background: #eaecec;
47 | border-top-left-radius: 0.5em;
48 | border-top-right-radius: 0.5em;
49 | }
50 |
51 | .ant-card-bordered {
52 | border-top-left-radius: 0.5em;
53 | border-top-right-radius: 0.5em;
54 | }
55 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 yangZ
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/web/src/reducers/comment.js:
--------------------------------------------------------------------------------
1 | import {
2 | COMMIT_COMMENT_REQUSET,
3 | COMMIT_COMMENT_SUCCESS,
4 | COMMIT_COMMENT_FAILED,
5 | UP_REPLY_REQUEST,
6 | UP_REPLY_SUCCESS,
7 | UP_REPLY_FAILD,
8 | DELETE_REPLY_REQUEST,
9 | DELETE_REPLY_SUCCESS,
10 | DELETE_REPLY_FAILED,
11 | } from './../apis';
12 |
13 | const initState = {
14 | commitStatus: '',
15 | upStatus: '',
16 | upAction: {},
17 | delReplyStatus: '',
18 | }
19 |
20 | export default function commitComment(state=initState, action) {
21 | switch(action.type) {
22 | case COMMIT_COMMENT_REQUSET:
23 | case COMMIT_COMMENT_SUCCESS:
24 | case COMMIT_COMMENT_FAILED:
25 | return Object.assign({}, state, { commitStatus: action.status });
26 | case UP_REPLY_REQUEST:
27 | case UP_REPLY_SUCCESS:
28 | case UP_REPLY_FAILD:
29 | return Object.assign({}, state, { upStatus: action.status, upAction: action.action });
30 | case DELETE_REPLY_REQUEST:
31 | case DELETE_REPLY_SUCCESS:
32 | case DELETE_REPLY_FAILED:
33 | return Object.assign({}, state, { delReplyStatus: action.status });
34 | default:
35 | return state;
36 | }
37 | }
--------------------------------------------------------------------------------
/server/middleware/token.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | const getVerify = function (token) {
4 | return new Promise((resolve, reject) => {
5 | jwt.verify(token, 'app.get(user)', function (err, decoded) {
6 | if (err) {
7 | reject(100);
8 | } else {
9 | resolve(decoded);
10 | }
11 | });
12 | })
13 | }
14 |
15 | const resolveToken = async function (ctx, next) {
16 | let token = ctx.request.body.token || ctx.request.query.token || ctx.request.header['x-access-token'];
17 | if (token) {
18 | try {
19 | const result = await getVerify(token);
20 | ctx.api_user = result;
21 | await next();
22 | } catch(e) {
23 | if (e === 100) {
24 | ctx.status = 401;
25 | ctx.body = {
26 | success: false,
27 | message: 'token过期',
28 | errCode: 100,
29 | }
30 | } else {
31 | ctx.status = 500;
32 | ctx.body = {
33 | success: false,
34 | message: e.message,
35 | }
36 | }
37 | }
38 | } else {
39 | ctx.status = 403;
40 | ctx.body = {
41 | success: false,
42 | message: '缺少token',
43 | }
44 | }
45 | }
46 |
47 | module.exports = resolveToken;
--------------------------------------------------------------------------------
/web/src/components/TopicItem/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card } from 'antd';
3 | import { Link } from 'react-router-dom';
4 | import moment from 'moment';
5 |
6 | import './index.scss';
7 |
8 | const TopicItem = (props) => {
9 | const style = {
10 | color: props.info.top || props.info.good ? '#cc1e4e' : '#fff'
11 | }
12 | const { info } = props;
13 | const lastReply = moment(info.last_reply_at).fromNow()
14 | return (
15 |
16 |
{info.kind}
18 | } bordered={true} style={{ width: '100%' }}>
19 |
20 |
21 | {info.author &&

}
22 | {info.author ?
23 |
24 |
{info.title}
25 |
{info.author.loginname} > 浏览量{info.visit_count} • 最后回复于{lastReply}
26 |
27 | :
无数据}
28 |
29 | {!!info.reply_count &&
{info.reply_count}
}
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default TopicItem;
--------------------------------------------------------------------------------
/web/src/reducers/message.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MESSAGE_COUNT_REQUEST,
3 | GET_MESSAGE_COUNT_SUCCESS,
4 | GET_MESSAGE_COUNT_FAILED,
5 | GET_MESSAGES_REQUEST,
6 | GET_MESSAGES_SUCCESS,
7 | GET_MESSAGES_FAILED,
8 | MARK_ONE_MESSAGE_REQUEST,
9 | MARK_ONE_MESSAGE_SUCCESS,
10 | MARK_ONE_MESSAGE_FAILED,
11 | MARK_ALL_MESSAGE_REQUEST,
12 | MARK_ALL_MESSAGE_SUCCESS,
13 | MARK_ALL_MESSAGE_FAILED,
14 | } from './../apis';
15 |
16 | const initState = {
17 | messageCountStatus: '',
18 | count: 0,
19 | getMessageStatus: '',
20 | messages: null,
21 | markOneStatus: '',
22 | markAllStatus: '',
23 | };
24 |
25 | export default function commitComment(state=initState, action) {
26 | switch(action.type) {
27 | case GET_MESSAGE_COUNT_REQUEST:
28 | return Object.assign({}, state, { messageCountStatus: action.status });
29 | case GET_MESSAGE_COUNT_SUCCESS:
30 | case GET_MESSAGE_COUNT_FAILED:
31 | return Object.assign({}, state, { messageCountStatus: action.status, count: action.count });
32 | case GET_MESSAGES_REQUEST:
33 | case GET_MESSAGES_SUCCESS:
34 | case GET_MESSAGES_FAILED:
35 | return Object.assign({}, state, { getMessageStatus: action.status, messages: action.messages });
36 | case MARK_ONE_MESSAGE_REQUEST:
37 | case MARK_ONE_MESSAGE_SUCCESS:
38 | case MARK_ONE_MESSAGE_FAILED:
39 | return Object.assign({}, state, { markOneStatus: action.status });
40 | case MARK_ALL_MESSAGE_REQUEST:
41 | case MARK_ALL_MESSAGE_SUCCESS:
42 | case MARK_ALL_MESSAGE_FAILED:
43 | return Object.assign({}, state, { markAllStatus: action.status, markResult: action.data });
44 | default:
45 | return state;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/web/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import {
2 | SUBMIT_REGISTER_REQUSET,
3 | SUBMIT_REGISTER_SUCCESS,
4 | SUBMIT_REGISTER_FAILED,
5 | LOGIN_REQUSET,
6 | LOGIN_SUCCESS,
7 | LOGIN_FAILED,
8 | GET_USERINFO_REQUSET,
9 | GET_USERINFO_SUCCESS,
10 | GET_USERINFO_FAILED,
11 | UPDATE_SETTING_REQUEST,
12 | UPDATE_SETTING_SUCCESS,
13 | UPDATE_SETTING_FAILED,
14 | UPDATE_PASSWORD_REQUEST,
15 | UPDATE_PASSWORD_SUCCESS,
16 | UPDATE_PASSWORD_FAILED,
17 | } from './../apis';
18 |
19 | const initState = {
20 | registerStatus: '',
21 | info: null,
22 | loginStatus: '',
23 | getInfoStatus: '',
24 | updateStatus: '',
25 | changeStatus: '',
26 | }
27 |
28 | export default function user (state = initState, action) {
29 | switch(action.type) {
30 | case SUBMIT_REGISTER_REQUSET:
31 | case SUBMIT_REGISTER_SUCCESS:
32 | case SUBMIT_REGISTER_FAILED:
33 | return Object.assign({}, state, { registerStatus: action.status });
34 | case LOGIN_REQUSET:
35 | case LOGIN_SUCCESS:
36 | case LOGIN_FAILED:
37 | return Object.assign({}, state, { loginStatus: action.status });
38 | case GET_USERINFO_REQUSET:
39 | case GET_USERINFO_SUCCESS:
40 | case GET_USERINFO_FAILED:
41 | return Object.assign({}, state, { info: action.info }, { getInfoStatus: action.status });
42 | case UPDATE_SETTING_REQUEST:
43 | case UPDATE_SETTING_SUCCESS:
44 | case UPDATE_SETTING_FAILED:
45 | return Object.assign({}, state, { updateStatus: action.status });
46 | case UPDATE_PASSWORD_REQUEST:
47 | case UPDATE_PASSWORD_SUCCESS:
48 | case UPDATE_PASSWORD_FAILED:
49 | return Object.assign({}, state, { changeStatus: action.status });
50 | default:
51 | return state;
52 | }
53 | }
--------------------------------------------------------------------------------
/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | call-club
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/web/src/components/Comment/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Icon } from 'antd';
3 | import { Link } from 'react-router-dom';
4 | import moment from 'moment';
5 | import ReactMarkdown from 'react-markdown';
6 |
7 | import './index.scss';
8 |
9 | const Comment = props => {
10 | const style = {
11 | fontSize: props.is_up ? '18px' : '14px',
12 | color: props.is_up ? '#000' : '#c8c8c8',
13 | transition: 'all .5s',
14 | }
15 | const { info, index, changeStatus, upReply, commentId, author, deleteReply } = props;
16 | const dura = moment(info.create_at).fromNow();
17 | return (
18 |
19 |
20 |

21 |
22 |
23 |
24 |
25 | {info.author.loginname}
26 | {`${index + 1}楼•${dura}`}
27 |
28 |
43 |
44 |
45 | {commentId === info.id && props.children}
46 |
47 |
48 | );
49 | }
50 |
51 | export default Comment;
--------------------------------------------------------------------------------
/server/routers/api.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')();
2 | const topicController = require('./../controllers/topic_controller');
3 | const userController = require('./../controllers/user_controller');
4 | const commentController = require('./../controllers/comment_controller');
5 | const messageController = require('./../controllers/message_controller');
6 | const resolveToken = require('./../middleware/token');
7 | const shouldAuth = require('./../middleware/shouldAuth');
8 |
9 |
10 | const routers = router
11 | .get('/topics', topicController.getTopics)
12 | .get('/topics/:id', shouldAuth, topicController.getTopicDetail)
13 | .get('/topic/no_reply', topicController.getNoReply)
14 | .post('/topic/create', resolveToken, topicController.create)
15 | .post('/topic/:topic_id/replies', resolveToken, commentController.create)
16 | .delete('/topic/:topic_id/delete', resolveToken, topicController.removeTopic)
17 | .put('/topic/update', resolveToken, topicController.updateTopic)
18 | .post('/reply/:reply_id/ups', resolveToken, commentController.upReply)
19 | .delete('/reply/:reply_id/delete', resolveToken, commentController.deleteReply)
20 | .post('/topic/topic_collect/collect', resolveToken, topicController.collect)
21 | .post('/topic_collect/de_collect', resolveToken, topicController.cancelCollect)
22 | .post('/user/login', userController.login)
23 | .post('/user/register', userController.createUser)
24 | .get('/user-info/:loginname', userController.getInfo)
25 | .get('/user/auth', resolveToken, userController.getAuth)
26 | .put('/user/basic_setting', resolveToken, userController.changeSetting)
27 | .put('/user/set_password', resolveToken, userController.setNewPassword)
28 | .get('/message/count', resolveToken, messageController.getCount)
29 | .get('/messages', resolveToken, messageController.getMessages)
30 | .post('/message/mark_one/:msg_id', resolveToken, messageController.markOne)
31 | .post('/message/mark_all', resolveToken, messageController.markAll);
32 |
33 | module.exports = routers;
34 |
--------------------------------------------------------------------------------
/web/src/containers/TopicDetail/index.scss:
--------------------------------------------------------------------------------
1 | .topic-detail {
2 | display: flex;
3 | padding: 20px 30px;
4 | width: 100%;
5 | box-sizing: border-box;
6 |
7 | .topic-info {
8 | flex: 7;
9 |
10 | .topic-item {
11 | margin-top: 20px;
12 | }
13 | }
14 |
15 | .article {
16 | flex: 3;
17 | margin-left: 20px;
18 |
19 | .no-reply-topic {
20 | padding: 10px 15px;
21 | font-size: 15px;
22 |
23 | &:not(:last-child) {
24 | border-bottom: 1px solid #ddd;
25 | }
26 | }
27 |
28 | a {
29 | color: #333;
30 | }
31 | }
32 |
33 | .topic-level {
34 | background: #80bd01;
35 | padding: 6px 12px;
36 | border-radius: 3px;
37 | color: #fff;
38 | font-size: 12px;
39 | margin-right: 10px;
40 | }
41 |
42 | .info-item {
43 | margin-right: 4px;
44 |
45 | &::before {
46 | content: '•';
47 | }
48 | }
49 |
50 | .topic-info {
51 | margin-bottom: 10px;
52 |
53 | .topic-header {
54 | display: flex;
55 | justify-content: space-between;
56 | align-items: center;
57 | }
58 | }
59 |
60 | ul {
61 | list-style: initial;
62 | }
63 |
64 | ol {
65 | list-style: decimal;
66 | }
67 |
68 | .reply-btn {
69 | margin-top: 10px;
70 | }
71 |
72 | .no-reply-item {
73 | margin: 20px 0;
74 |
75 | .ant-card-body {
76 | padding: 0;
77 | }
78 | }
79 | .ant-card-head {
80 | background: #B5CAC6;
81 | }
82 |
83 | .loading {
84 | display: flex;
85 | justify-content: center;
86 | padding: 20px 0;
87 | }
88 | }
89 |
90 | .detail-page {
91 | width: 100vw;
92 | height: 100vh;
93 |
94 | .comment {
95 | .ant-card-body {
96 | padding: 0;
97 | }
98 | }
99 |
100 | a {
101 | color: #108ee9;
102 | }
103 |
104 | .ant-card-head-title {
105 | width: 100%;
106 | }
107 |
108 | @media (max-width: 1000px) {
109 | .topic-detail {
110 | flex-direction: column;
111 | }
112 |
113 | .article {
114 | margin: 0;
115 | }
116 | }
117 | }
--------------------------------------------------------------------------------
/web/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