├── src
├── @types
│ └── .gitkeep
├── assets
│ ├── scss
│ │ ├── CV.scss
│ │ ├── app.scss
│ │ ├── about.scss
│ │ ├── iconfont
│ │ │ └── iconfont.css
│ │ ├── header.scss
│ │ ├── index.scss
│ │ ├── common
│ │ │ ├── reset.scss
│ │ │ └── common.scss
│ │ ├── detail.scss
│ │ ├── topic.scss
│ │ ├── user.scss
│ │ └── github-markdown.css
│ ├── .DS_Store
│ └── images
│ │ ├── .DS_Store
│ │ ├── index.png
│ │ ├── logo.png
│ │ ├── loading.gif
│ │ └── components
│ │ ├── user.png
│ │ ├── go_icon.png
│ │ ├── nav_icon.png
│ │ ├── go_next_icon.png
│ │ └── login_icon.png
├── constants
│ ├── userinfo.ts
│ ├── counter.ts
│ └── auth.ts
├── interfaces
│ ├── autho.d.ts
│ ├── auth.d.ts
│ ├── store.d.ts
│ ├── index.ts
│ ├── topic.d.ts
│ ├── member.d.ts
│ ├── node.d.ts
│ └── thread.d.ts
├── actions
│ ├── index.ts
│ └── auth.ts
├── reducers
│ ├── index.ts
│ └── auth.ts
├── components
│ ├── loading2
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── backtotop
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── topics
│ │ └── index.tsx
│ ├── loading
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── menu
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── topic
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── activityIndicator
│ │ ├── index.tsx
│ │ └── index.module.scss
│ ├── link
│ │ └── index.tsx
│ ├── layout
│ │ └── index.tsx
│ ├── user-info
│ │ └── index.tsx
│ ├── drawer
│ │ └── index.jsx
│ ├── header
│ │ └── index.tsx
│ └── reply
│ │ └── index.tsx
├── hoc
│ ├── redirect.ts
│ └── router.tsx
├── utils
│ ├── store
│ │ └── index.ts
│ └── request.ts
├── store
│ ├── index.ts
│ └── with-redux-store.tsx
├── ui
│ ├── index.module.scss
│ └── index.tsx
└── libs
│ └── utils.tsx
├── next-env.d.ts
├── public
└── favicon.ico
├── docs
└── ssr-deploy-flow.png
├── nodemon.json
├── sls.js
├── postcss.config.js
├── tsconfig.server.json
├── layer
└── serverless.yml
├── .env.example
├── server
├── cache
│ └── index.ts
└── index.ts
├── pages
├── _app.tsx
├── add
│ ├── index.module.scss
│ └── index.tsx
├── login
│ ├── index.module.scss
│ └── index.tsx
├── about
│ └── index.tsx
├── index
│ └── index.tsx
├── message
│ └── index.tsx
├── user
│ └── index.tsx
└── topic
│ └── index.tsx
├── next.config.js
├── .babelrc
├── LICENSE
├── .gitignore
├── serverless.yml
├── README.md
├── tsconfig.json
└── package.json
/src/@types/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/scss/CV.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/constants/userinfo.ts:
--------------------------------------------------------------------------------
1 | export const GET = 'GET'
2 | export const SET = 'SET'
3 |
--------------------------------------------------------------------------------
/src/constants/counter.ts:
--------------------------------------------------------------------------------
1 | export const ADD = 'ADD'
2 | export const MINUS = 'MINUS'
3 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/.DS_Store
--------------------------------------------------------------------------------
/src/interfaces/autho.d.ts:
--------------------------------------------------------------------------------
1 | export interface IAuthor {
2 | avatar_url: string;
3 | loginname: string;
4 | }
5 |
--------------------------------------------------------------------------------
/docs/ssr-deploy-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/docs/ssr-deploy-flow.png
--------------------------------------------------------------------------------
/src/assets/images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/.DS_Store
--------------------------------------------------------------------------------
/src/assets/images/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/index.png
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | auth,
3 | logout,
4 | setAuthRedirectPath,
5 | authCheckState
6 | } from './auth';
7 |
--------------------------------------------------------------------------------
/src/assets/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/loading.gif
--------------------------------------------------------------------------------
/src/assets/images/components/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/components/user.png
--------------------------------------------------------------------------------
/src/assets/images/components/go_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/components/go_icon.png
--------------------------------------------------------------------------------
/src/assets/images/components/nav_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/components/nav_icon.png
--------------------------------------------------------------------------------
/src/interfaces/auth.d.ts:
--------------------------------------------------------------------------------
1 | export interface IAuth {
2 | avatar_url: string;
3 | loginname: string;
4 | userId: string;
5 | token: string;
6 | }
7 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server", "public"],
3 | "exec": "ts-node --project tsconfig.server.json server/index.ts",
4 | "ext": "js ts"
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/images/components/go_next_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/components/go_next_icon.png
--------------------------------------------------------------------------------
/src/assets/images/components/login_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serverless-plus/serverless-cnode/HEAD/src/assets/images/components/login_icon.png
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import auth from "./auth";
3 |
4 | export default combineReducers({
5 | auth
6 | });
7 |
--------------------------------------------------------------------------------
/src/components/loading2/index.module.scss:
--------------------------------------------------------------------------------
1 | .loading2 {
2 | width: 100%;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | }
7 |
--------------------------------------------------------------------------------
/src/interfaces/store.d.ts:
--------------------------------------------------------------------------------
1 | export interface IStore {
2 | removeItem: Function;
3 | getItem: Function;
4 | setItem: Function;
5 | clear: Function;
6 | }
7 |
--------------------------------------------------------------------------------
/sls.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'production';
2 | process.env.SERVERLESS = true;
3 |
4 | const createServer = require('./dist')
5 |
6 | module.exports = createServer
7 |
--------------------------------------------------------------------------------
/src/components/backtotop/index.module.scss:
--------------------------------------------------------------------------------
1 | .icon-top {
2 | position: fixed;
3 | right: 10px;
4 | bottom: 80px;
5 | font-size: 48PX;
6 | z-index: 9999;
7 | color: #42b983;
8 | }
9 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | 'postcss-pxtransform': {
5 | platform: 'h5',
6 | designWidth: 750,
7 | },
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from '@interfaces/auth';
2 | export * from '@interfaces/autho';
3 | export * from '@interfaces/member';
4 | export * from '@interfaces/node';
5 | export * from '@interfaces/store';
6 | export * from '@interfaces/thread';
7 | export * from '@interfaces/topic';
8 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "dist",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "include": ["server/**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/layer/serverless.yml:
--------------------------------------------------------------------------------
1 | org: serverless-cnode
2 | app: serverless-cnode
3 | stage: dev
4 | component: layer
5 | name: serverless-cnode-layer
6 |
7 | inputs:
8 | region: ${env:REGION}
9 | name: ${name}
10 | src: ../node_modules
11 | runtimes:
12 | - Nodejs10.15
13 | - Nodejs12.16
14 |
--------------------------------------------------------------------------------
/src/assets/scss/app.scss:
--------------------------------------------------------------------------------
1 | @import 'common/reset.scss';
2 | @import 'common/common.scss';
3 | @import 'index.scss';
4 | @import 'header.scss';
5 | @import 'about.scss';
6 | @import 'topic.scss';
7 | @import 'user.scss';
8 | @import 'detail.scss';
9 | @import 'iconfont/iconfont.css';
10 | @import 'github-markdown.css';
--------------------------------------------------------------------------------
/src/interfaces/topic.d.ts:
--------------------------------------------------------------------------------
1 | import { IAuthor } from './autho';
2 | export interface ITopic {
3 | id: string;
4 | title: string;
5 | tab: string;
6 | good: boolean;
7 | top: boolean;
8 | author: IAuthor;
9 | reply_count: number;
10 | visit_count: number;
11 | create_at: string;
12 | last_reply_at: string;
13 | }
14 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # 腾讯云授权密钥
2 | TENCENT_SECRET_ID=xxx
3 | TENCENT_SECRET_KEY=xxx
4 |
5 | # 部署地区
6 | REGION=ap-guangzhou
7 |
8 | # 静态资源上传 COS 桶名称
9 | BUCKET=serverless-cnode
10 |
11 | # API 网关自定义域名 和 证书 ID
12 | APIGW_CUSTOM_DOMAIN=cnode.yuga.chat
13 | APIGW_CUSTOM_DOMAIN_CERTID=xxx
14 |
15 | # CDN 域名,证书 ID
16 | CDN_DOMAIN=static.cnode.yuga.chat
17 | CDN_DOMAIN_CERTID=xxx
--------------------------------------------------------------------------------
/src/hoc/redirect.ts:
--------------------------------------------------------------------------------
1 | import Router from 'next/router';
2 |
3 | export default (context, target) => {
4 | if (context.res) {
5 | // server
6 | // 303: "See other"
7 | context.res.writeHead(303, { Location: target });
8 | context.res.end();
9 | } else {
10 | // In the browser, we just pretend like this never even happened ;)
11 | Router.replace(target);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/interfaces/member.d.ts:
--------------------------------------------------------------------------------
1 | export interface IMember {
2 | username: string;
3 | website?: null;
4 | github?: null;
5 | psn?: null;
6 | avatar_normal: string;
7 | bio?: null;
8 | url: string;
9 | tagline?: null;
10 | twitter?: null;
11 | created: number;
12 | avatar_large: string;
13 | avatar_mini: string;
14 | location?: null;
15 | btc?: null;
16 | id: number;
17 | }
18 |
--------------------------------------------------------------------------------
/src/interfaces/node.d.ts:
--------------------------------------------------------------------------------
1 | export interface INode {
2 | avatar_large: string;
3 | name: string;
4 | avatar_normal: string;
5 | title: string;
6 | url: string;
7 | topics: number;
8 | footer: string;
9 | header: string;
10 | title_alternative: string;
11 | avatar_mini: string;
12 | stars: number;
13 | root: boolean;
14 | id: number;
15 | parent_node_name: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/interfaces/thread.d.ts:
--------------------------------------------------------------------------------
1 | import { INode } from "./node";
2 | import { IMember } from "./member";
3 |
4 | export interface IThread {
5 | node: INode;
6 | member: IMember;
7 | last_reply_by: string;
8 | last_touched: number;
9 | title: string;
10 | url: string;
11 | created: number;
12 | content: string;
13 | content_rendered: string;
14 | last_modified: number;
15 | replies: number;
16 | id: number;
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/scss/about.scss:
--------------------------------------------------------------------------------
1 | .about-info {
2 | height: 100%;
3 | padding: 100px 15px;
4 | line-height: 1.5;
5 | background: #f7f7f7;
6 |
7 | dt {
8 | @extend .title;
9 | padding: 1em 0;
10 | font-weight: bold;
11 | }
12 | dd {
13 | padding-bottom: 15px;
14 | font-size: $font-content;
15 | border-bottom: $border;
16 | }
17 |
18 | a {
19 | color: #42b983;
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/server/cache/index.ts:
--------------------------------------------------------------------------------
1 | import LRU from 'lru-cache';
2 | // github.com/isaacs/node-lru-cache
3 |
4 | const options = {
5 | max: 500,
6 | length: function (n, key) {
7 | return n * 2 + key.length;
8 | },
9 | maxAge: 1000 * 60 * 60,
10 | },
11 | cache = new LRU(options); // sets just the max size
12 |
13 | export default cache;
14 |
15 | // export const set = (key, value, maxAge?) => {
16 | // return cache.set(key, value, maxAge);
17 | // };
18 |
19 | // export const get = (key) => {
20 | // return cache.get(key);
21 | // };
22 |
--------------------------------------------------------------------------------
/src/components/loading2/index.tsx:
--------------------------------------------------------------------------------
1 | import { View } from '@ui';
2 | import { Component } from 'react';
3 | import ActivityIndicator from '@components/activityIndicator';
4 |
5 | import styles from './index.module.scss';
6 |
7 | export default class Loading extends Component<{
8 | height: string;
9 | }> {
10 | render() {
11 | const { height = '8rem' } = this.props;
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/store/index.ts:
--------------------------------------------------------------------------------
1 | import { IStore } from '@interfaces';
2 | const isServer = typeof window === 'undefined';
3 |
4 | class Store implements IStore {
5 | removeItem(key) {
6 | return localStorage.removeItem(key);
7 | }
8 | getItem(key) {
9 | if (isServer) {
10 | return '{}';
11 | }
12 | return localStorage.getItem(key);
13 | }
14 | setItem(key, value) {
15 | return localStorage.setItem(key, value);
16 | }
17 | clear() {
18 | return localStorage.clear();
19 | }
20 | }
21 |
22 | const instance = new Store();
23 | export default instance;
24 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import thunkMiddleware from 'redux-thunk';
3 | import { createLogger } from 'redux-logger';
4 | import rootReducer from '../reducers';
5 | const isServer = typeof window === 'undefined';
6 |
7 | let middlewares = [thunkMiddleware];
8 | if (isServer) {
9 | middlewares = middlewares.concat([createLogger()]);
10 | }
11 | export const initializeStore = (initialState = {}) => {
12 | const store = createStore(
13 | rootReducer,
14 | initialState,
15 | applyMiddleware(...middlewares),
16 | );
17 | return store;
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/topics/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View } from '@ui';
3 | import { Topic } from '@components/topic';
4 | import { ITopic } from '@interfaces/topic';
5 |
6 | interface IProps {
7 | topics: ITopic[];
8 | }
9 |
10 | class TopicsList extends Component {
11 | static defaultProps = {
12 | topics: [],
13 | };
14 |
15 | render() {
16 | const { topics } = this.props;
17 | const element = topics.map((topic) => {
18 | return ;
19 | });
20 |
21 | return {element};
22 | }
23 | }
24 |
25 | export { TopicsList };
26 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import App from 'next/app';
2 | import React from 'react';
3 | import { Provider } from 'react-redux';
4 | import { Store } from 'redux';
5 | import { AppProps } from 'next/app';
6 | import withReduxStore from '@store/with-redux-store';
7 |
8 | import '@assets/scss/app.scss';
9 |
10 | type APageProps = {
11 | reduxStore: Store;
12 | props: any;
13 | };
14 |
15 | type MyAppProps = AppProps & APageProps;
16 |
17 | // @ts-ignore
18 | class MyApp extends App {
19 | render() {
20 | // @ts-ignore
21 | const { Component, pageProps, reduxStore } = this.props;
22 | return (
23 |
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | export default withReduxStore(MyApp);
31 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
3 | const withPlugins = require('next-compose-plugins');
4 |
5 | const isProd = process.env.NODE_ENV === 'production';
6 |
7 | // if not use CDN, change to your cos access domain
8 | const STATIC_URL = `https://static.cnode.yuga.chat`;
9 |
10 |
11 | const config = withPlugins([], {
12 | env: {
13 | STATIC_URL: isProd
14 | ? STATIC_URL
15 | : `http://localhost:${parseInt(process.env.PORT, 10) || 8000}`,
16 | },
17 | assetPrefix: isProd ? STATIC_URL : '',
18 | webpack(config, options) {
19 | config.plugins = config.plugins || [];
20 | if (options.isServer) config.plugins.push(new ForkTsCheckerWebpackPlugin());
21 |
22 | return config;
23 | },
24 | });
25 |
26 | module.exports = config;
27 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | [
5 | "module-resolver",
6 | {
7 | "root": ["./"],
8 | "alias": {
9 | "@assets": "./src/assets",
10 | "@components": "./src/components",
11 | "@ui": "./src/ui",
12 | "@hoc": "./src/hoc",
13 | "@utils": "./src/utils",
14 | "@libs": "./src/libs",
15 | "@interfaces": "./src/interfaces",
16 | "@actions": "./src/actions",
17 | "@store": "./src/store",
18 | "@constants": "./src/constants"
19 | }
20 | }
21 | ],
22 | [
23 | "@babel/plugin-proposal-decorators",
24 | {
25 | "decoratorsBeforeExport": true
26 | }
27 | ],
28 | ["@babel/plugin-proposal-class-properties"],
29 | ["@babel/plugin-proposal-object-rest-spread"]
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/loading/index.module.scss:
--------------------------------------------------------------------------------
1 | @keyframes loading {
2 | 0% {
3 | transform: rotate(0deg);
4 | }
5 |
6 | 100% {
7 | transform: rotate(360deg);
8 | }
9 | }
10 |
11 | .at-loading {
12 | display: inline-block;
13 | position: relative;
14 | width: 18PX;
15 | height: 18PX;
16 |
17 | &__ring {
18 | box-sizing: border-box;
19 | display: block;
20 | position: absolute;
21 | width: 18PX;
22 | height: 18PX;
23 | margin: 1PX;
24 | border: 1PX solid #fff;
25 | border-radius: 50%;
26 | animation: loading 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
27 | border-color: #fff transparent transparent transparent;
28 |
29 | &:nth-child(1) {
30 | animation-delay: -0.45s;
31 | }
32 |
33 | &:nth-child(2) {
34 | animation-delay: -0.3s;
35 | }
36 |
37 | &:nth-child(3) {
38 | animation-delay: -0.15s;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/utils/request.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import * as utils from '../libs/utils';
3 |
4 | export const post = async (options, data?) => {
5 | if (typeof options == 'string') {
6 | options = {
7 | url: options,
8 | };
9 | }
10 | if (utils.isObject(data)) {
11 | options.data = data;
12 | }
13 | return await axios.request({
14 | header: {
15 | 'Content-Type': 'application/x-www-form-urlencoded',
16 | Accept: 'application/json',
17 | },
18 | ...options,
19 | data: utils.param(options.data),
20 | method: 'post',
21 | });
22 | };
23 |
24 | export const get = async (options, data?) => {
25 | if (typeof options == 'string') {
26 | options = {
27 | url: options,
28 | };
29 | }
30 | if (utils.isObject(data)) {
31 | options.data = data;
32 | }
33 | return await axios.get(options.url, { ...options, params: options.data });
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/menu/index.module.scss:
--------------------------------------------------------------------------------
1 | .nav-list {
2 | position: fixed;
3 | top: 0;
4 | bottom: 0;
5 | left: -460px;
6 | width: 460px;
7 | background-color: #fff;
8 | color: #313131;
9 | transition: all .3s ease;
10 | z-index: 99;
11 |
12 | &.show {
13 | transform: translateX(460px);
14 | }
15 | }
16 |
17 | /*侧边栏列表*/
18 | .list-ul {
19 | margin: 0 24px;
20 | border-top: 1px solid #d4d4d4;
21 | overflow: hidden;
22 | padding-top: 9%;
23 |
24 | .item {
25 | display: block;
26 | // font-size: 14px;
27 | font-weight: 200;
28 | padding: 9% 0;
29 | text-align: left;
30 | text-indent: 1px;
31 | // line-height: 15px;
32 | color: #313131;
33 | font-weight: 700;
34 |
35 | &:last-child {
36 | margin-bottom: 50px;
37 | }
38 |
39 | &:before {
40 | color: #2c3e50;
41 | }
42 | }
43 |
44 | .line {
45 | border-top: 1px solid #d4d4d4;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/constants/auth.ts:
--------------------------------------------------------------------------------
1 | export const ADD_INGREDIENT = "ADD_INGREDIENT";
2 | export const REMOVE_INGREDIENT = "REMOVE_INGREDIENT";
3 | export const SET_INGREDIENTS = "SET_INGREDIENTS";
4 | export const FETCH_INGREDIENTS_FAILED = "FETCH_INGREDIENTS_FAILED";
5 |
6 | export const PURCHASE_BURGER_START = "PURCHASE_BURGER_START";
7 | export const PURCHASE_BURGER_SUCCESS = "PURCHASE_BURGER_SUCCESS";
8 | export const PURCHASE_BURGER_FAIL = "PURCHASE_BURGER_FAIL";
9 | export const PURCHASE_INIT = "PURCHASE_INIT";
10 |
11 | export const FETCH_ORDERS_START = "FETCH_ORDERS_START";
12 | export const FETCH_ORDERS_SUCCESS = "FETCH_ORDERS_SUCCESS";
13 | export const FETCH_ORDERS_FAIL = "FETCH_ORDERS_FAIL";
14 |
15 | export const AUTH_START = "AUTH_START";
16 | export const AUTH_SUCCESS = "AUTH_SUCCESS";
17 | export const AUTH_FAIL = "AUTH_FAIL";
18 | export const AUTH_LOGOUT = "AUTH_LOGOUT";
19 |
20 | export const SET_AUTH_REDIRECT_PATH = "SET_AUTH_REDIRECT_PATH";
21 |
--------------------------------------------------------------------------------
/pages/add/index.module.scss:
--------------------------------------------------------------------------------
1 | .add-container {
2 | margin-top: 100px;
3 | background-color: #fff;
4 | }
5 |
6 | .line {
7 | padding: 10px 15px;
8 | border-bottom: solid 1px #d4d4d4;
9 | font-size: 30px;
10 | }
11 |
12 | .add-btn {
13 | color: #fff;
14 | background-color: #80bd01;
15 | padding: 10px 25px;
16 | border-radius: 5px;
17 | vertical-align: middle;
18 | display: inline;
19 | }
20 |
21 | .add-tab {
22 | display: inline-block;
23 | width: calc(100% - 220px);
24 | min-width: 50%;
25 | background: transparent;
26 | padding: 3px;
27 | margin: 3px 6px;
28 | vertical-align: middle;
29 | border: 1px solid #222;
30 | }
31 |
32 | .add-title {
33 | font-size: 16px;
34 | border: none;
35 | width: 100%;
36 | background: transparent;
37 | height: 25px;
38 | }
39 |
40 | .err {
41 | border: solid 1px red;
42 | }
43 |
44 | .add-content {
45 | margin: 15px 2%;
46 | width: 96%;
47 | border-color: #d4d4d4;
48 | color: #000;
49 | }
50 |
51 | .err {
52 | border: solid 1px red;
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/topic/index.module.scss:
--------------------------------------------------------------------------------
1 | .topic {
2 | display: flex;
3 | padding: 15px;
4 | border-bottom: 1px solid #f1f1f1;
5 | flex-direction: column;
6 | }
7 |
8 | .info {
9 | display: flex;
10 | // height: 65px;
11 | // margin-bottom: 5px;
12 | // margin-top: 5px;
13 |
14 | .author {
15 | font-size: 28px;
16 | }
17 |
18 | .replies {
19 | font-size: 20px;
20 | color: darkgray;
21 | }
22 | }
23 |
24 | .bold {
25 | font-size: 32px;
26 | font-weight: bold;
27 | }
28 |
29 | .avatar {
30 | max-width: 65px;
31 | max-height: 65px;
32 | border-radius: 50%;
33 | margin-right: 10px;
34 | }
35 |
36 | .middle {
37 | flex: 1;
38 | display: flex;
39 | margin-left: 10px;
40 | flex-direction: column;
41 | }
42 |
43 | .mr10 {
44 | margin-right: 10px;
45 | }
46 |
47 | .node {
48 | float: right;
49 |
50 | .tag {
51 | font-size: 24px;
52 | padding: 5px 15px;
53 | float: right;
54 | background-color: #eeeeee;
55 | border-radius: 5px;
56 | }
57 | }
58 |
59 | .title {
60 | margin-top: 10px;
61 | font-size: 48px;
62 | }
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Serverless Plus
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 |
--------------------------------------------------------------------------------
/pages/login/index.module.scss:
--------------------------------------------------------------------------------
1 | .page-body {
2 | margin-top: 100px;
3 | padding: 50px 15px;
4 | min-height: 400px;
5 | background-color: #fff;
6 | .label {
7 | display: inline-block;
8 | width: 100%;
9 | margin-top: 15px;
10 | position: relative;
11 | left: 0;
12 | top: 0;
13 |
14 | .txt {
15 | padding: 12px 0;
16 | border: none;
17 | border-bottom: 1px solid #4fc08d;
18 | background-color: transparent;
19 | width: 100%;
20 | font-size: 24px;
21 | color: #313131;
22 | }
23 |
24 | .button {
25 | display: inline-block;
26 | width: 99%;
27 | height: 80px;
28 | line-height: 80px;
29 | border-radius: 3px;
30 | color: #fff;
31 | font-size: 32px;
32 | background-color: #4fc08d;
33 | border: none;
34 | border-bottom: 4px solid #3aa373;
35 | text-align: center;
36 | vertical-align: middle;
37 | }
38 |
39 | .file {
40 | position: absolute;
41 | top: 0;
42 | left: 0;
43 | height: 42px;
44 | width: 48%;
45 | outline: medium none;
46 | opacity: 0;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | dist
64 |
--------------------------------------------------------------------------------
/src/components/activityIndicator/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import classNames from 'classnames';
3 | import Loading from '@components/loading';
4 | import { View, Text } from '@ui';
5 |
6 | import './index.module.scss';
7 |
8 | interface Iprops {
9 | size?: number;
10 | mode?: 'center' | 'normal';
11 | color?: string;
12 | content?: string;
13 | className?: string;
14 | }
15 |
16 | export default class ActivityIndicator extends Component {
17 | static defaultProps = {
18 | size: 24,
19 | color: '#6190E8',
20 | };
21 | render() {
22 | const { color, size, mode, content } = this.props;
23 |
24 | const rootClass = classNames(
25 | 'at-activity-indicator',
26 | {
27 | 'at-activity-indicator--center': mode === 'center',
28 | },
29 | this.props.className,
30 | );
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | {content && (
38 | {content}
39 | )}
40 |
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/activityIndicator/index.module.scss:
--------------------------------------------------------------------------------
1 | $line-height-base: 1; // 单行
2 | $hd: 2; // 基本单位
3 | $color-grey-2: #999;
4 | $spacing-h-lg: 12px * $hd;
5 | /**
6 | * 元素居中定位
7 | */
8 | @mixin absolute-center($pos: absolute) {
9 | position: $pos;
10 | top: 50%;
11 | left: 50%;
12 | transform: translate(-50%, -50%);
13 | }
14 |
15 | /* Flex Item */
16 | @mixin flex($fg: 1, $fs: null, $fb: null) {
17 | flex: $fg $fs $fb;
18 | -webkit-box-flex: $fg;
19 | }
20 |
21 | @mixin flex-order($n) {
22 | order: $n;
23 | -webkit-box-ordinal-group: $n;
24 | }
25 |
26 | @mixin align-self($value: auto) {
27 | align-self: $value;
28 | }
29 |
30 | @mixin display-flex {
31 | display: flex;
32 | }
33 |
34 | .at-activity-indicator {
35 | @include display-flex();
36 |
37 | line-height: $line-height-base;
38 |
39 | &--center {
40 | @include absolute-center;
41 | }
42 |
43 | &__body {
44 | @include flex(0, 0, auto);
45 |
46 | line-height: 0;
47 | }
48 |
49 | &__content {
50 | @include flex(0, 0, auto);
51 | @include align-self(center);
52 |
53 | font-size: 28px;
54 | margin-left: $spacing-h-lg;
55 | color: $color-grey-2;
56 | }
57 | }
--------------------------------------------------------------------------------
/pages/about/index.tsx:
--------------------------------------------------------------------------------
1 | // import { ComponentClass } from 'react'
2 | import React, { Component } from 'react';
3 | import { ScrollView } from '@ui';
4 | import Header from '@components/header';
5 | import Layout from '@components/layout';
6 |
7 | class About extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 | 关于项目
14 |
15 | 使用 Next.js + TypeScript 开发,并且基于 Serverless 部署的 cnode
16 | 客户端
17 |
18 | 源码地址
19 |
20 |
21 | https://github.com/serverless-plus/serverless-cnode
22 |
23 |
24 | 意见反馈
25 |
26 |
27 | 发表意见或者提需求
28 |
29 |
30 | 当前版本
31 | V0.0.1
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default About;
39 |
--------------------------------------------------------------------------------
/src/components/link/index.tsx:
--------------------------------------------------------------------------------
1 | import { View } from '@ui';
2 | import React, { Component } from 'react';
3 | import { withRouter } from 'next/router';
4 |
5 | import * as utils from '@libs/utils';
6 |
7 | interface IProps {
8 | to: {
9 | url: string;
10 | params?: object;
11 | };
12 | style?: object;
13 | className?: string;
14 | children?: React.ReactNode;
15 | }
16 | interface PageState {}
17 |
18 | class Link extends Component {
19 | static defaultProps = {
20 | to: {
21 | url: '',
22 | params: {},
23 | },
24 | style: {},
25 | className: '',
26 | children: '',
27 | };
28 |
29 | goTo = ({ url, params }) => {
30 | const href = url + (params ? '?' + utils.param(params) : '');
31 | // Router.push(href, as);
32 | window.location.href = href;
33 | return false;
34 | };
35 | render() {
36 | const { className, style, to, children, ...rest } = this.props;
37 | const withpointer = { ...style, cursor: 'pointer' };
38 | return (
39 |
44 | {children}
45 |
46 | );
47 | }
48 | }
49 |
50 | export default withRouter(Link as React.ComponentType);
51 |
--------------------------------------------------------------------------------
/src/components/loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View } from '@ui';
3 | import PropTypes from 'prop-types';
4 |
5 | import styles from './index.module.scss';
6 |
7 | export default class Loading extends Component<{
8 | size;
9 | color;
10 | }> {
11 | static defaultProps = {
12 | size: '18',
13 | color: '#fff',
14 | };
15 |
16 | static propTypes = {
17 | size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
18 | color: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
19 | };
20 |
21 | render() {
22 | const { color, size } = this.props;
23 | const sizeStyle = {
24 | width: `${size}px`,
25 | height: `${size}px`,
26 | };
27 | const colorStyle = {
28 | border: `1px solid ${color}`,
29 | borderColor: `${color} transparent transparent transparent`,
30 | };
31 | const ringStyle = Object.assign({}, colorStyle, sizeStyle);
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/assets/scss/iconfont/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'iconfont';
3 | src: url('//at.alicdn.com/t/font_1449145493_7316074.eot'); /* IE9*/
4 | src: url('//at.alicdn.com/t/font_1449145493_7316074.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('//at.alicdn.com/t/font_1449145493_7316074.woff') format('woff'), /* chrome、firefox */
6 | url('//at.alicdn.com/t/font_1449145493_7316074.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
7 | url('//at.alicdn.com/t/font_1449145493_7316074.svg#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 |
11 | .iconfont {
12 | font-family:"iconfont" !important;
13 | font-size: 32px;
14 | font-style:normal;
15 | -webkit-font-smoothing: antialiased;
16 | -webkit-text-stroke-width: 0.2px;
17 | -moz-osx-font-smoothing: grayscale;
18 | }
19 | .icon-tianjia:before { content: "\e60d"; margin-right: 30px; }
20 | .icon-fenxiang:before { content: "\e600"; margin-right: 30px; }
21 | .icon-shezhi:before { content: "\e601"; margin-right: 30px;}
22 | .icon-wenda:before { content: "\e602"; margin-right: 30px;}
23 | .icon-hao:before { content: "\e603"; margin-right: 30px;}
24 | .icon-quanbu:before { content: "\e604"; margin-right: 30px;}
25 | .icon-zhaopin:before { content: "\e605"; margin-right: 30px;}
26 | .icon-xiaoxi:before { content: "\e606"; margin-right: 30px;}
27 | .icon-about:before { content: "\e607"; margin-right: 30px;}
28 |
--------------------------------------------------------------------------------
/src/components/layout/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { View } from '@ui';
3 | import Head from 'next/head';
4 |
5 | export default class Layout extends Component<{
6 | className?: string;
7 | title?: string;
8 | }> {
9 | render() {
10 | const { children, className, title } = this.props;
11 | const clsName = className || 'flex-wrp';
12 |
13 | const staticMarkup = `!function(x){function w() { var v, u, t, tes, s = x.document, r = s.documentElement, a = r.getBoundingClientRect().width; if (!v && !u) { var n = !!x.navigator.appVersion.match(/AppleWebKit.*Mobile.*/); v = x.devicePixelRatio; tes = x.devicePixelRatio; v = n ? v : 1, u = 1 / v } if (a >= 640) { r.style.fontSize = "40px" } else { if (a <= 320) { r.style.fontSize = "20px" } else { r.style.fontSize = a / 320 * 20 + "px" } } }x.addEventListener("resize",function(){w()});w()}(window);`;
14 |
15 | return (
16 |
17 |
18 |
19 |
23 | {title ? {title} : ''}
24 |
25 |
26 |
27 | {children}
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | org: serverless-cnode
2 | app: serverless-cnode
3 | stage: dev
4 | component: nextjs
5 | name: serverless-cnode
6 |
7 | inputs:
8 | src:
9 | dist: ./
10 | hook: npm run build:sls
11 | exclude:
12 | - .env
13 | - '.git/**'
14 | - 'docs/**'
15 | - '.next/cache/**'
16 | - 'node_modules/**'
17 | region: ${env:REGION}
18 | runtime: Nodejs10.15
19 | functionName: serverless-cnode
20 | layers:
21 | - name: ${output:${stage}:${app}:${name}-layer.name}
22 | version: ${output:${stage}:${app}:${name}-layer.version}
23 | functionConf:
24 | timeout: 10
25 | environment:
26 | variables:
27 | NODE_ENV: production
28 | SERVERLESS: true
29 | apigatewayConf:
30 | protocols:
31 | - http
32 | - https
33 | environment: release
34 | enableCORS: true
35 | customDomains:
36 | - domain: ${env:APIGW_CUSTOM_DOMAIN}
37 | certificateId: ${env:APIGW_CUSTOM_DOMAIN_CERTID}
38 | isDefaultMapping: false
39 | pathMappingSet:
40 | - path: /
41 | environment: release
42 | protocols:
43 | - http
44 | - https
45 | staticConf:
46 | cosConf:
47 | bucket: ${env:BUCKET}
48 | cdnConf:
49 | # after you deploy CDN once, just set onlyRefresh to true for refresh CDN cache
50 | onlyRefresh: true
51 | domain: ${env:CDN_DOMAIN}
52 | https:
53 | certId: ${env:CDN_DOMAIN_CERTID}
54 |
--------------------------------------------------------------------------------
/src/ui/index.module.scss:
--------------------------------------------------------------------------------
1 | .taro-img {
2 | display: inline-block;
3 | overflow: hidden;
4 | position: relative;
5 | font-size: 0;
6 | width: 320px;
7 | height: 240px;
8 |
9 | &.taro-img__widthfix {
10 | height: 100%;
11 | }
12 |
13 | &__mode {
14 | &-scaletofill {
15 | width: 100%;
16 | height: 100%;
17 | }
18 |
19 | &-aspectfit {
20 | max-width: 100%;
21 | max-height: 100%;
22 | }
23 |
24 | &-aspectfill {
25 | min-width: 100%;
26 | height: 100%;
27 | }
28 |
29 | &-widthfix {
30 | width: 100%;
31 | }
32 |
33 | &-top {
34 | width: 100%;
35 | }
36 |
37 | &-bottom {
38 | width: 100%;
39 | position: absolute;
40 | bottom: 0;
41 | }
42 |
43 | &-left {
44 | height: 100%;
45 | }
46 |
47 | &-right {
48 | position: absolute;
49 | height: 100%;
50 | right: 0;
51 | }
52 |
53 | &-topleft {}
54 |
55 | &-topright {
56 | position: absolute;
57 | right: 0;
58 | }
59 |
60 | &-bottomleft {
61 | position: absolute;
62 | bottom: 0;
63 | }
64 |
65 | &-bottomright {
66 | position: absolute;
67 | right: 0;
68 | bottom: 0;
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Serverless Cnode
2 |
3 | [在线预览](https://cnode.yuga.chat)
4 |
5 | 使用 Next.js + TypeScript 开发,并且基于 Serverless 部署的 cnode 客户端
6 |
7 | ## 流程图
8 |
9 | 
10 |
11 | ## 功能
12 |
13 | - [x] Typescript
14 | - [x] Next.js
15 | - [x] 自定义 Express Server
16 | - [x] LRU Render Cache
17 | - [x] 基于 Serverless Next.js 组件部署
18 | - [x] **静态资源分离,自动部署到 COS**
19 | - [x] **自动为静态 COS 配置 CDN**
20 | - [x] **node_modules 基于层部署,大大提高部署效率**
21 |
22 | ## 本地开发
23 |
24 | ```bash
25 | $ npm install
26 |
27 | $ npm run dev
28 | ```
29 |
30 | ## 构建
31 |
32 | ```bash
33 | $ npm run build
34 | ```
35 |
36 | ## 配置
37 |
38 | 在部署到 Serverless 前,将 `.env.example` 重命名为 `.env`,并请完成如下配置:
39 |
40 | ```dotenv
41 | # 腾讯云授权密钥
42 | TENCENT_APP_ID=xxx
43 | TENCENT_SECRET_ID=xxx
44 | TENCENT_SECRET_KEY=xxx
45 |
46 | # 部署地区
47 | REGION=ap-guangzhou
48 |
49 | # 静态资源上传 COS 桶名称
50 | BUCKET=serverless-cnode
51 |
52 | # API 网关自定义域名 和 证书 ID
53 | APIGW_CUSTOM_DOMAIN=cnode.yuga.chat
54 | APIGW_CUSTOM_DOMAIN_CERTID=xxx
55 |
56 | # CDN 域名,证书 ID
57 | CDN_DOMAIN=static.cnode.yuga.chat
58 | CDN_DOMAIN_CERTID=xxx
59 | ```
60 |
61 | > 注意:如果不需要使用 CDN,直接使用 COS 自动生成的域名,也是可以的,只需要删除
62 | > `serverless.yml` 中的 `cdnConf` 即可。
63 |
64 | ## 部署
65 |
66 | 此项目会先将 `node_modules` 部署到
67 | [层](https://cloud.tencent.com/document/product/583/40159),然后在部署项目代码,
68 | 这样下次部署项目时,如果 `node_modules` 没有修改,我们就不需要部署庞大的
69 | `node_modules` 文件夹了。
70 |
71 | 1. 部署层:
72 |
73 | ```bash
74 | $ npm run deploy:layer
75 | ```
76 |
77 | > 注意:如果项目 `node_modules` 没有变更,就不需要执行此命令。
78 |
79 | 2. 部署业务代码:
80 |
81 | ```bash
82 | $ npm run deploy
83 | ```
84 |
85 | ## License
86 |
87 | MIT
88 |
--------------------------------------------------------------------------------
/src/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 |
4 | import styles from './index.module.scss';
5 |
6 | export const View = (props) => {
7 | const { children, className, ...reset } = props;
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | export const ScrollView = (props) => {
16 | const { children, className, ...reset } = props;
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | };
23 |
24 | export const Text = (props) => {
25 | const { children, className, ...reset } = props;
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | interface Imageprops {
34 | src: string;
35 | mode?: 'widthFix' | 'scaleToFill';
36 | onLoad?: React.ReactEventHandler;
37 | onError?: React.ReactEventHandler;
38 | style?: any;
39 | className?: string;
40 | }
41 |
42 | export const Image = ({
43 | className,
44 | src,
45 | style,
46 | mode,
47 | onLoad,
48 | onError,
49 | ...reset
50 | }: Imageprops) => {
51 | const cls = classNames(
52 | styles['taro-img'],
53 | mode === 'widthFix' && style['taro-img__widthfix'],
54 | className,
55 | );
56 | const imgCls =
57 | 'taro-img__mode-' +
58 | (mode || 'scaleToFill').toLowerCase().replace(/\s/g, '');
59 |
60 | return (
61 |
62 |

68 |
69 | );
70 | };
71 |
--------------------------------------------------------------------------------
/src/store/with-redux-store.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from "react";
2 | import { initializeStore } from './'
3 | const isServer = typeof window === 'undefined'
4 | const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'
5 |
6 | function getOrCreateStore(initialState = {}) {
7 | // Always make a new store if server, otherwise state is shared between requests
8 | if (isServer) {
9 | return initializeStore(initialState)
10 | }
11 |
12 | // Create store if unavailable on the client and set it on the window object
13 | if (!window[__NEXT_REDUX_STORE__]) {
14 | window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
15 | }
16 | return window[__NEXT_REDUX_STORE__]
17 | }
18 |
19 |
20 |
21 | export default (RApp): React.ReactNode => {
22 | return class AppWithRedux extends Component {
23 | static async getInitialProps(appContext) {
24 | // Get or Create the store with `undefined` as initialState
25 | // This allows you to set a custom default initialState
26 | const reduxStore = getOrCreateStore();
27 |
28 | // Provide the store to getInitialProps of pages
29 | appContext.ctx.reduxStore = reduxStore;
30 |
31 | let appProps = {};
32 | if (typeof RApp.getInitialProps === "function") {
33 | appProps = await RApp.getInitialProps.call(RApp, appContext);
34 | }
35 |
36 | return { ...appProps, initialReduxState: reduxStore.getState() };
37 | }
38 | constructor(props) {
39 | super(props);
40 | // @ts-ignore
41 | this.reduxStore = getOrCreateStore(props.initialReduxState);
42 | }
43 | render() {
44 | // @ts-ignore
45 | return ;
46 | }
47 | };
48 | };
49 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "jsx": "preserve",
8 | "allowJs": true,
9 | "experimentalDecorators": true,
10 | "allowSyntheticDefaultImports": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "removeComments": false,
14 | "preserveConstEnums": true,
15 | "sourceMap": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "strict": false,
19 | "noEmit": true,
20 | "esModuleInterop": true,
21 | "resolveJsonModule": true,
22 | "isolatedModules": true,
23 | "baseUrl": ".",
24 | "paths": {
25 | "@assets": ["src/assets"],
26 | "@assets/*": ["src/assets/*"],
27 | "@components": ["src/components"],
28 | "@components/*": ["src/components/*"],
29 | "@ui": ["src/ui"],
30 | "@ui/*": ["src/ui/*"],
31 | "@hoc": ["src/hoc"],
32 | "@hoc/*": ["src/hoc/*"],
33 | "@utils": ["src/utils"],
34 | "@utils/*": ["src/utils/*"],
35 | "@libs": ["src/libs"],
36 | "@libs/*": ["src/libs/*"],
37 | "@interfaces": ["src/interfaces"],
38 | "@interfaces/*": ["src/interfaces/*"],
39 | "@actions": ["src/actions"],
40 | "@actions/*": ["src/actions/*"],
41 | "@store": ["src/store"],
42 | "@store/*": ["src/store/*"],
43 | "@constants": ["src/constants"],
44 | "@constants/*": ["src/constants/*"]
45 | }
46 | },
47 | "exclude": [
48 | "node_modules",
49 | "dist",
50 | ".next",
51 | "out",
52 | "next.config.js",
53 | ".babelrc",
54 | "bundles",
55 | "coverage",
56 | "test/*"
57 | ],
58 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
59 | }
60 |
--------------------------------------------------------------------------------
/src/reducers/auth.ts:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../constants/auth';
2 | import { updateObject } from '../libs/utils';
3 |
4 | const initialState = {
5 | // token: null,
6 | // userId: null,
7 | error: null,
8 | loading: false,
9 | loginname: null,
10 | avatar_url: null,
11 | userId: null,
12 | token: null,
13 | authRedirectPath: '/',
14 | };
15 | // @ts-ignore
16 | const authStart = (state, action) => {
17 | return updateObject(state, { error: null, loading: true });
18 | };
19 |
20 | const authSuccess = (state, action) => {
21 | return updateObject(state, {
22 | token: action.token,
23 | userId: action.userId,
24 | loginname: action.loginname,
25 | avatar_url: action.avatar_url,
26 | error: null,
27 | loading: false,
28 | });
29 | };
30 |
31 | const authFail = (state, action) => {
32 | return updateObject(state, {
33 | error: action.error,
34 | loading: false,
35 | });
36 | };
37 | // @ts-ignore
38 | const authLogout = (state, action) => {
39 | return updateObject(state, {
40 | loginname: null,
41 | avatar_url: null,
42 | userId: null,
43 | token: null,
44 | });
45 | };
46 |
47 | const setAuthRedirectPath = (state, action) => {
48 | return updateObject(state, { authRedirectPath: action.path });
49 | };
50 |
51 | const reducer = (state = initialState, action) => {
52 | switch (action.type) {
53 | case actionTypes.AUTH_START:
54 | return authStart(state, action);
55 | case actionTypes.AUTH_SUCCESS:
56 | return authSuccess(state, action);
57 | case actionTypes.AUTH_FAIL:
58 | return authFail(state, action);
59 | case actionTypes.AUTH_LOGOUT:
60 | return authLogout(state, action);
61 | case actionTypes.SET_AUTH_REDIRECT_PATH:
62 | return setAuthRedirectPath(state, action);
63 | default:
64 | return state;
65 | }
66 | };
67 |
68 | export default reducer;
69 |
--------------------------------------------------------------------------------
/src/components/backtotop/index.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentClass } from 'react';
2 | import React, { Component } from 'react';
3 | import { View } from '@ui';
4 | import { throttle } from 'throttle-debounce';
5 | import * as utils from '@libs/utils';
6 |
7 | import styles from './index.module.scss';
8 |
9 | type PageOwnProps = {};
10 |
11 | type PageState = {};
12 |
13 | type IProps = PageOwnProps;
14 |
15 | interface BackTop {
16 | props: IProps;
17 | scrollbinding: () => void;
18 | }
19 |
20 | class BackTop extends Component {
21 | componentScrollBox;
22 | constructor(props) {
23 | super(props);
24 | if (utils.getEnv() == 'WEB') {
25 | this.componentScrollBox = document.documentElement;
26 | }
27 | }
28 | state = {
29 | show: false,
30 | };
31 |
32 | componentWillUnmount() {
33 | if (utils.getEnv() == 'WEB') {
34 | window.removeEventListener('scroll', this.scrollbinding);
35 | }
36 | }
37 |
38 | componentDidMount() {
39 | if (utils.getEnv() == 'WEB') {
40 | this.scrollbinding = throttle(300, this.handleScroll);
41 | window.addEventListener('scroll', this.scrollbinding);
42 | }
43 | }
44 | // web
45 | handleScroll = () => {
46 | const scrollTop = this.componentScrollBox.scrollTop;
47 | const show = scrollTop >= 0.5 * document.body.clientHeight;
48 | this.setState({
49 | show: show,
50 | });
51 | };
52 | goTop = () => {
53 | if (utils.getEnv() == 'WEB') {
54 | this.componentScrollBox.scrollTop = 0;
55 | }
56 | };
57 | render() {
58 | const { show } = this.state;
59 | return (
60 |
61 | {show ? (
62 |
65 |
66 |
67 | ) : (
68 | ''
69 | )}
70 |
71 | );
72 | }
73 | }
74 |
75 | export default BackTop as ComponentClass;
76 |
--------------------------------------------------------------------------------
/src/components/user-info/index.tsx:
--------------------------------------------------------------------------------
1 | // import { ComponentClass } from 'react'
2 | import React, { Component } from 'react';
3 | import { View, Image, Text } from '@ui';
4 | import Link from '@components/link';
5 | import { connect } from 'react-redux';
6 | import * as actions from '@actions/auth';
7 | import { IAuth } from '@interfaces/auth';
8 |
9 | type PageStateProps = {
10 | userInfo: IAuth;
11 | };
12 |
13 | type PageDispatchProps = {
14 | authCheckState: () => void;
15 | };
16 |
17 | type PageOwnProps = {};
18 |
19 | type PageState = {};
20 |
21 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps;
22 |
23 | class UserInfo extends Component {
24 | componentWillMount() {
25 | this.props.authCheckState();
26 | }
27 | render() {
28 | const userInfo = this.props.userInfo;
29 | return (
30 |
31 | {!userInfo.loginname ? (
32 |
33 |
34 | 登录
35 |
36 |
37 | ) : (
38 |
41 |
42 | {userInfo.avatar_url ? (
43 |
44 | ) : (
45 | ''
46 | )}
47 |
48 |
49 | {userInfo.loginname ? {userInfo.loginname} : ''}
50 |
51 |
52 | )}
53 |
54 | );
55 | }
56 | }
57 |
58 | export default connect(
59 | ({ auth }) => ({
60 | userInfo: auth,
61 | }),
62 | (dispatch: Function) => ({
63 | authCheckState() {
64 | dispatch(actions.authCheckState());
65 | },
66 | }),
67 | )(UserInfo);
68 |
--------------------------------------------------------------------------------
/src/components/topic/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View, Text, Image } from '@ui';
3 | import Link from '@components/link';
4 |
5 | // import api from '../lib/utils/api'
6 | import * as utils from '@libs/utils';
7 | import { ITopic } from '@interfaces/topic';
8 |
9 | import styles from './index.module.scss';
10 |
11 | interface IProps {
12 | topic: ITopic;
13 | key: string;
14 | }
15 |
16 | class Topic extends Component {
17 | getTabInfo(tab, good, top, isClass) {
18 | return utils.getTabInfo(tab, good, top, isClass);
19 | }
20 | render() {
21 | const {
22 | title,
23 | tab,
24 | good,
25 | top,
26 | author,
27 | visit_count,
28 | reply_count,
29 | create_at,
30 | last_reply_at,
31 | id,
32 | } = this.props.topic;
33 |
34 | const classnames = 'stitle ' + this.getTabInfo(tab, good, top, true);
35 | const tit = this.getTabInfo(tab, good, top, false);
36 | return (
37 |
38 |
39 | {title}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {author.loginname}
48 | {reply_count > 0 ? (
49 |
50 | {reply_count}/ {visit_count}
51 |
52 | ) : (
53 | ''
54 | )}
55 |
56 |
57 |
58 | {utils.getLastTimeStr(create_at, true)}
59 |
60 |
61 | {utils.getLastTimeStr(last_reply_at, true)}
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 | }
70 |
71 | export { Topic };
72 |
--------------------------------------------------------------------------------
/src/assets/scss/header.scss:
--------------------------------------------------------------------------------
1 | #hd {
2 | border-bottom: 1px solid #e8e8e8;
3 | &.fix-header {
4 | width: 100%;
5 | background-color: rgba(255, 255, 255, 0.95);
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | transition: all 0.3s ease;
10 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
11 | z-index: 6;
12 | }
13 | &.no-fix {
14 | width: 100%;
15 | background-color: #fff;
16 | overflow: hidden;
17 | }
18 | &.show {
19 | transform: translateX(460px);
20 | }
21 | }
22 | .nv-toolbar {
23 | width: 100%;
24 | height: 100px;
25 | display: flex;
26 | align-items: center;
27 |
28 | .toolbar-nav {
29 | width: 82px;
30 | height: 100px;
31 | position: absolute;
32 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAgBAMAAACMSheAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAD1BMVEUAAAAxMTExMTExMTEAAADLnhNSAAAAA3RSTlMAvdLDwKfTAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABtJREFUKM9jEDZGB0YM2MRGAQPDaFgRD4gMKwBpixXx/+q05AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMC0xMFQxMDowNzoxOSswODowMDBkz6cAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMTAtMTBUMTA6MDc6MTkrMDg6MDBBOXcbAAAAAElFTkSuQmCC')
33 | no-repeat center center;
34 | background-size: 38px 32px;
35 | margin: 0;
36 | z-index: 1;
37 | top: 0;
38 | left: 0;
39 | }
40 |
41 | & > span {
42 | display: block;
43 | text-align: center;
44 | height: 100%;
45 | line-height: 100px;
46 | font-size: 38px;
47 | width: 100%;
48 | position: relative;
49 | z-index: 0;
50 | }
51 | .num {
52 | background-color: #80bd01;
53 | color: #fff;
54 | width: 20px;
55 | height: 20px;
56 | line-height: 20px;
57 | vertical-align: middle;
58 | text-align: center;
59 | border-radius: 50%;
60 | position: absolute;
61 | right: 10px;
62 | top: 10px;
63 | z-index: 10;
64 | }
65 | .add-icon {
66 | color: #42b983;
67 | position: absolute;
68 | right: 15px;
69 | top: 15px;
70 | z-index: 10;
71 | font-size: 38px;
72 | padding: 5px 15px;
73 | border-radius: 5px;
74 | }
75 | }
76 | .scroll-hide {
77 | height: 100%;
78 | overflow: hidden;
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/menu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View } from '@ui';
3 | import UserInfo from '@components/user-info';
4 | import Drawer from '@components/drawer';
5 | import Link from '@components/link';
6 |
7 | import styles from './index.module.scss';
8 |
9 | interface IProps {
10 | showMenu: boolean;
11 | pageType: string;
12 | nickName: string;
13 | profileUrl: string;
14 | }
15 |
16 | class NvMenu extends Component {
17 | render() {
18 | const { showMenu } = this.props;
19 | return (
20 |
64 | );
65 | }
66 | }
67 |
68 | export default NvMenu;
69 |
--------------------------------------------------------------------------------
/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View } from '@ui';
3 | import Header from '@components/header/index';
4 | import { withUser } from '@hoc/router';
5 | import * as utils from '@libs/utils';
6 | import Layout from '@components/layout';
7 |
8 | import styles from './index.module.scss';
9 |
10 | type PageStateProps = {};
11 |
12 | type PageDispatchProps = {
13 | authLogin: (token) => Promise;
14 | };
15 |
16 | type PageOwnProps = {};
17 |
18 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps;
19 |
20 | interface PageState {
21 | token: any;
22 | err: any;
23 | }
24 |
25 | class Login extends Component {
26 | state = {
27 | token: '',
28 | err: {
29 | isHiddenIcon: true,
30 | iconSize: 36,
31 | iconType: 'error',
32 | iconColor: '#f00',
33 | text: '',
34 | },
35 | };
36 |
37 | showMessage(message) {
38 | utils.showToast({ title: message });
39 | }
40 | logon = () => {
41 | if (this.state.token === '') {
42 | this.showMessage('令牌格式错误,应为36位UUID字符串');
43 | return false;
44 | }
45 | this.props.authLogin(this.state.token).then(() => {
46 | utils.navigateTo({ url: '/' });
47 | });
48 | };
49 | handleChange(e) {
50 | this.setState({ token: e.target.value });
51 | }
52 | render() {
53 | const { token } = this.state;
54 | console.log('token', token);
55 |
56 | return (
57 |
58 |
59 |
60 |
61 |
69 |
70 |
71 |
72 | 登录
73 |
74 |
75 |
76 |
77 | );
78 | }
79 | }
80 |
81 | export default withUser(Login, true);
82 |
--------------------------------------------------------------------------------
/src/assets/scss/index.scss:
--------------------------------------------------------------------------------
1 | #brim-mask {
2 | pointer-events: none;
3 | }
4 |
5 | #page {
6 | padding-top: 100px;
7 | background-color: #fff;
8 | transition: all .3s ease;
9 | // overflow-x:hidden;
10 | }
11 |
12 | .show-menu {
13 | transform: translateX($gap*40);
14 | }
15 |
16 | .page-cover {
17 | position: fixed;
18 | top: 0;
19 | right: 0;
20 | bottom: 0;
21 | left: 0;
22 | background: rgba(0, 0, 0, .4);
23 | z-index: 7;
24 | }
25 |
26 |
27 | /*模块入口样式*/
28 |
29 | .posts-list {
30 | background-color: $white;
31 |
32 | .topic {
33 | padding: 20px $padding;
34 | border-bottom: $border;
35 | }
36 |
37 | .stitle {
38 | @extend .title;
39 |
40 | &:before {
41 | content: attr(title);
42 | margin-right: 10PX;
43 | margin-top: -3px;
44 | @extend .tag;
45 | color: $white;
46 | }
47 |
48 | &.top:before {
49 | background: #E74C3C;
50 | }
51 |
52 | &.ask:before {
53 | background: #3498DB;
54 | }
55 |
56 | &.good:before {
57 | background: #E67E22;
58 | }
59 |
60 | &.job:before {
61 | background: #9B59B6;
62 | }
63 |
64 | &.share:before {
65 | background: #1ABC9C;
66 | }
67 | }
68 |
69 | .content {
70 | padding-top: 10Px;
71 | display: flex;
72 | }
73 |
74 | .avatar {
75 | @extend .user-avatar;
76 | }
77 |
78 | .info {
79 | display: block;
80 | width: 100%;
81 | flex: 1;
82 |
83 | div {
84 | padding: 3px 0;
85 | display: flex;
86 | color: $text;
87 | font-size: $font-info;
88 |
89 | &:first-child {
90 | font-size: $font-content;
91 | }
92 |
93 | .name,
94 | .time:first-child {
95 | display: block;
96 | width: 100%;
97 | flex: 1;
98 | }
99 |
100 | b {
101 | color: #42b983;
102 | }
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/src/assets/scss/common/reset.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Global Reset of all HTML Elements
3 | *
4 | * Resetting all of our HTML Elements ensures a smoother
5 | * visual transition between browsers. If you don't believe me,
6 | * try temporarily commenting out this block of code, then go
7 | * and look at Mozilla versus Safari, both good browsers with
8 | * a good implementation of CSS. The thing is, all browser CSS
9 | * defaults are different and at the end of the day if visual
10 | * consistency is what we're shooting for, then we need to
11 | * make sure we're resetting all spacing elements.
12 | *
13 | */
14 | html, body {
15 | height: 100%;
16 | border: 0;
17 | font-family: "Helvetica-Neue", "Helvetica", Arial, sans-serif;
18 | line-height: 1.5;
19 | margin: 0;
20 | padding: 0;
21 | box-sizing: border-box;
22 | }
23 |
24 | div, span, object, iframe, img, table, caption, thead, tbody,
25 | tfoot, tr, tr, td, article, aside, canvas, details, figure, hgroup, menu,
26 | nav, footer, header, section, summary, mark, audio, video {
27 | border: 0;
28 | margin: 0;
29 | padding: 0;
30 | box-sizing: border-box;
31 | }
32 |
33 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, address, cit, code,
34 | del, dfn, em, ins, q, samp, small, strong, sub, sup, b, i, hr, dl, dt, dd,
35 | ol, ul, li, fieldset, legend, label {
36 | border: 0;
37 | font-size: 100%;
38 | vertical-align: baseline;
39 | margin: 0;
40 | padding: 0;
41 | box-sizing: border-box;
42 | }
43 |
44 | article, aside, canvas, figure, figure img, figcaption, hgroup,
45 | footer, header, nav, section, audio, video {
46 | display: block;
47 | box-sizing: border-box;
48 | }
49 |
50 | table {
51 | border-collapse: separate;
52 | border-spacing: 0;
53 | caption, th, td {
54 | text-align: left;
55 | vertical-align: middle;
56 | }
57 | }
58 | li{
59 | list-style: none;
60 | }
61 | a{
62 | text-decoration: none;
63 | }
64 | a img {
65 | border: 0;
66 | }
67 |
68 | :focus {
69 | outline: 0;
70 | }
71 |
72 |
73 | textarea{
74 | resize: none;
75 | appearance: none;
76 | box-sizing: border-box;
77 | }
78 | input{
79 | appearance: none;
80 | box-sizing: border-box;
81 | }
82 | select {
83 | appearance: none;
84 | background-color: #fff;
85 | box-sizing: border-box;
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/drawer/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View } from '@ui';
3 |
4 | export default class Drawer extends Component {
5 | constructor() {
6 | super(...arguments);
7 | this.state = { animShow: false };
8 | if (this.props.show) this.animShow();
9 | }
10 | // onItemClick (index, e) {
11 | // this.props.onItemClick && this.props.onItemClick(index)
12 | // this.animHide(e, index)
13 | // }
14 |
15 | onHide() {
16 | this.setState({ show: false });
17 | this.props.onClose && this.props.onClose();
18 | }
19 |
20 | animHide() {
21 | this.setState({
22 | animShow: false,
23 | });
24 | this.props.onStartHide && this.props.onStartHide();
25 | setTimeout(() => {
26 | this.onHide(...arguments);
27 | }, 300);
28 | }
29 |
30 | animShow() {
31 | this.setState({ show: true });
32 | setTimeout(() => {
33 | this.setState({
34 | animShow: true,
35 | });
36 | }, 200);
37 | }
38 |
39 | onMaskClick() {
40 | this.animHide(...arguments);
41 | }
42 |
43 | componentWillReceiveProps(props) {
44 | const { show } = props;
45 | if (show !== this.props.show) {
46 | if (show) this.animShow();
47 | else this.animHide(...arguments);
48 | }
49 | }
50 |
51 | render() {
52 | const {
53 | mask,
54 | width,
55 | right,
56 | // items,
57 | } = this.props;
58 | const { animShow, show } = this.state;
59 | let rootClassName = ['at-drawer'];
60 |
61 | const maskStyle = {
62 | display: mask ? 'block' : 'none',
63 | opacity: animShow ? 1 : 0,
64 | };
65 | const listStyle = {
66 | // width,
67 | // transition: animShow ? 'all 225ms cubic-bezier(0, 0, 0.2, 1)' : 'all 195ms cubic-bezier(0.4, 0, 0.6, 1)',
68 | };
69 | if (right) rootClassName.push('at-drawer--right');
70 | else rootClassName.push('at-drawer--left');
71 |
72 | if (animShow) rootClassName.push('at-drawer--show');
73 | rootClassName = rootClassName.filter((str) => str !== '');
74 |
75 | return show ? (
76 |
77 |
81 |
82 | {this.props.children}
83 |
84 |
85 | ) : (
86 |
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/hoc/router.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as actions from '../actions/auth';
4 | import { IAuth } from '../interfaces/auth';
5 | import * as utils from '@libs/utils';
6 | import redirect from './redirect';
7 |
8 | type PageStateProps = {
9 | userInfo: IAuth;
10 | };
11 |
12 | type PageDispatchProps = {
13 | authLogin: (token) => void;
14 | authCheckState: () => void;
15 | };
16 |
17 | type PageOwnProps = {};
18 |
19 | type PageState = {};
20 |
21 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps;
22 |
23 | function withUser(WrappedComponent, allowNologin = false) {
24 | class WithUserHOC extends Component {
25 | static async getInitialProps(context) {
26 | const { reduxStore, query, req } = context;
27 | const log = reduxStore.dispatch(actions.authCheckState());
28 | if (!(allowNologin || log)) {
29 | // Already signed in? No need to continue.
30 | // Throw them back to the main page
31 | redirect(context, '/login');
32 | return {};
33 | }
34 | let appProps = {};
35 | if (typeof WrappedComponent.getInitialProps === 'function') {
36 | appProps = await WrappedComponent.getInitialProps.call(
37 | WrappedComponent,
38 | context,
39 | );
40 | }
41 | return {
42 | ...appProps,
43 | query,
44 | isServer: !!req,
45 | };
46 | }
47 |
48 | isSuperRender() {
49 | const props = this.props;
50 | return allowNologin || (props.userInfo && props.userInfo.userId);
51 | }
52 | // refer https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Redirect.js
53 | perform() {
54 | if (!this.isSuperRender()) {
55 | utils.redirectTo({ url: '/login' });
56 | }
57 | }
58 | componentDidMount() {
59 | this.perform();
60 | }
61 | render() {
62 | return ;
63 | // if (this.isSuperRender()) {
64 | // return ;
65 | // } else {
66 | // return null;
67 | // }
68 | }
69 | }
70 | return connect(
71 | ({ auth }) => ({ userInfo: auth }),
72 | (dispatch: Function) => ({
73 | authLogin: (token) => dispatch(actions.auth(token)),
74 | authCheckState: () => dispatch(actions.authCheckState()),
75 | }),
76 | )(WithUserHOC);
77 | }
78 |
79 | export { Component, withUser };
80 |
--------------------------------------------------------------------------------
/src/components/header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import classNames from 'classnames';
3 | import { View, Text } from '@ui';
4 | import NvMenu from '../menu';
5 | import Link from '../link';
6 |
7 | type IProps = {
8 | pageType: string;
9 | fixHead: boolean;
10 | messageCount?: number;
11 | scrollTop?: number;
12 | needAdd?: boolean;
13 | showMenu?: boolean;
14 | };
15 |
16 | interface IState {
17 | nickname: string;
18 | profileimgurl: string;
19 | show: boolean;
20 | }
21 |
22 | class Header extends Component {
23 | static defaultProps = {
24 | messageCount: 0,
25 | scrollTop: 0,
26 | needAdd: false,
27 | showMenu: true,
28 | };
29 |
30 | state = {
31 | nickname: '',
32 | profileimgurl: '',
33 | show: false,
34 | };
35 |
36 | openMenu = () => {
37 | this.setState({
38 | show: !this.state.show,
39 | });
40 | };
41 | showMenus = () => {
42 | this.setState({
43 | show: !this.state.show,
44 | });
45 | };
46 |
47 | render() {
48 | const { show, nickname, profileimgurl } = this.state;
49 | const { needAdd, pageType, fixHead, messageCount } = this.props;
50 | const classnames = classNames({
51 | show: show && fixHead,
52 | 'fix-header': fixHead,
53 | 'no-fix': !fixHead,
54 | });
55 | return (
56 |
57 | {show && fixHead ? (
58 |
59 |
60 |
61 | ) : (
62 | ''
63 | )}
64 |
65 |
66 | {fixHead ? (
67 |
68 | ) : (
69 | ''
70 | )}
71 | {pageType}
72 | {messageCount > 0 ? (
73 | {messageCount}
74 | ) : (
75 | ''
76 | )}
77 | {(needAdd && !messageCount) || messageCount <= 0 ? (
78 |
79 |
80 |
81 | ) : (
82 | ''
83 | )}
84 |
85 |
86 |
92 | {/* {fixHead ? "" : ""} */}
93 |
94 | );
95 | }
96 | }
97 |
98 | export default Header;
99 |
--------------------------------------------------------------------------------
/src/actions/auth.ts:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '@constants/auth';
2 | import { post } from '@utils/request';
3 | import store from '@utils/store';
4 |
5 | export const authStart = () => {
6 | return {
7 | type: actionTypes.AUTH_START,
8 | };
9 | };
10 |
11 | export const authSuccess = (user) => {
12 | return { type: actionTypes.AUTH_SUCCESS, ...user };
13 | };
14 |
15 | export const authFail = (error) => {
16 | return {
17 | type: actionTypes.AUTH_FAIL,
18 | error: error,
19 | };
20 | };
21 |
22 | export const logout = () => {
23 | store.removeItem('user');
24 | // store.removeItem('expirationDate');
25 | // store.removeItem('userId');
26 | return {
27 | type: actionTypes.AUTH_LOGOUT,
28 | };
29 | };
30 |
31 | export const checkAuthTimeout = (expirationTime) => {
32 | return (dispatch) => {
33 | setTimeout(() => {
34 | dispatch(logout());
35 | }, expirationTime * 1000);
36 | };
37 | };
38 |
39 | export const auth = (accesstoken) => {
40 | return async (dispatch) => {
41 | dispatch(authStart());
42 | const userInfo = { accesstoken: accesstoken };
43 | try {
44 | const response = await post({
45 | url: 'https://cnodejs.org/api/v1/accesstoken',
46 | data: userInfo,
47 | });
48 |
49 | if (response.data && response.data.success) {
50 | let res = response.data;
51 | let user = {
52 | loginname: res.loginname,
53 | avatar_url: res.avatar_url,
54 | userId: res.id,
55 | token: accesstoken,
56 | };
57 | // const expirationDate = new Date(new Date().getTime() + response.data.expiresIn * 1000);
58 | store.setItem('user', JSON.stringify(user));
59 | dispatch(authSuccess(user));
60 | // store.setItem("expirationDate", expirationDate);
61 | // store.setItem("userId", response.data.localId);
62 | // dispatch(checkAuthTimeout(response.data.expiresIn));
63 | }
64 | } catch (err) {
65 | dispatch(authFail(err.response.data.error));
66 | }
67 | };
68 | };
69 |
70 | export const setAuthRedirectPath = (path) => {
71 | return {
72 | type: actionTypes.SET_AUTH_REDIRECT_PATH,
73 | path: path,
74 | };
75 | };
76 |
77 | export const authCheckState = () => {
78 | return (dispatch) => {
79 | const token = store.getItem('user');
80 | if (!token) {
81 | // dispatch(logout());
82 | } else {
83 | // const expirationDate = new Date(store.getItem("expirationDate"));
84 | // && expirationDate <= new Date()
85 | if (false) {
86 | dispatch(logout());
87 | } else {
88 | // const userId = store.getItem("userId");
89 | dispatch(authSuccess(JSON.parse(token)));
90 | // dispatch(checkAuthTimeout((expirationDate.getTime() - new Date().getTime()) / 1000));
91 | }
92 | }
93 | };
94 | };
95 |
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 | import Next from 'next';
3 | import Express from 'express';
4 | import Cache from './cache';
5 |
6 | const dev = process.env.NODE_ENV !== 'production';
7 | const port = parseInt(process.env.PORT, 10) || 8000;
8 |
9 | const app = Next({ dev });
10 | const handle = app.getRequestHandler();
11 |
12 | function getCacheKey(req) {
13 | return `${req.url}`;
14 | }
15 | async function cacheRender(req, res) {
16 | const key = getCacheKey(req);
17 | // reder /list to /
18 | // const reqPath = req.path === '/list' ? '/' : req.path;
19 | const reqPath = req.path;
20 | if (Cache.has(key)) {
21 | res.setHeader('X-Cache', 'HIT');
22 | res.send(Cache.get(key));
23 | return;
24 | }
25 |
26 | try {
27 | const html = await app.renderToHTML(req, res, reqPath, req.query);
28 | if (res.statusCode !== 200) {
29 | res.send(html);
30 | return;
31 | } else {
32 | Cache.set(key, html);
33 | res.setHeader('X-Cache', 'MISS');
34 | res.send(html);
35 | }
36 | } catch (err) {
37 | res.statusCode = 500;
38 | app.renderError(err, req, res, reqPath, req.query);
39 | }
40 | }
41 |
42 | // not report route for custom monitor
43 | const noReportRoutes = ['/_next', '/static'];
44 |
45 | async function startServer() {
46 | await app.prepare();
47 |
48 | const server = Express();
49 | server.use(Express.static(join(__dirname, '../public/static')));
50 |
51 | server.get('/', async (req, res) => {
52 | return cacheRender(req, res);
53 | });
54 |
55 | server.get('/user', async (req, res) => {
56 | return cacheRender(req, res);
57 | });
58 |
59 | server.get('/about', async (req, res) => {
60 | return cacheRender(req, res);
61 | });
62 |
63 | server.get('/login', async (req, res) => {
64 | return handle(req, res);
65 | });
66 |
67 | server.get('/add', async (req, res) => {
68 | return handle(req, res);
69 | });
70 | server.get('/message', async (req, res) => {
71 | return handle(req, res);
72 | });
73 |
74 | server.get('*', (req, res) => {
75 | noReportRoutes.forEach((route) => {
76 | if (req.path.indexOf(route) === 0) {
77 | req['__SLS_NO_REPORT__'] = true;
78 | }
79 | });
80 | return handle(req, res);
81 | });
82 |
83 | // define binary type for response
84 | // if includes, will return base64 encoded, very useful for images
85 | server['binaryTypes'] = ['*/*'];
86 |
87 | return server;
88 | }
89 |
90 | if (process.env.SERVERLESS) {
91 | module.exports = startServer;
92 | } else {
93 | try {
94 | startServer().then((server) => {
95 | server.listen(port, () => {
96 | console.log(`> Ready on http://localhost:${port}`);
97 | });
98 | });
99 | } catch (e) {
100 | throw e;
101 | }
102 | }
103 |
104 | process.on('unhandledRejection', (e) => {
105 | throw e;
106 | });
107 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "serverless-cnode",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "dev": "nodemon",
6 | "build": "rimraf .next && cross-env NODE_ENV=production next build && tsc --project tsconfig.server.json",
7 | "build:sls": "rimraf .next && cross-env NODE_ENV=production SERVERLESS=true next build && tsc --project tsconfig.server.json",
8 | "start": "cross-env NODE_ENV=production node ./dist/index.js",
9 | "deploy": "serverless deploy",
10 | "deploy:layer": "serverless deploy --target=./layer"
11 | },
12 | "keywords": [
13 | "next.js",
14 | "cnode"
15 | ],
16 | "license": "MIT",
17 | "dependencies": {
18 | "axios": "^0.19.2",
19 | "classnames": "^2.2.6",
20 | "express": "^4.17.1",
21 | "immutability-helper": "^3.1.1",
22 | "koa": "^2.12.1",
23 | "koa-router": "^9.0.1",
24 | "lodash": "^4.17.19",
25 | "lru-cache": "^5.1.1",
26 | "markdown": "^0.5.0",
27 | "next": "^9.4.4",
28 | "prop-types": "^15.7.2",
29 | "react": "^16.13.1",
30 | "react-dom": "^16.13.1",
31 | "react-redux": "^7.2.0",
32 | "redux": "^4.0.5",
33 | "redux-logger": "^3.0.6",
34 | "redux-thunk": "^2.3.0",
35 | "throttle-debounce": "^2.2.1",
36 | "timeago.js": "^4.0.2"
37 | },
38 | "devDependencies": {
39 | "@babel/core": "^7.10.2",
40 | "@babel/generator": "^7.10.2",
41 | "@babel/plugin-proposal-class-properties": "^7.10.1",
42 | "@babel/plugin-proposal-decorators": "^7.10.1",
43 | "@babel/plugin-proposal-object-rest-spread": "^7.10.1",
44 | "@types/koa": "^2.11.3",
45 | "@types/koa-router": "^7.4.1",
46 | "@types/lru-cache": "^5.1.0",
47 | "@types/next": "^9.0.0",
48 | "@types/react": "^16.9.38",
49 | "@zeit/next-css": "^1.0.1",
50 | "@zeit/next-sass": "^1.0.1",
51 | "@zeit/next-typescript": "^1.1.1",
52 | "autoprefixer": "^9.8.0",
53 | "babel-loader": "^8.1.0",
54 | "babel-plugin-module-resolver": "^4.0.0",
55 | "babel-plugin-transform-assets": "^1.0.2",
56 | "cross-env": "^7.0.2",
57 | "css-loader": "^3.6.0",
58 | "dotenv-webpack": "^1.8.0",
59 | "file-loader": "^6.0.0",
60 | "fork-ts-checker-webpack-plugin": "^5.0.1",
61 | "next-compose-plugins": "^2.2.0",
62 | "next-images": "^1.4.0",
63 | "next-optimized-images": "^2.6.1",
64 | "node-sass": "^4.14.1",
65 | "nodemon": "^2.0.4",
66 | "postcss-loader": "^3.0.0",
67 | "postcss-pxtransform": "^2.2.9",
68 | "rimraf": "^3.0.2",
69 | "sass-loader": "^8.0.2",
70 | "ts-node": "^8.10.2",
71 | "tslint": "^6.1.2",
72 | "typescript": "^3.9.5",
73 | "url-loader": "^4.1.0"
74 | },
75 | "description": "> A cnodejs client using next.js",
76 | "repository": {
77 | "type": "git",
78 | "url": "git+https://github.com/serverless-plus/serverless-cnode.git"
79 | },
80 | "author": "yugasun",
81 | "bugs": {
82 | "url": "https://github.com/serverless-plus/serverless-cnode/issues"
83 | },
84 | "homepage": "https://github.com/serverless-plus/serverless-cnode#readme"
85 | }
86 |
--------------------------------------------------------------------------------
/src/assets/scss/detail.scss:
--------------------------------------------------------------------------------
1 | .page {
2 | background-color: $colorfff;
3 | height: 100%;
4 | padding: $gap*3 0;
5 |
6 | .reply_num {
7 | margin-top: $gap*4;
8 | background-color: $colore7;
9 | padding: $gap*2 0 $gap*2 $gap*2;
10 | border-top: solid 1px $colord4;
11 | border-bottom: solid 1px $colord4;
12 | }
13 | .reply-list {
14 | width: 100%;
15 | margin-top: $gap*3;
16 | .ul {
17 | width: 100%;
18 | list-style: none;
19 | padding-left: 0;
20 | .li {
21 | width: 100%;
22 | list-style: none;
23 | border-bottom: solid 1px $colord4;
24 | &:last-child {
25 | border-bottom: none;
26 | }
27 | .uped {
28 | color: $color80;
29 | }
30 | .icon {
31 | font-size: 26PX;
32 | }
33 | }
34 | }
35 | }
36 | .topic_content {
37 | margin: 0 $gap*2;
38 | }
39 | .reply {
40 | width: 100%;
41 | margin-top: $gap*3;
42 | border-top: solid 1px $colord4;
43 | .text {
44 | margin: $gap*3 2%;
45 | width: 96%;
46 | border-color: $colord4;
47 | color: #000;
48 | }
49 | .btn {
50 | display: inline-block;
51 | border-radius: $gap;
52 | text-align: center;
53 | font-size: 16px;
54 | margin: 0 2% $gap*3;
55 | width: 96%;
56 | background-color: $color80;
57 | padding: $gap*2 0;
58 | color: $colorfff;
59 | }
60 | .err {
61 | border-color: red;
62 | }
63 | }
64 | .message {
65 | border-bottom: solid 1px $colord4;
66 | padding: $gap*2 0;
67 | }
68 | .tabs {
69 | width: 100%;
70 | // height: $gap* 8.2;
71 | // margin-top: $gap* 9;
72 | list-style: none;
73 | display: flex;
74 | // position: relative;
75 | // margin-bottom: -1px;
76 | .item {
77 | width: 50%;
78 | padding: $gap*2.3 0;
79 | flex: 0 1 auto;
80 | font-size: 16PX;
81 | text-align: center;
82 | font-weight: bold;
83 | border-bottom: solid 1px $colord4;
84 |
85 | }
86 | .read {
87 | font-size:$gap*5;
88 | color: $color80;
89 | position:absolute;
90 | right:$gap*1;
91 | top:$gap*1.5;
92 | font-weight:bold;
93 | }
94 | .br {
95 | border-right: solid 1px $colord4;
96 | }
97 | .selected {
98 | color: $red;
99 | border-bottom: solid 2px $red;
100 | }
101 | }
102 | .icon-empty {
103 | font-size: $gap*25;
104 | display: block;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/assets/scss/topic.scss:
--------------------------------------------------------------------------------
1 | .topic-title {
2 | @extend .title-nooverflow;
3 | padding: $gap;
4 | margin:$padding;
5 | color: $title;
6 | background-color:$colorf0;
7 | border-radius:$gap;
8 | font-weight: 700;
9 | }
10 |
11 | .author-info {
12 | display: flex;
13 | align-items: center;
14 | padding: 0 $padding;
15 | color: $text;
16 | font-size: 12px;
17 | .col {
18 | display: block;
19 | width: 100%;
20 | flex: 1;
21 | }
22 | .avatar {
23 | display: block;
24 | width: 40Px;
25 | height: 40Px;
26 | margin-right: $padding;
27 | border-radius: 50%;
28 | }
29 | .right{
30 | text-align: right;
31 | }
32 | span, time {
33 | display: block;
34 | padding: 5px 0;
35 | }
36 | .tag {
37 | @extend .tag;
38 | color: #ffffff;
39 | &.top {
40 | background: #E74C3C;
41 | }
42 | &.ask {
43 | background: #3498DB;
44 | }
45 | &.good {
46 | background: #E67E22;
47 | }
48 | &.job {
49 | background: #9B59B6;
50 | }
51 | &.share {
52 | background: #1ABC9C;
53 | }
54 | }
55 | }
56 | .topic-content {
57 | padding: $padding;
58 | margin-top: $padding;
59 | background: #ffffff;
60 | border-bottom: solid 1px $colord4;
61 | .from{
62 | color:$red;
63 | }
64 | }
65 | .topic-reply {
66 | @extend .title;
67 | padding: $padding;
68 | border-bottom: solid 1px $colord4;
69 | strong {
70 | color: #42b983;
71 | }
72 | }
73 | .reply_num {
74 | margin-top: $gap*4;
75 | background-color: $colore7;
76 | padding: $gap*2 0 $gap*2 $gap*2;
77 | border-top: solid 1px $colord4;
78 | border-bottom: solid 1px $colord4;
79 | }
80 | .reply-list {
81 | width: 100%;
82 | margin-top: $gap*3;
83 | .ul {
84 | width: 100%;
85 | list-style: none;
86 | padding-left: 0;
87 | .li {
88 | width: 100%;
89 | list-style: none;
90 | border-bottom: solid 1px $colord4;
91 | &:last-child {
92 | border-bottom: none;
93 | }
94 | .uped {
95 | color: $color80;
96 | }
97 | .icon {
98 | font-size: 26PX;
99 | }
100 | .from{
101 | color:$red;
102 | }
103 | .language-javascript{
104 | background-color:$colorf0;
105 | overflow-x:auto;
106 | }
107 | }
108 | }
109 | }
110 |
111 | /* 回复框样式 */
112 |
113 | .reply {
114 | margin: 0 $padding;
115 | textarea {
116 | display: block;
117 | width: 100%;
118 | flex: 1;
119 | border: $border;
120 | background-color: #fff;
121 | font-size: 14px;
122 | padding: $padding;
123 | color: #313131;
124 | }
125 | .button {
126 | display: inline-block;
127 | width: 100%;
128 | height: 80px;
129 | margin: $padding 0;
130 | line-height: 80px;
131 | color: #fff;
132 | font-size: 32px;
133 | background-color: #4fc08d;
134 | border: none;
135 | border-bottom: 4px solid #3aa373;
136 | text-align: center;
137 | vertical-align: middle;
138 | }
139 | }
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/src/assets/scss/user.scss:
--------------------------------------------------------------------------------
1 | /*侧边栏用户信息区域*/
2 | .user-info {
3 | padding: 15px;
4 | font-size: 24px;
5 | color: #313131;
6 | }
7 |
8 | /*未登录*/
9 | .login-no {
10 | overflow: hidden;
11 | margin: 8px 9px;
12 | & > .login {
13 | float: right;
14 | height: 24px;
15 | line-height: 24px;
16 | padding-left: 34px;
17 | position: relative;
18 | &:before {
19 | width: 24px;
20 | height: 24px;
21 | content: '';
22 | position: absolute;
23 | left: 0;
24 | top: 0;
25 | }
26 | }
27 | .login {
28 | float: left;
29 | &:before {
30 | background: url('../images/components/login_icon.png') no-repeat left
31 | center;
32 | background-size: 24px 24px;
33 | }
34 | }
35 | }
36 |
37 | /*已登录*/
38 | .login-yes {
39 | height: 100%;
40 | background: url('../images/components/go_next_icon.png') no-repeat right
41 | center;
42 | background-size: 6px 10px;
43 | overflow: hidden;
44 | position: relative;
45 | .avertar {
46 | width: 40px;
47 | height: 40px;
48 | background: url('../images/components/user.png') no-repeat center center;
49 | background-size: 40px 40px;
50 | border-radius: 20px;
51 | overflow: hidden;
52 | float: left;
53 | & > img {
54 | width: 40px;
55 | height: 40px;
56 | display: block;
57 | }
58 | }
59 | .info {
60 | margin-left: 10px;
61 | overflow: hidden;
62 | float: left;
63 | }
64 | p {
65 | width: 85px;
66 | overflow: hidden;
67 | white-space: nowrap;
68 | text-overflow: ellipsis;
69 | font-size: 12px;
70 | line-height: 12px;
71 | line-height: 40px;
72 | &.lh20 {
73 | line-height: 20px;
74 | }
75 | }
76 | &:after {
77 | display: block;
78 | background: url('../images/components/go_icon.png') no-repeat center right;
79 | background-size: 7px 7px;
80 | }
81 | }
82 |
83 | .userinfo {
84 | margin-top: 100px;
85 | width: 100%;
86 | background-color: $colore7;
87 | text-align: center;
88 | // height: 180px;
89 | .u-img {
90 | width: 100px;
91 | height: 100px;
92 | border-radius: 50%;
93 | margin-top: $gap * 3;
94 | display: inline-block;
95 | background: url('../images/components/user.png') no-repeat center center;
96 | background-size: 100px 100px;
97 | }
98 | .u-name {
99 | color: #000;
100 | }
101 | .u-bottom {
102 | background-color: $colore7;
103 | width: 100%;
104 | margin-top: 20px;
105 | padding-bottom: 20px;
106 | display: flex;
107 | .u-time {
108 | width: 50%;
109 | padding-left: $gap * 2;
110 | justify-items: center;
111 | align-items: center;
112 | justify-content: center;
113 | }
114 |
115 | .u-score {
116 | width: 50%;
117 | text-align: right;
118 | padding-right: $gap * 2;
119 | color: $color80;
120 | justify-items: center;
121 | align-items: center;
122 | justify-content: center;
123 | }
124 | }
125 | }
126 |
127 | .message {
128 | background-color: #fff;
129 | padding: $gap;
130 | border-bottom: solid 1px $colord4;
131 | }
132 |
133 | .user-tabs {
134 | width: 100%;
135 | // height: $gap*8.2;
136 | list-style: none;
137 |
138 | display: flex;
139 | // position: relative;
140 | .item {
141 | width: 50%;
142 | padding: $gap * 2.3 0;
143 | flex: 0 1 auto;
144 | font-size: 32px;
145 | text-align: center;
146 | font-weight: bold;
147 | border-bottom: solid 1px $colord4;
148 | }
149 | .read {
150 | font-size: $gap * 5;
151 | color: $color80;
152 | position: absolute;
153 | right: $gap * 1;
154 | top: $gap * 1.5;
155 | font-weight: bold;
156 | }
157 | .br {
158 | border-right: solid 1px $colord4;
159 | }
160 | .selected {
161 | color: $red;
162 | border-bottom: solid 2px $red;
163 | }
164 | }
165 |
166 | .topics {
167 | .icon-empty {
168 | font-size: $gap * 25;
169 | color: $colord4;
170 | display: block;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/components/reply/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View } from '@ui';
3 | import * as utils from '@libs/utils';
4 | import { withUser } from '@hoc/router';
5 | import classNames from 'classnames';
6 | import update from 'immutability-helper';
7 | import { post } from '@utils/request';
8 |
9 | const markdown = require('markdown').markdown;
10 |
11 | interface updateRepliesFunc {
12 | (func: any): any;
13 | }
14 |
15 | type Iprops = {
16 | userInfo;
17 | topic;
18 | topicId;
19 | replyId;
20 | replyTo?;
21 | show;
22 | updateReplies: updateRepliesFunc;
23 | onClose: () => void;
24 | };
25 |
26 | type PageState = {
27 | hasErr;
28 | content;
29 | author_txt;
30 | };
31 |
32 | // props: ['topic', 'replyId', 'topicId', 'replyTo', 'show'],
33 | class Reply extends Component {
34 | state = {
35 | hasErr: false,
36 | content: '',
37 | author_txt:
38 | '\n\n 来自拉风的 [React-cnode](https://github.com/icai/taro-cnode)',
39 | };
40 |
41 | handleChange = (e) => {
42 | this.setState({
43 | content: e.target.value,
44 | });
45 | };
46 | componentDidMount() {
47 | if (this.props.replyTo) {
48 | this.setState({
49 | content: `@${this.props.replyTo}`,
50 | });
51 | }
52 | }
53 | addReply() {
54 | const { content, author_txt } = this.state;
55 | const { userInfo, topicId, replyId, show, updateReplies } = this.props;
56 | if (!content) {
57 | this.setState({ hasErr: true });
58 | } else {
59 | let time = new Date();
60 | let linkUsers = utils.linkUsers(content);
61 | let htmlText = markdown.toHTML(linkUsers) + author_txt;
62 | let replyContent = utils.getContentHtml(htmlText);
63 | let postData: any = {
64 | accesstoken: userInfo.token,
65 | content: content + author_txt,
66 | };
67 | if (replyId) {
68 | postData.reply_id = replyId;
69 | }
70 |
71 | post({
72 | data: postData,
73 | url: `https://cnodejs.org/api/v1/topic/${topicId}/replies`,
74 | })
75 | .then((resp) => {
76 | let res = resp.data;
77 | if (res.success) {
78 | updateReplies &&
79 | updateReplies((topic, context) => {
80 | const newreplies = update(topic.replies, {
81 | $push: [
82 | {
83 | id: res.reply_id,
84 | author: {
85 | loginname: userInfo.loginname,
86 | avatar_url: userInfo.avatar_url,
87 | },
88 | content: replyContent,
89 | ups: [],
90 | create_at: time,
91 | },
92 | ],
93 | });
94 | topic.replies = newreplies;
95 | context.setState({ topic: topic });
96 | });
97 | this.setState({ content: '' });
98 | if (show) {
99 | this.props.onClose();
100 | }
101 | } else {
102 | utils.showToast({ title: res.error_msg });
103 | }
104 | })
105 | .catch((resp) => {
106 | console.info(resp);
107 | });
108 | }
109 | }
110 |
111 | render() {
112 | const { hasErr } = this.state;
113 | return (
114 |
115 |
125 |
126 | 确定
127 |
128 |
129 | );
130 | }
131 | }
132 |
133 | // #region 导出注意
134 | //
135 | // 经过上面的声明后需要将导出的 React.Component 子类修改为子类本身的 props 属性
136 | // 这样在使用这个子类时 Ts 才不会提示缺少 JSX 类型参数错误
137 | //
138 | // #endregion
139 |
140 | export default withUser(Reply); // as ComponentClass;
141 |
--------------------------------------------------------------------------------
/pages/index/index.tsx:
--------------------------------------------------------------------------------
1 | // import { ComponentClass } from 'react'
2 | import React, { Component } from 'react';
3 |
4 | import { View } from '@ui';
5 | import { TopicsList } from '@components/topics';
6 | import Header from '@components/header';
7 | import { throttle } from 'throttle-debounce';
8 | import { ITopic } from '@interfaces/topic';
9 | // import BackTop from "@components/backtotop";
10 | // import update from "immutability-helper";
11 | import { get } from '@utils/request';
12 | import Loading from '@components/loading2';
13 |
14 | // import Link from "next/link";
15 | import { withRouter } from 'next/router';
16 | import Head from 'next/head';
17 | import Layout from '@components/layout';
18 |
19 | // type IProps = {};
20 | interface IProps {
21 | state: IState;
22 | router: {
23 | query: any;
24 | };
25 | }
26 |
27 | type TsearchKey = {
28 | page: number;
29 | limit: number;
30 | tab: string;
31 | mdrender: boolean;
32 | };
33 |
34 | interface IState {
35 | scroll: boolean;
36 | loading: boolean;
37 | topics: ITopic[];
38 | searchKey: TsearchKey;
39 | }
40 |
41 | class List extends Component {
42 | componentScrollBox;
43 | throttledScrollHandler;
44 |
45 | constructor(props) {
46 | super(props);
47 | console.log(props);
48 | this.state = props.state;
49 | }
50 | static getInitialProps({ query: { tab } }) {
51 | return {
52 | state: {
53 | scroll: true,
54 | topics: [],
55 | index: {},
56 | loading: true,
57 | searchDataStr: '',
58 | searchKey: {
59 | page: 1,
60 | limit: 20,
61 | tab: tab,
62 | mdrender: true,
63 | },
64 | },
65 | };
66 | }
67 |
68 | index = {};
69 |
70 | componentWillUnmount() {
71 | window.removeEventListener('scroll', this.throttledScrollHandler);
72 | }
73 | componentDidMount() {
74 | this.componentScrollBox = document.documentElement;
75 | this.throttledScrollHandler = throttle(300, this.getScrollData);
76 | const { router } = this.props;
77 | if (router.query && this.props.router.query.tab) {
78 | this.setState(
79 | (prevState) => {
80 | searchKey: Object.assign(prevState.searchKey, {
81 | tab: this.props.router.query.tab,
82 | });
83 | },
84 | () => {
85 | this.getTopics();
86 | },
87 | );
88 | } else {
89 | this.getTopics();
90 | }
91 |
92 | window.addEventListener('scroll', this.throttledScrollHandler);
93 | }
94 | getScrollData = () => {
95 | if (this.state.scroll) {
96 | let totalheight =
97 | document.documentElement.clientHeight +
98 | document.documentElement.scrollTop;
99 | if (document.documentElement.scrollHeight <= totalheight + 200) {
100 | this.onReachBottom();
101 | }
102 | }
103 | };
104 | getTitleStr(tab) {
105 | let str = '';
106 | switch (tab) {
107 | case 'share':
108 | str = '分享';
109 | break;
110 | case 'ask':
111 | str = '问答';
112 | break;
113 | case 'job':
114 | str = '招聘';
115 | break;
116 | case 'good':
117 | str = '精华';
118 | break;
119 | default:
120 | str = '全部';
121 | break;
122 | }
123 | return str;
124 | }
125 |
126 | async getTopics() {
127 | let params = this.state.searchKey;
128 | try {
129 | const res = await get({
130 | url: 'https://cnodejs.org/api/v1/topics',
131 | data: params,
132 | });
133 | let data = res.data;
134 | this.setState({
135 | scroll: true,
136 | loading: false,
137 | });
138 | if (data && data.data) {
139 | this.mergeTopics(data.data);
140 | }
141 | } catch (error) {
142 | // utils.showToast({
143 | // title: "载入远程数据错误"
144 | // });
145 | }
146 | }
147 | mergeTopics = (topics) => {
148 | this.setState({ topics: [...this.state.topics, ...topics] });
149 | };
150 | onReachBottom = () => {
151 | if (this.state.scroll) {
152 | this.setState(
153 | (prevState) => ({
154 | scroll: false,
155 | loading: true,
156 | searchKey: {
157 | ...prevState.searchKey,
158 | page: prevState.searchKey.page + 1,
159 | },
160 | }),
161 | () => {
162 | this.getTopics();
163 | },
164 | );
165 | }
166 | };
167 |
168 | render() {
169 | const { searchKey, topics, loading } = this.state || this.props.state;
170 | return (
171 |
172 |
173 | 首页
174 |
175 |
180 |
181 |
182 |
183 |
184 | {loading && searchKey.page > 1 && }
185 |
186 | {/* */}
187 |
188 | );
189 | }
190 | }
191 |
192 | export default withRouter(List);
193 |
--------------------------------------------------------------------------------
/pages/add/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { withUser } from '@hoc/router';
3 | import { View } from '@ui';
4 | import Header from '@components/header';
5 | import * as utils from '@libs/utils';
6 | import { post } from '@utils/request';
7 | import Head from 'next/head';
8 | import Layout from '@components/layout';
9 | import { IAuth } from '@interfaces/auth';
10 |
11 | import styles from './index.module.scss';
12 |
13 | type PageStateProps = {
14 | userInfo: IAuth;
15 | state: PageState;
16 | };
17 |
18 | type PageDispatchProps = {
19 | authCheckState: () => void;
20 | };
21 |
22 | type PageOwnProps = {};
23 | interface PageState {
24 | topic?: any;
25 | err: string;
26 | authorTxt: string;
27 | selectorIndex: number;
28 | selector: any[];
29 | }
30 |
31 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps;
32 |
33 | class Add extends Component {
34 | constructor(props) {
35 | super(props);
36 | this.state = props.state;
37 | }
38 | static getInitialProps() {
39 | return {
40 | state: {
41 | topic: {
42 | tab: 'share',
43 | title: '',
44 | content: '',
45 | },
46 | selectorIndex: 0,
47 | selector: [
48 | {
49 | name: '分享',
50 | value: 'share',
51 | },
52 | {
53 | name: '问答',
54 | value: 'ask',
55 | },
56 | {
57 | name: '招聘',
58 | value: 'job',
59 | },
60 | ],
61 | err: '',
62 | authorTxt:
63 | '\n\nfrom [serverless-cnode](https://github.com/serverless-plus/serverless-cnode)',
64 | },
65 | };
66 | }
67 |
68 | async addTopic() {
69 | let title = utils.trim(this.state.topic.title);
70 | let contents = utils.trim(this.state.topic.content);
71 | if (!title || title.length < 10) {
72 | this.setState({
73 | err: 'title',
74 | });
75 | return false;
76 | }
77 | if (!contents) {
78 | this.setState({
79 | err: 'content',
80 | });
81 | return false;
82 | }
83 | let postData = {
84 | ...this.state.topic,
85 | content: this.state.topic.content + this.state.authorTxt,
86 | accesstoken: this.props.userInfo.token,
87 | };
88 |
89 | try {
90 | const resp = await post({
91 | data: postData,
92 | url: 'https://cnodejs.org/api/v1/topics',
93 | });
94 | let res = resp.data;
95 | if (res.success) {
96 | utils.navigateTo({ url: '/' });
97 | } else {
98 | utils.showToast({ title: res.error_msg });
99 | }
100 | } catch (err) {
101 | console.info(err);
102 | }
103 | }
104 | handleTopicTabChange = (e) => {
105 | this.setState((prevState) => ({
106 | topic: {
107 | ...prevState.topic,
108 | tab: prevState.selector[e.detail.value]['value'],
109 | },
110 | selectorIndex: e.detail.value,
111 | }));
112 | };
113 | handleTopicContentChange = (e) => {
114 | this.setState((prevState) => ({
115 | topic: {
116 | ...prevState.topic,
117 | content: e.target.value,
118 | },
119 | }));
120 | };
121 | handleTopicChange = (e) => {
122 | this.setState((prevState) => ({
123 | topic: {
124 | ...prevState.topic,
125 | title: e,
126 | },
127 | }));
128 | };
129 | render() {
130 | const { err } = this.state || this.props.state;
131 | return (
132 |
133 |
134 | 发表
135 |
136 |
137 |
138 |
139 | 选择分类:
140 |
148 |
151 | 发布
152 |
153 |
154 |
155 |
165 |
166 |
176 |
177 |
178 | );
179 | }
180 | }
181 |
182 | export default withUser(Add);
183 |
--------------------------------------------------------------------------------
/src/assets/scss/common/common.scss:
--------------------------------------------------------------------------------
1 | $color31: #313131;
2 | $colorf0: #f0f0f0;
3 | $colorfff: #fff;
4 | $colore7: #e7e7e7;
5 | $colorfa: #fafafa;
6 | $color62: #626262;
7 | $colord4: #d4d4d4;
8 | $colora0: #a0a0a0;
9 | $red: #ff5a5f;
10 | $yellow: #feb501;
11 | $color80: #80bd01;
12 | $gap: 5px;
13 |
14 | body {
15 | height: 100%;
16 | width: 100%;
17 | font-size: 24px;
18 | color: #313131;
19 | overflow-x: hidden;
20 | line-height: 1;
21 | background-image: url('../../images/loading.gif');
22 | background-repeat: no-repeat;
23 | background-position: center 250px;
24 | }
25 |
26 | html {
27 | -ms-text-size-adjust: 100%;
28 | -webkit-text-size-adjust: 100%;
29 | }
30 |
31 | body {
32 | line-height: 1.6;
33 | font-family: -apple-system-font, 'Helvetica Neue', sans-serif;
34 | }
35 |
36 | * {
37 | margin: 0;
38 | padding: 0;
39 | }
40 |
41 | a img {
42 | border: 0;
43 | }
44 |
45 | a {
46 | text-decoration: none;
47 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
48 | }
49 |
50 | .mt5 {
51 | margin-top: $gap;
52 | }
53 |
54 | .mt10 {
55 | margin-top: $gap * 2;
56 | }
57 |
58 | .color80 {
59 | color: #42b983;
60 | }
61 |
62 | .clear {
63 | zoom: 1;
64 | &:after {
65 | content: '';
66 | display: table;
67 | clear: both;
68 | }
69 | }
70 |
71 | #app {
72 | height: 100%;
73 | }
74 |
75 | .display-flex {
76 | display: -moz-box;
77 | /* Firefox */
78 | display: -ms-flexbox;
79 | /* IE10 */
80 | display: -webkit-box;
81 | /* Safari */
82 | display: -webkit-flex;
83 | /* Chrome, WebKit */
84 | display: flexbox;
85 | display: flex;
86 | }
87 |
88 | .break {
89 | overflow: hidden;
90 | text-overflow: ellipsis;
91 | white-space: nowrap;
92 | }
93 |
94 | .flex-wrp {
95 | background: #fff;
96 | }
97 |
98 | .user {
99 | width: 100%;
100 | margin: $gap * 2 0;
101 | padding: 0 $gap * 2;
102 | display: flex;
103 | align-items: center;
104 |
105 | .tab {
106 | display: inline-block;
107 | width: $gap * 8;
108 | color: $color62;
109 | border-radius: $gap;
110 | background-color: $colore7;
111 | margin-right: $gap * 2;
112 | text-align: center;
113 | height: $gap * 4;
114 | line-height: $gap * 4;
115 | vertical-align: middle;
116 | }
117 | .good {
118 | background-color: $color80;
119 | color: $colorfff;
120 | }
121 | .title {
122 | font-size: $gap * 8;
123 | font-weight: bold;
124 | display: block;
125 | width: 100%;
126 | flex: 1;
127 | @extend .break;
128 | }
129 | .head {
130 | display: inline-block;
131 | width: $gap * 14;
132 | height: $gap * 14;
133 | margin-right: $gap * 2;
134 | border-radius: 10px;
135 | img {
136 | width: $gap * 14;
137 | border: 2px solid #fff6e6;
138 | border-radius: 50%;
139 | }
140 | }
141 | .info {
142 | overflow: hidden;
143 | display: block;
144 | width: 100%;
145 | flex: 1;
146 | &:after {
147 | clear: both;
148 | }
149 | .t-title {
150 | font-size: $gap * 5;
151 | font-weight: bold;
152 | width: 100%;
153 | color: #333;
154 | @extend .break;
155 | }
156 | .mt12 {
157 | margin-top: 12px;
158 | }
159 | .cl {
160 | display: inline-block;
161 | width: 68%;
162 | .name {
163 | color: $color62;
164 | }
165 | .mt10 {
166 | margin-top: $gap * 2;
167 | }
168 | }
169 | .cr {
170 | width: 30%;
171 | display: inline-block;
172 | text-align: right;
173 | .name {
174 | margin-top: $gap * 2;
175 | color: $color80;
176 | font-size: $gap * 4;
177 | }
178 | }
179 | }
180 | }
181 |
182 | .reply_content {
183 | padding: 0 $gap * 3;
184 | margin-bottom: $gap * 3;
185 | img {
186 | width: auto\9;
187 | height: auto;
188 | max-width: 100%;
189 | vertical-align: middle;
190 | border: 0;
191 | -ms-interpolation-mode: bicubic;
192 | }
193 | p {
194 | line-height: 1.3;
195 | }
196 | }
197 |
198 | .no-data {
199 | width: 100%;
200 | height: 100%;
201 | padding: 40% 0;
202 | color: $colord4;
203 | display: inline-block;
204 | font-size: 18px;
205 | text-align: center;
206 | }
207 |
208 | /*
209 | ** rework 分界线
210 | ** ------------
211 | */
212 |
213 | /* 颜色 */
214 |
215 | $white: #ffffff;
216 | $light: #d5dbdb;
217 | $title: #2c3e50;
218 | $text: #34495e;
219 |
220 | /* 基础布局 */
221 |
222 | $padding: 15px;
223 | $border: 1px solid $light;
224 | $radius: 4px;
225 | $font-title: 28px;
226 | $font-content: 28px;
227 | $font-info: 24px;
228 | $font-tag: 20px;
229 |
230 | /* 综合布局 */
231 |
232 | .text-overflow {
233 | white-space: nowrap;
234 | text-overflow: ellipsis;
235 | overflow: hidden;
236 | }
237 |
238 | .title-nooverflow {
239 | color: $title;
240 | font-size: $font-title;
241 | line-height: 1.5;
242 | }
243 |
244 | .title {
245 | color: $title;
246 | font-size: $font-title;
247 | line-height: 1.5;
248 | @extend .text-overflow;
249 | }
250 |
251 | .markdown-text {
252 | font-size: 28px;
253 | }
254 |
255 | .tag {
256 | padding: 5px 6px;
257 | font-size: $font-tag;
258 | font-weight: 400;
259 | border-radius: $radius;
260 | background-color: $colore7;
261 | text-align: center;
262 | vertical-align: middle;
263 | }
264 |
265 | .user-avatar {
266 | display: block;
267 | width: 40px;
268 | height: 40px;
269 | border-radius: 50%;
270 | margin-right: 10px;
271 | border: 1px solid #f3f3f3;
272 | }
273 |
274 | .week-alert {
275 | position: fixed;
276 | top: 0;
277 | left: 0;
278 | right: 0;
279 | text-align: center;
280 | background-color: rgba(75, 75, 75, 0.85);
281 | color: #fff;
282 | padding: 9px 20px;
283 | font-size: 15px;
284 | z-index: 999;
285 | &.alert-show {
286 | animation: show 2s;
287 | }
288 | @keyframes show {
289 | 0% {
290 | transform: translateY(-100%);
291 | }
292 | 20% {
293 | transform: translateY(0);
294 | }
295 | 50% {
296 | transform: translateY(0);
297 | }
298 | 80% {
299 | transform: translateY(0);
300 | }
301 | 100% {
302 | transform: translateY(-100%);
303 | }
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/pages/message/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { withUser } from '@hoc/router';
3 | import { View, Image, Text } from '@ui';
4 | import Link from '@components/link';
5 | import Header from '@components/header/index';
6 | import classNames from 'classnames';
7 | import * as utils from '@libs/utils';
8 | import { post, get } from '@utils/request';
9 | // import { withRouter } from "next/router";
10 | import Layout from '@components/layout';
11 | import { IAuth } from '@interfaces/auth';
12 |
13 | type PageStateProps = {
14 | userInfo: IAuth;
15 | };
16 |
17 | type PageDispatchProps = {
18 | authLogin: (token) => Promise;
19 | };
20 |
21 | type PageOwnProps = {};
22 |
23 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps;
24 |
25 | interface PageState {}
26 |
27 | class Message extends Component {
28 | state = {
29 | showMenu: false,
30 | selectItem: 2,
31 | message: {
32 | hasnot_read_messages: [],
33 | has_read_messages: [],
34 | },
35 | noData: false,
36 | currentData: [],
37 | no_read_len: 0,
38 | };
39 | changeItem = (idx) => {
40 | const currentData =
41 | idx === 1
42 | ? this.state.message.hasnot_read_messages
43 | : this.state.message.has_read_messages;
44 | this.setState((prevState) => ({
45 | ...prevState,
46 | selectItem: idx,
47 | currentData: currentData,
48 | noData: currentData.length === 0,
49 | }));
50 | };
51 | componentDidShow() {
52 | const { userInfo } = this.props;
53 | get({
54 | url: 'https://cnodejs.org/api/v1/messages?accesstoken=' + userInfo.token,
55 | }).then((resp) => {
56 | const d = resp.data;
57 | const willdata: any = {};
58 | if (d && d.data) {
59 | willdata.message = d.data;
60 | willdata.no_read_len = d.data.hasnot_read_messages.length;
61 | if (d.data.hasnot_read_messages.length > 0) {
62 | willdata.currentData = d.data.hasnot_read_messages;
63 | } else {
64 | willdata.currentData = d.data.has_read_messages;
65 | willdata.selectItem = 2;
66 | }
67 | willdata.noData = willdata.currentData.length === 0;
68 | } else {
69 | willdata.noData = true;
70 | }
71 | this.setState({ ...willdata });
72 | });
73 | }
74 | markall = () => {
75 | const { userInfo } = this.props;
76 | post({
77 | url: 'https://cnodejs.org/api/v1/message/mark_all',
78 | data: {
79 | accesstoken: userInfo.token,
80 | },
81 | }).then((resp) => {
82 | const d = resp.data;
83 | if (d && d.success) {
84 | window.location.reload();
85 | }
86 | });
87 | };
88 | render() {
89 | const { currentData, no_read_len, selectItem, noData } = this.state;
90 | const getLastTimeStr = (date, friendly) => {
91 | return utils.getLastTimeStr(date, friendly);
92 | };
93 | return (
94 |
95 |
102 |
103 |
104 |
111 | 已读消息
112 |
113 |
116 | 未读消息
117 | {no_read_len > 0 ? (
118 |
119 |
120 |
121 | ) : (
122 | ''
123 | )}
124 |
125 |
126 |
127 | {currentData.map((item, idx) => {
128 | return (
129 |
130 |
131 |
132 |
133 |
134 | {item.author.loginname}
135 | {item.type === 'at' ? (
136 | 在回复中@了您
137 | ) : (
138 | ''
139 | )}
140 | {item.type === 'reply' ? (
141 | 回复了您的话题
142 | ) : (
143 | ''
144 | )}
145 |
146 |
147 |
148 | {getLastTimeStr(item.reply.create_at, true)}
149 |
150 |
151 |
152 |
153 |
157 |
158 |
159 | 话题:
160 | {item.topic.title}
161 |
162 |
163 |
164 | );
165 | })}
166 | {noData ? (
167 |
168 |
169 | 暂无数据!
170 |
171 | ) : (
172 | ''
173 | )}
174 |
175 |
176 |
177 | );
178 | }
179 | }
180 |
181 | export default withUser(Message);
182 |
--------------------------------------------------------------------------------
/pages/user/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Component } from '@hoc/router';
3 | import { connect } from 'react-redux';
4 | import classNames from 'classnames';
5 | import { withRouter, NextRouter } from 'next/router';
6 | import { View, Image, Text } from '@ui';
7 | import Header from '@components/header/index';
8 | import Link from '@components/link';
9 | import * as actions from '@actions/auth';
10 | import * as utils from '@libs/utils';
11 | import { get } from '@utils/request';
12 | import { IAuth } from '@interfaces/auth';
13 | import Layout from '@components/layout';
14 |
15 | type PageStateProps = {
16 | userInfo: IAuth;
17 | router: NextRouter;
18 | };
19 |
20 | type PageDispatchProps = {
21 | authCheckState: () => void;
22 | };
23 |
24 | type PageOwnProps = {};
25 |
26 | interface PageState {
27 | user: {
28 | avatar_url: string;
29 | recent_replies?: any[];
30 | recent_topics?: any[];
31 | loginname: string;
32 | create_at: string;
33 | score: number;
34 | };
35 | showMenu: boolean;
36 | selectItem: boolean | number;
37 | currentData: any[];
38 | }
39 |
40 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps;
41 |
42 | class User extends Component {
43 | state = {
44 | currentData: [],
45 | user: {
46 | avatar_url: '',
47 | recent_replies: [],
48 | recent_topics: [],
49 | loginname: '',
50 | create_at: '',
51 | score: 0,
52 | },
53 | showMenu: false,
54 | selectItem: 1,
55 | };
56 |
57 | componentDidMount() {
58 | this.getUser();
59 | }
60 |
61 | componentDidHide() {}
62 |
63 | changeItem = (idx) => {
64 | const currentData =
65 | idx === 1
66 | ? this.state.user.recent_replies
67 | : this.state.user.recent_topics;
68 | this.setState((prevState) => ({
69 | ...prevState,
70 | selectItem: idx,
71 | currentData: currentData,
72 | }));
73 | };
74 | async getUser() {
75 | const { loginname } = this.props.router.query;
76 | if (!loginname) {
77 | utils.showToast({
78 | title: '缺少用户名参数',
79 | });
80 | utils.navigateTo({
81 | url: '/',
82 | });
83 | return false;
84 | }
85 | const res = await get({
86 | url: `https://cnodejs.org/api/v1/user/${loginname}`,
87 | });
88 |
89 | let d = res.data;
90 | if (d && d.data) {
91 | let data = d.data;
92 | this.setState({
93 | user: data,
94 | });
95 | if (data.recent_replies.length > 0) {
96 | this.setState({
97 | currentData: data.recent_replies,
98 | });
99 | } else {
100 | this.setState({
101 | currentData: data.recent_topics,
102 | selectItem: 2,
103 | });
104 | }
105 | }
106 | }
107 |
108 | render() {
109 | const { selectItem, user, currentData } = this.state;
110 | const getLastTimeStr = (date, friendly) => {
111 | return utils.getLastTimeStr(date, friendly);
112 | };
113 | return (
114 |
115 |
121 |
122 |
123 |
124 | {user.loginname}
125 |
126 |
127 | {getLastTimeStr(user.create_at, false)}
128 |
129 |
130 | 积分:
131 | {user.score}
132 |
133 |
134 |
135 |
136 |
137 |
144 | 最近回复
145 |
146 |
152 | 最新发布
153 |
154 |
155 | {currentData.map((item) => {
156 | return (
157 |
158 |
159 |
165 |
166 |
167 |
170 | {item.title}
171 |
172 | {item.author.loginname}
173 |
174 |
175 |
176 | {getLastTimeStr(item.last_reply_at, true)}
177 |
178 |
179 |
180 |
181 |
182 | );
183 | })}
184 | {currentData.length === 0 ? (
185 |
186 |
187 | 暂无数据!
188 |
189 | ) : (
190 | ''
191 | )}
192 |
193 |
194 | );
195 | }
196 | }
197 |
198 | export default connect(
199 | ({ auth }) => ({
200 | userInfo: auth,
201 | }),
202 | (dispatch: Function) => ({
203 | authCheckState() {
204 | dispatch(actions.authCheckState());
205 | },
206 | }),
207 | )(withRouter(User as React.ComponentType));
208 |
--------------------------------------------------------------------------------
/pages/topic/index.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentClass } from 'react';
2 | import React, { Component } from 'react';
3 | import { View, Text, Image } from '@ui';
4 | import Header from '@components/header';
5 | import Link from '@components/link';
6 | import Reply from '@components/reply';
7 | import classNames from 'classnames';
8 | import * as utils from '@libs/utils';
9 | import { withUser } from '@hoc/router';
10 | import update from 'immutability-helper';
11 | import { post, get } from '@utils/request';
12 | import BackTop from '@components/backtotop';
13 | import { withRouter, NextRouter } from 'next/router';
14 | import Layout from '@components/layout';
15 | import { IAuth } from '@interfaces/auth';
16 |
17 | type PageStateProps = {
18 | userInfo: IAuth;
19 | router: NextRouter;
20 | state: IState;
21 | };
22 |
23 | type PageDispatchProps = {
24 | getUserInfo: () => void;
25 | setUserInfo: () => void;
26 | };
27 |
28 | type PageOwnProps = {};
29 |
30 | type PageState = {};
31 |
32 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps;
33 |
34 | type IState = {
35 | noData;
36 | topicId;
37 | showMenu;
38 | curReplyId;
39 | topic;
40 | };
41 |
42 | class Topic extends Component {
43 | static async getInitialProps({ query: { id } }) {
44 | let stopic = {};
45 | if (utils.isServer) {
46 | const topic = await get({
47 | url: 'https://cnodejs.org/api/v1/topic/' + id,
48 | });
49 | stopic = topic.data.data;
50 | }
51 | return {
52 | state: {
53 | showMenu: false,
54 | noData: false,
55 | topic: stopic,
56 | topicId: id,
57 | },
58 | };
59 | }
60 |
61 | state = {
62 | showMenu: false,
63 | topic: {
64 | title: '',
65 | create_at: '',
66 | visit_count: 0,
67 | content: '',
68 | tab: '',
69 | good: false,
70 | top: false,
71 | reply_count: 0,
72 | author: {
73 | avatar_url: '',
74 | loginname: '',
75 | },
76 | replies: [],
77 | },
78 | noData: false,
79 | topicId: '',
80 | curReplyId: '',
81 | };
82 | constructor(props) {
83 | super(props);
84 | this.state = props.state;
85 | }
86 | componentDidMount() {
87 | if (utils.isEmptyObject(this.state.topic)) {
88 | this.getTopic();
89 | }
90 | }
91 | addReply(id) {
92 | this.setState({ curReplyId: id });
93 | if (!this.props.userInfo.userId) {
94 | }
95 | }
96 |
97 | hideItemReply() {
98 | this.setState({ curReplyId: '' });
99 | }
100 | upReply(item, index) {
101 | const { userInfo } = this.props;
102 | const { topic } = this.state;
103 | if (!userInfo.userId) {
104 | utils.navigateTo({
105 | url: '/login',
106 | params: {
107 | redirect: encodeURIComponent(this.props.router.asPath),
108 | },
109 | });
110 | } else {
111 | post({
112 | url: 'https://cnodejs.org/api/v1/reply/' + item.id + '/ups',
113 | data: {
114 | accesstoken: userInfo.token,
115 | },
116 | }).then((resp) => {
117 | let res = resp.data;
118 | if (res.success) {
119 | if (res.action === 'down') {
120 | let index = utils.inArray(userInfo.userId, item.ups);
121 | item.ups.splice(index, 1);
122 | } else {
123 | item.ups.push(userInfo.userId);
124 | }
125 | update(topic.replies, {
126 | [index]: {
127 | $set: item,
128 | },
129 | });
130 | this.setState({ topic: topic });
131 | }
132 | });
133 | }
134 | }
135 | getTopic = () => {
136 | const topicId = this.state.topicId;
137 | this.setState({ topicId });
138 | get({
139 | url: 'https://cnodejs.org/api/v1/topic/' + topicId,
140 | // data: {
141 | // mdrender: false
142 | // }
143 | }).then((resp) => {
144 | let d = resp.data;
145 | if (d && d.data) {
146 | this.setState({ topic: d.data });
147 | } else {
148 | this.setState({ noData: true });
149 | }
150 | });
151 | };
152 | render() {
153 | const { noData, topicId, showMenu, curReplyId, topic } =
154 | this.state || this.props.state;
155 | const { userInfo } = this.props;
156 | const getLastTimeStr = (Text, ago) => {
157 | return utils.getLastTimeStr(Text, ago);
158 | };
159 |
160 | const getTabInfo = (tab, good = false, top, isClass) => {
161 | return utils.getTabInfo(tab, good, top, isClass);
162 | };
163 | const isUps = (ups) => {
164 | return ups.includes((userInfo || ({} as IAuth)).userId);
165 | };
166 | const replayList =
167 | topic.replies &&
168 | topic.replies.map((item, index) => {
169 | return (
170 |
171 |
172 |
177 |
178 |
179 |
180 |
181 | {item.author.loginname}
182 |
183 |
184 | 发布于:
185 | {getLastTimeStr(item.create_at, true)}
186 |
187 |
188 |
189 |
196 |
197 |
198 | {item.ups.length}
199 |
202 |
203 |
204 |
205 |
206 |
207 |
211 | {userInfo.userId && curReplyId === item.id ? (
212 | {
215 | fn(topic, this);
216 | }}
217 | topicId={topicId}
218 | replyId={item.id}
219 | replyTo={item.author.loginname}
220 | show={curReplyId}
221 | onClose={this.hideItemReply.bind(this)}
222 | />
223 | ) : (
224 | ''
225 | )}
226 |
227 | );
228 | });
229 |
230 | return (
231 |
232 |
233 | {topic.title ? (
234 |
239 | {topic.title}
240 |
241 |
246 |
247 |
248 |
249 | {topic.author.loginname}
250 |
251 | 发布于:
252 | {getLastTimeStr(topic.create_at, true)}
253 |
254 |
255 |
256 |
260 | {getTabInfo(topic.tab, topic.good, topic.top, false)}
261 |
262 |
263 | {topic.visit_count}
264 | 次浏览
265 |
266 |
267 |
268 |
272 |
273 | {topic.reply_count} 回复
274 |
275 |
276 | {replayList}
277 |
278 |
279 | {userInfo.userId ? (
280 | {
283 | fn(topic, this);
284 | }}
285 | topicId={topicId}
286 | />
287 | ) : (
288 | ''
289 | )}
290 |
291 | ) : (
292 | ''
293 | )}
294 | {noData ? (
295 |
296 |
297 | 该话题不存在!
298 |
299 | ) : (
300 | ''
301 | )}
302 |
303 | );
304 | }
305 | }
306 |
307 | export default withUser(
308 | withRouter(Topic) as ComponentClass,
309 | true,
310 | );
311 |
--------------------------------------------------------------------------------
/src/libs/utils.tsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // import _ from 'lodash';
4 | import * as Timeago from 'timeago.js';
5 | // import React from "react";
6 | // import { ITopic } from "../interfaces/topic";
7 | import Router from 'next/router';
8 |
9 | export const updateObject = (oldObject, updatedProperties) => {
10 | return {
11 | ...oldObject,
12 | ...updatedProperties,
13 | };
14 | };
15 |
16 | export const typeOf = (obj) => {
17 | return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
18 | };
19 |
20 | export const isObject = (obj) => {
21 | return typeOf(obj) === 'object';
22 | };
23 |
24 | export const isEmptyObject = (obj) => {
25 | let name;
26 | for (name in obj) {
27 | return false;
28 | }
29 | return true;
30 | };
31 |
32 | type UrlParams = {
33 | url: string;
34 | params?: object;
35 | };
36 |
37 | export const navigateTo = ({ url, params }: UrlParams) => {
38 | const href = url + (params ? '?' + param(params) : '');
39 | Router.push(href);
40 | };
41 |
42 | export const redirectTo = ({ url, params }: UrlParams) => {
43 | const href = url + (params ? '?' + param(params) : '');
44 | Router.replace(href);
45 | };
46 |
47 | export const showToast = ({ title }) => {
48 | alert(title);
49 | };
50 |
51 | export const isServer = typeof window === 'undefined';
52 |
53 | export const getEnv = () => {
54 | if (typeof window === 'undefined') {
55 | return 'SERVER';
56 | } else {
57 | return 'WEB';
58 | }
59 | };
60 |
61 | let getCheck = {
62 | checkEmail(val) {
63 | var filter = /^([a-zA-Z0-9_\\.\\-])+\\@(([a-zA-Z0-9\\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
64 | if (filter.test(val)) {
65 | return true;
66 | } else {
67 | return false;
68 | }
69 | },
70 | checkPhone(val) {
71 | var filter = /^1\d{10}$/;
72 |
73 | if (filter.test(val)) {
74 | return true;
75 | } else {
76 | return false;
77 | }
78 | },
79 | };
80 |
81 | const uniq = (x) => {
82 | const s = new Set(x);
83 | return Array.from(s);
84 | };
85 |
86 | /**
87 | * 从文本中提取出@username 标记的用户名数组
88 | * @param {String} text 文本内容
89 | * @return {Array} 用户名数组
90 | */
91 | const fetchUsers = (text) => {
92 | if (!text) {
93 | return [];
94 | }
95 |
96 | var ignoreRegexs = [
97 | /```.+?```/g, // 去除单行的 ```
98 | /^```[\s\S]+?^```/gm, // ``` 里面的是 pre 标签内容
99 | /`[\s\S]+?`/g, // 同一行中,`some code` 中内容也不该被解析
100 | /^.*/gm, // 4个空格也是 pre 标签,在这里 . 不会匹配换行
101 | /\b\S*?@[^\s]*?\..+?\b/g, // somebody@gmail.com 会被去除
102 | /\[@.+?\\]\(\/.+?\)/g, // 已经被 link 的 username
103 | ];
104 |
105 | ignoreRegexs.forEach((ignoreRegex) => {
106 | text = text.replace(ignoreRegex, '');
107 | });
108 |
109 | var results = text.match(/@[a-z0-9\-_]+\b/gim);
110 | var names = [];
111 | if (results) {
112 | for (var i = 0, l = results.length; i < l; i++) {
113 | var s = results[i];
114 | // remove leading char @
115 | s = s.slice(1);
116 | names.push(s);
117 | }
118 | }
119 | names = uniq(names);
120 | return names;
121 | };
122 |
123 | /**
124 | * 根据文本内容,替换为数据库中的数据
125 | * @param {String} text 文本内容
126 | * @param {Function} callback 回调函数
127 | */
128 | const linkUsers = (text) => {
129 | var users = fetchUsers(text);
130 | for (var i = 0, l = users.length; i < l; i++) {
131 | var name = users[i];
132 | text = text.replace(
133 | new RegExp('@' + name + '\\b(?!\\])', 'g'),
134 | '[@' + name + '](/user/' + name + ')',
135 | );
136 | }
137 | return text;
138 | };
139 |
140 | /**
141 | * 对Date的扩展,将 Date 转化为指定格式的String
142 | * 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
143 | * 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
144 | * 例子:
145 | * (new Date()).Format('yyyy-MM-dd hh:mm:ss.S') ==> 2006-07-02 08:09:04.423
146 | * (new Date()).Format('yyyy-M-d h:m:s.S') ==> 2006-7-2 8:9:4.18
147 | */
148 | const fmtDate = (date, fmt) => {
149 | // author: meizz
150 | var o = {
151 | 'M+': date.getMonth() + 1, // 月份
152 | 'd+': date.getDate(), // 日
153 | 'h+': date.getHours(), // 小时
154 | 'm+': date.getMinutes(), // 分
155 | 's+': date.getSeconds(), // 秒
156 | 'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
157 | S: date.getMilliseconds(), // 毫秒
158 | };
159 | if (/(y+)/.test(fmt)) {
160 | fmt = fmt.replace(
161 | RegExp.$1,
162 | (date.getFullYear() + '').substr(4 - RegExp.$1.length),
163 | );
164 | }
165 | for (var k in o) {
166 | if (new RegExp('(' + k + ')').test(fmt)) {
167 | fmt = fmt.replace(
168 | RegExp.$1,
169 | RegExp.$1.length === 1
170 | ? o[k]
171 | : ('00' + o[k]).substr(('' + o[k]).length),
172 | );
173 | }
174 | }
175 | return fmt;
176 | };
177 |
178 | /**
179 | * 调用Timeago库显示到现在的时间
180 | */
181 | export const MillisecondToDate = (time) => {
182 | var str = '';
183 | if (time !== null && time !== '') {
184 | str = Timeago.format(time, 'zh_CN');
185 | }
186 | return str;
187 | };
188 |
189 | export const inArray = (str, arr) => {
190 | return arr.indexOf(str);
191 | };
192 |
193 | export const getContentHtml = (v) => {
194 | return v;
195 | // let dom = document.createElement("div");
196 | // dom.className = "markdown-text";
197 | // dom.innerHTML = v;
198 | // return dom.outerHTML;
199 | };
200 |
201 | /**
202 | * 格式化日期或时间
203 | * @param {string} time 需要格式化的时间
204 | * @param {bool} friendly 是否是fromNow
205 | */
206 | export const getLastTimeStr = (time, friendly) => {
207 | if (friendly) {
208 | return MillisecondToDate(time);
209 | } else {
210 | return fmtDate(new Date(time), 'yyyy-MM-dd hh:mm');
211 | }
212 | };
213 |
214 | /**
215 | * 获取不同tab的信息
216 | * @param {[type]} tab [tab分类]
217 | * @param {[type]} good [是否是精华]
218 | * @param {[type]} top [是否置顶]
219 | * @param {Boolean} isClass [是否是样式]
220 | * @return {[type]} [返回对应字符串]
221 | */
222 | export const getTabInfo = (tab, good, top, isClass) => {
223 | let str = '';
224 | let className = '';
225 | if (top) {
226 | str = '置顶';
227 | className = 'top';
228 | } else if (good) {
229 | str = '精华';
230 | className = 'good';
231 | } else {
232 | switch (tab) {
233 | case 'share':
234 | str = '分享';
235 | className = 'share';
236 | break;
237 | case 'ask':
238 | str = '问答';
239 | className = 'ask';
240 | break;
241 | case 'job':
242 | str = '招聘';
243 | className = 'job';
244 | break;
245 | default:
246 | str = '暂无';
247 | className = 'default';
248 | break;
249 | }
250 | }
251 | return isClass ? className : str;
252 | };
253 |
254 | /**
255 | * 配置节流函数
256 | * @param {[Function]} fn [要执行的函数]
257 | * @param {[Number]} delay [延迟执行的毫秒数]
258 | * @param {[Number]} mustRun [至少多久执行一次]
259 | * @return {[Function]} [节流函数]
260 | */
261 | export const throttle = (fn, wait, mustRun) => {
262 | let timeout;
263 | let startTime = +new Date();
264 | return function () {
265 | let context = this;
266 | let args = arguments;
267 | let curTime = +new Date();
268 |
269 | clearTimeout(timeout);
270 | // 如果达到了规定的触发时间间隔,触发 handler
271 | if (curTime - startTime >= mustRun) {
272 | fn.apply(context, args);
273 | startTime = curTime;
274 | // 没达到触发间隔,重新设定定时器
275 | } else {
276 | timeout = setTimeout(fn, wait);
277 | }
278 | };
279 | };
280 |
281 | export { linkUsers, fetchUsers, getCheck, fmtDate };
282 |
283 | // tslint:disable-next-line
284 | // export const Thread_DETAIL_NAVIGATE = 'thread_detail_navigate'
285 |
286 | // export interface ITopicProps extends ITopic {
287 | // tid: string
288 | // }
289 |
290 | // eventCenter.on(Thread_DETAIL_NAVIGATE, (topic: ITopicProps) => {
291 | // GlobalState.topic = topic
292 | // })
293 |
294 | // export const GlobalState = {
295 | // topic: {}
296 | // as ITopicProps
297 | // }
298 |
299 | // 数字/英文与中文之间需要加空格
300 | const betterChineseDict = (_, index): [string, string] => {
301 | return [
302 | ['刚刚', '片刻后'],
303 | ['%s 秒前', '%s 秒后'],
304 | ['1 分钟前', '1 分钟后'],
305 | ['%s 分钟前', '%s 分钟后'],
306 | ['1 小时前', '1小 时后'],
307 | ['%s 小时前', '%s 小时后'],
308 | ['1 天前', '1 天后'],
309 | ['%s 天前', '%s 天后'],
310 | ['1 周前', '1 周后'],
311 | ['%s 周前', '%s 周后'],
312 | ['1 月前', '1 月后'],
313 | ['%s 月前', '%s 月后'],
314 | ['1 年前', '1 年后'],
315 | ['%s 年前', '%s 年后'],
316 | ][index] as [string, string];
317 | };
318 |
319 | Timeago.register('zh', betterChineseDict);
320 |
321 | export const trim = (v) => {
322 | var re = /^\s+|\s+$/g;
323 | return v.replace(re, '');
324 | };
325 |
326 | export const param = function (a) {
327 | var s = [];
328 | var add = function (k, v) {
329 | v = typeof v === 'function' ? v() : v;
330 | v = v === null ? '' : v === undefined ? '' : v;
331 | s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v);
332 | };
333 | var buildParams = function (prefix, obj) {
334 | var i, len, key;
335 |
336 | if (prefix) {
337 | if (Array.isArray(obj)) {
338 | for (i = 0, len = obj.length; i < len; i++) {
339 | buildParams(
340 | prefix +
341 | '[' +
342 | (typeof obj[i] === 'object' && obj[i] ? i : '') +
343 | ']',
344 | obj[i],
345 | );
346 | }
347 | } else if (String(obj) === '[object Object]') {
348 | for (key in obj) {
349 | buildParams(prefix + '[' + key + ']', obj[key]);
350 | }
351 | } else {
352 | add(prefix, obj);
353 | }
354 | } else if (Array.isArray(obj)) {
355 | for (i = 0, len = obj.length; i < len; i++) {
356 | add(obj[i].name, obj[i].value);
357 | }
358 | } else {
359 | for (key in obj) {
360 | buildParams(key, obj[key]);
361 | }
362 | }
363 | return s;
364 | };
365 |
366 | return buildParams('', a).join('&');
367 | };
368 |
--------------------------------------------------------------------------------
/src/assets/scss/github-markdown.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: octicons-anchor;
3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==) format('woff');
4 | }
5 |
6 | .markdown-body {
7 | -webkit-text-size-adjust: 100%;
8 | text-size-adjust: 100%;
9 | color: #333;
10 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif;
11 | font-size: 32px;
12 | line-height: 1.6;
13 | word-wrap: break-word;
14 | }
15 |
16 | .markdown-body a {
17 | background-color: transparent;
18 | }
19 |
20 | .markdown-body a:active,
21 | .markdown-body a:hover {
22 | outline: 0;
23 | }
24 |
25 | .markdown-body strong {
26 | font-weight: bold;
27 | }
28 |
29 | .markdown-body h1 {
30 | font-size: 2em;
31 | margin: 0.67em 0;
32 | }
33 |
34 | .markdown-body img {
35 | border: 0;
36 | }
37 |
38 | .markdown-body hr {
39 | box-sizing: content-box;
40 | height: 0;
41 | }
42 |
43 | .markdown-body pre {
44 | overflow: auto;
45 | }
46 |
47 | .markdown-body code,
48 | .markdown-body kbd,
49 | .markdown-body pre {
50 | font-family: monospace, monospace;
51 | font-size: 1em;
52 | }
53 |
54 | .markdown-body input {
55 | color: inherit;
56 | font: inherit;
57 | margin: 0;
58 | }
59 |
60 | .markdown-body html input[disabled] {
61 | cursor: default;
62 | }
63 |
64 | .markdown-body input {
65 | line-height: normal;
66 | }
67 |
68 | .markdown-body input[type="checkbox"] {
69 | box-sizing: border-box;
70 | padding: 0;
71 | }
72 |
73 | .markdown-body table {
74 | border-collapse: collapse;
75 | border-spacing: 0;
76 | }
77 |
78 | .markdown-body td,
79 | .markdown-body th {
80 | padding: 0;
81 | }
82 |
83 |
84 |
85 | .markdown-body input {
86 | font: 13px/1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
87 | }
88 |
89 | .markdown-body a {
90 | color: #4078c0;
91 | text-decoration: none;
92 | }
93 |
94 | .markdown-body a:hover,
95 | .markdown-body a:active {
96 | text-decoration: underline;
97 | }
98 |
99 | .markdown-body hr {
100 | height: 0;
101 | margin: 15px 0;
102 | overflow: hidden;
103 | background: transparent;
104 | border: 0;
105 | border-bottom: 1px solid #ddd;
106 | }
107 |
108 | .markdown-body hr:before {
109 | display: table;
110 | content: "";
111 | }
112 |
113 | .markdown-body hr:after {
114 | display: table;
115 | clear: both;
116 | content: "";
117 | }
118 |
119 | .markdown-body h1,
120 | .markdown-body h2,
121 | .markdown-body h3,
122 | .markdown-body h4,
123 | .markdown-body h5,
124 | .markdown-body h6 {
125 | margin-top: 15px;
126 | margin-bottom: 15px;
127 | line-height: 1.1;
128 | }
129 |
130 | .markdown-body h1 {
131 | font-size: 30px;
132 | }
133 |
134 | .markdown-body h2 {
135 | font-size: 21px;
136 | }
137 |
138 | .markdown-body h3 {
139 | font-size: 16px;
140 | }
141 |
142 | .markdown-body h4 {
143 | font-size: 14px;
144 | }
145 |
146 | .markdown-body h5 {
147 | font-size: 12px;
148 | }
149 |
150 | .markdown-body h6 {
151 | font-size: 11px;
152 | }
153 |
154 | .markdown-body blockquote {
155 | margin: 0;
156 | }
157 |
158 | .markdown-body ul,
159 | .markdown-body ol {
160 | padding: 0;
161 | margin-top: 0;
162 | margin-bottom: 0;
163 | }
164 |
165 | .markdown-body ol ol,
166 | .markdown-body ul ol {
167 | list-style-type: lower-roman;
168 | }
169 |
170 | .markdown-body ul ul ol,
171 | .markdown-body ul ol ol,
172 | .markdown-body ol ul ol,
173 | .markdown-body ol ol ol {
174 | list-style-type: lower-alpha;
175 | }
176 |
177 | .markdown-body dd {
178 | margin-left: 0;
179 | }
180 |
181 | .markdown-body code {
182 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
183 | font-size: 12px;
184 | }
185 |
186 | .markdown-body pre {
187 | margin-top: 0;
188 | margin-bottom: 0;
189 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
190 | }
191 |
192 | .markdown-body .select::-ms-expand {
193 | opacity: 0;
194 | }
195 |
196 | .markdown-body .octicon {
197 | font: normal normal normal 16px/1 octicons-anchor;
198 | display: inline-block;
199 | text-decoration: none;
200 | text-rendering: auto;
201 | -webkit-font-smoothing: antialiased;
202 | -moz-osx-font-smoothing: grayscale;
203 | user-select: none;
204 | }
205 |
206 | .markdown-body .octicon-link:before {
207 | content: '\f05c';
208 | }
209 |
210 | .markdown-body div:first-child {
211 | margin-top: 0 !important;
212 | }
213 |
214 | .markdown-body div:last-child {
215 | margin-bottom: 0 !important;
216 | }
217 |
218 | .markdown-body a:not([href]) {
219 | color: inherit;
220 | text-decoration: none;
221 | }
222 |
223 | .markdown-body .anchor {
224 | display: inline-block;
225 | padding-right: 2px;
226 | margin-left: -18px;
227 | }
228 |
229 | .markdown-body .anchor:focus {
230 | outline: none;
231 | }
232 |
233 | .markdown-body h1,
234 | .markdown-body h2,
235 | .markdown-body h3,
236 | .markdown-body h4,
237 | .markdown-body h5,
238 | .markdown-body h6 {
239 | margin-top: 1em;
240 | margin-bottom: 16px;
241 | font-weight: bold;
242 | line-height: 1.4;
243 | }
244 |
245 | .markdown-body h1 .octicon-link,
246 | .markdown-body h2 .octicon-link,
247 | .markdown-body h3 .octicon-link,
248 | .markdown-body h4 .octicon-link,
249 | .markdown-body h5 .octicon-link,
250 | .markdown-body h6 .octicon-link {
251 | color: #000;
252 | vertical-align: middle;
253 | visibility: hidden;
254 | }
255 |
256 | .markdown-body h1:hover .anchor,
257 | .markdown-body h2:hover .anchor,
258 | .markdown-body h3:hover .anchor,
259 | .markdown-body h4:hover .anchor,
260 | .markdown-body h5:hover .anchor,
261 | .markdown-body h6:hover .anchor {
262 | text-decoration: none;
263 | }
264 |
265 | .markdown-body h1:hover .anchor .octicon-link,
266 | .markdown-body h2:hover .anchor .octicon-link,
267 | .markdown-body h3:hover .anchor .octicon-link,
268 | .markdown-body h4:hover .anchor .octicon-link,
269 | .markdown-body h5:hover .anchor .octicon-link,
270 | .markdown-body h6:hover .anchor .octicon-link {
271 | visibility: visible;
272 | }
273 |
274 | .markdown-body h1 {
275 | padding-bottom: 0.3em;
276 | font-size: 2.25em;
277 | line-height: 1.2;
278 | border-bottom: 1px solid #eee;
279 | }
280 |
281 | .markdown-body h1 .anchor {
282 | line-height: 1;
283 | }
284 |
285 | .markdown-body h2 {
286 | padding-bottom: 0.3em;
287 | font-size: 1.75em;
288 | line-height: 1.225;
289 | border-bottom: 1px solid #eee;
290 | }
291 |
292 | .markdown-body h2 .anchor {
293 | line-height: 1;
294 | }
295 |
296 | .markdown-body h3 {
297 | font-size: 1.5em;
298 | line-height: 1.43;
299 | }
300 |
301 | .markdown-body h3 .anchor {
302 | line-height: 1.2;
303 | }
304 |
305 | .markdown-body h4 {
306 | font-size: 1.25em;
307 | }
308 |
309 | .markdown-body h4 .anchor {
310 | line-height: 1.2;
311 | }
312 |
313 | .markdown-body h5 {
314 | font-size: 1em;
315 | }
316 |
317 | .markdown-body h5 .anchor {
318 | line-height: 1.1;
319 | }
320 |
321 | .markdown-body h6 {
322 | font-size: 1em;
323 | color: #777;
324 | }
325 |
326 | .markdown-body h6 .anchor {
327 | line-height: 1.1;
328 | }
329 |
330 | .markdown-body p,
331 | .markdown-body blockquote,
332 | .markdown-body ul,
333 | .markdown-body ol,
334 | .markdown-body dl,
335 | .markdown-body table,
336 | .markdown-body pre {
337 | margin-top: 0;
338 | margin-bottom: 16px;
339 | }
340 |
341 | .markdown-body hr {
342 | height: 4px;
343 | padding: 0;
344 | margin: 16px 0;
345 | background-color: #e7e7e7;
346 | border: 0 none;
347 | }
348 |
349 | .markdown-body ul,
350 | .markdown-body ol {
351 | padding-left: 2em;
352 | }
353 |
354 | .markdown-body ul ul,
355 | .markdown-body ul ol,
356 | .markdown-body ol ol,
357 | .markdown-body ol ul {
358 | margin-top: 0;
359 | margin-bottom: 0;
360 | }
361 |
362 | .markdown-body li p {
363 | margin-top: 16px;
364 | }
365 |
366 | .markdown-body dl {
367 | padding: 0;
368 | }
369 |
370 | .markdown-body dl dt {
371 | padding: 0;
372 | margin-top: 16px;
373 | font-size: 1em;
374 | font-style: italic;
375 | font-weight: bold;
376 | }
377 |
378 | .markdown-body dl dd {
379 | padding: 0 16px;
380 | margin-bottom: 16px;
381 | }
382 |
383 | .markdown-body blockquote {
384 | padding: 0 15px;
385 | color: #777;
386 | border-left: 4px solid #ddd;
387 | }
388 |
389 | .markdown-body blockquote :first-child {
390 | margin-top: 0;
391 | }
392 |
393 | .markdown-body blockquote :last-child {
394 | margin-bottom: 0;
395 | }
396 |
397 | .markdown-body table {
398 | display: block;
399 | width: 100%;
400 | overflow: auto;
401 | word-break: normal;
402 | word-break: keep-all;
403 | }
404 |
405 | .markdown-body table th {
406 | font-weight: bold;
407 | }
408 |
409 | .markdown-body table th,
410 | .markdown-body table td {
411 | padding: 6px 13px;
412 | border: 1px solid #ddd;
413 | }
414 |
415 | .markdown-body table tr {
416 | background-color: #fff;
417 | border-top: 1px solid #ccc;
418 | }
419 |
420 | .markdown-body table tr:nth-child(2n) {
421 | background-color: #f8f8f8;
422 | }
423 |
424 | .markdown-body img {
425 | max-width: 100%;
426 | box-sizing: content-box;
427 | background-color: #fff;
428 | }
429 |
430 | .markdown-body code {
431 | padding: 0;
432 | padding-top: 0.2em;
433 | padding-bottom: 0.2em;
434 | margin: 0;
435 | font-size: 85%;
436 | background-color: rgba(0,0,0,0.04);
437 | border-radius: 3px;
438 | }
439 |
440 | .markdown-body code:before,
441 | .markdown-body code:after {
442 | letter-spacing: -0.2em;
443 | content: "\00a0";
444 | }
445 |
446 | .markdown-body pre code {
447 | padding: 0;
448 | margin: 0;
449 | font-size: 100%;
450 | word-break: normal;
451 | white-space: pre;
452 | background: transparent;
453 | border: 0;
454 | }
455 |
456 | .markdown-body .highlight {
457 | margin-bottom: 16px;
458 | }
459 |
460 | .markdown-body .highlight pre,
461 | .markdown-body pre {
462 | padding: 16px;
463 | overflow: auto;
464 | font-size: 85%;
465 | line-height: 1.45;
466 | background-color: #f7f7f7;
467 | border-radius: 3px;
468 | }
469 |
470 | .markdown-body .highlight pre {
471 | margin-bottom: 0;
472 | word-break: normal;
473 | }
474 |
475 | .markdown-body pre {
476 | word-wrap: normal;
477 | }
478 |
479 | .markdown-body pre code {
480 | display: inline;
481 | max-width: initial;
482 | padding: 0;
483 | margin: 0;
484 | overflow: initial;
485 | line-height: inherit;
486 | word-wrap: normal;
487 | background-color: transparent;
488 | border: 0;
489 | }
490 |
491 | .markdown-body pre code:before,
492 | .markdown-body pre code:after {
493 | content: normal;
494 | }
495 |
496 | .markdown-body kbd {
497 | display: inline-block;
498 | padding: 3px 5px;
499 | font-size: 11px;
500 | line-height: 10px;
501 | color: #555;
502 | vertical-align: middle;
503 | background-color: #fcfcfc;
504 | border: solid 1px #ccc;
505 | border-bottom-color: #bbb;
506 | border-radius: 3px;
507 | box-shadow: inset 0 -1px 0 #bbb;
508 | }
509 |
510 | /* .markdown-body .pl-c {
511 | color: #969896;
512 | }
513 |
514 | .markdown-body .pl-c1,
515 | .markdown-body .pl-s .pl-v {
516 | color: #0086b3;
517 | }
518 |
519 | .markdown-body .pl-e,
520 | .markdown-body .pl-en {
521 | color: #795da3;
522 | }
523 |
524 | .markdown-body .pl-s .pl-s1,
525 | .markdown-body .pl-smi {
526 | color: #333;
527 | }
528 |
529 | .markdown-body .pl-ent {
530 | color: #63a35c;
531 | }
532 |
533 | .markdown-body .pl-k {
534 | color: #a71d5d;
535 | }
536 |
537 | .markdown-body .pl-pds,
538 | .markdown-body .pl-s,
539 | .markdown-body .pl-s .pl-pse .pl-s1,
540 | .markdown-body .pl-sr,
541 | .markdown-body .pl-sr .pl-cce,
542 | .markdown-body .pl-sr .pl-sra,
543 | .markdown-body .pl-sr .pl-sre {
544 | color: #183691;
545 | }
546 |
547 | .markdown-body .pl-v {
548 | color: #ed6a43;
549 | }
550 |
551 | .markdown-body .pl-id {
552 | color: #b52a1d;
553 | }
554 |
555 | .markdown-body .pl-ii {
556 | background-color: #b52a1d;
557 | color: #f8f8f8;
558 | }
559 |
560 | .markdown-body .pl-sr .pl-cce {
561 | color: #63a35c;
562 | font-weight: bold;
563 | }
564 |
565 | .markdown-body .pl-ml {
566 | color: #693a17;
567 | }
568 |
569 | .markdown-body .pl-mh,
570 | .markdown-body .pl-mh .pl-en,
571 | .markdown-body .pl-ms {
572 | color: #1d3e81;
573 | font-weight: bold;
574 | }
575 |
576 | .markdown-body .pl-mq {
577 | color: #008080;
578 | }
579 |
580 | .markdown-body .pl-mi {
581 | color: #333;
582 | font-style: italic;
583 | }
584 |
585 | .markdown-body .pl-mb {
586 | color: #333;
587 | font-weight: bold;
588 | }
589 |
590 | .markdown-body .pl-md {
591 | background-color: #ffecec;
592 | color: #bd2c00;
593 | }
594 |
595 | .markdown-body .pl-mi1 {
596 | background-color: #eaffea;
597 | color: #55a532;
598 | }
599 |
600 | .markdown-body .pl-mdr {
601 | color: #795da3;
602 | font-weight: bold;
603 | }
604 |
605 | .markdown-body .pl-mo {
606 | color: #1d3e81;
607 | }
608 |
609 | .markdown-body kbd {
610 | display: inline-block;
611 | padding: 3px 5px;
612 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
613 | line-height: 10px;
614 | color: #555;
615 | vertical-align: middle;
616 | background-color: #fcfcfc;
617 | border: solid 1px #ccc;
618 | border-bottom-color: #bbb;
619 | border-radius: 3px;
620 | box-shadow: inset 0 -1px 0 #bbb;
621 | } */
622 |
623 | .markdown-body:before {
624 | display: table;
625 | content: "";
626 | }
627 |
628 | .markdown-body:after {
629 | display: table;
630 | clear: both;
631 | content: "";
632 | }
633 |
634 |
--------------------------------------------------------------------------------