├── .env ├── .prettierignore ├── assets ├── _styles.less ├── self-styles.less └── antd-custom.less ├── .eslintignore ├── redux ├── reducers │ ├── selectors │ │ └── index.ts │ ├── index.ts │ ├── channel.ts │ ├── topic.ts │ └── user.ts ├── actions │ ├── channel.ts │ ├── topic.ts │ └── user.ts ├── store.ts └── sagas │ └── index.ts ├── screenshot ├── demo.gif ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── Nobibi-structure.png ├── static └── favicon.ico ├── .prettierrc ├── next-env.d.ts ├── now.json ├── constants ├── CustomTheme.ts ├── ConstTypes.ts └── ActionTypes.ts ├── .babelrc ├── components ├── NoFooter │ └── index.tsx ├── TopicEditor │ └── index.tsx ├── NoAvatar │ └── index.tsx ├── Editor │ └── index.tsx ├── NoHeader │ ├── index.less │ └── index.tsx ├── TopicItem │ └── index.tsx ├── ErrorPage.tsx ├── NoLayout │ ├── index.tsx │ └── index.less └── CommentList │ └── index.tsx ├── pages ├── _error.tsx ├── _document.tsx ├── _app.tsx ├── login.tsx ├── topicEdit.tsx ├── register.tsx ├── modifyUser.tsx ├── changePass.tsx ├── index.tsx └── topicDetail.tsx ├── tsconfig.json ├── .gitignore ├── @types └── index.d.ts ├── .editorconfig ├── utils ├── index.ts ├── fetch.ts └── timer.ts ├── pm2.config.js ├── README_en.md ├── LICENSE ├── server.js ├── .eslintrc ├── package.json ├── README.md ├── api └── index.ts └── next.config.js /.env: -------------------------------------------------------------------------------- 1 | BASE_URL=http://47.244.103.124:3001 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ -------------------------------------------------------------------------------- /assets/_styles.less: -------------------------------------------------------------------------------- 1 | @import "./antd-custom.less"; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | .editorconfig 4 | -------------------------------------------------------------------------------- /assets/self-styles.less: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.less"; 2 | @import "./antd-custom.less"; -------------------------------------------------------------------------------- /redux/reducers/selectors/index.ts: -------------------------------------------------------------------------------- 1 | export const selectUserInfo = state => state.user; 2 | -------------------------------------------------------------------------------- /screenshot/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seawind8888/Nobibi/HEAD/screenshot/demo.gif -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seawind8888/Nobibi/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /screenshot/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seawind8888/Nobibi/HEAD/screenshot/screenshot1.png -------------------------------------------------------------------------------- /screenshot/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seawind8888/Nobibi/HEAD/screenshot/screenshot2.png -------------------------------------------------------------------------------- /screenshot/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seawind8888/Nobibi/HEAD/screenshot/screenshot3.png -------------------------------------------------------------------------------- /screenshot/Nobibi-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seawind8888/Nobibi/HEAD/screenshot/Nobibi-structure.png -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "Nobibi", 4 | "builds": [{ "src": "package.json", "use": "@now/next" }] 5 | } 6 | -------------------------------------------------------------------------------- /constants/CustomTheme.ts: -------------------------------------------------------------------------------- 1 | /* custom define */ 2 | export const color_youdao = '#E93D34'; 3 | export const color_youdao_border = '#c20c0c'; 4 | export const color_primary = '#52c41a'; 5 | 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": [ 4 | "next/babel", 5 | "@zeit/next-typescript/babel" 6 | ], 7 | "plugins": [ 8 | [ 9 | "import", 10 | { 11 | "libraryName": "antd", 12 | "style": "less" 13 | } 14 | ], 15 | ["lodash"] 16 | ] 17 | } -------------------------------------------------------------------------------- /constants/ConstTypes.ts: -------------------------------------------------------------------------------- 1 | // 用户级别 2 | 3 | 4 | // 路由对应页面标题 5 | export const RouterTitle = { 6 | '/': 'Home', 7 | '/topicDetail': 'TopicDetail', 8 | '/login': 'Nobibi | 登录', 9 | '/register': 'Nobibi | 注册', 10 | '/topicEdit': 'Nobibi | 发布主题', 11 | '/modifyUser': 'Nobibi | 修改用户信息' 12 | }; 13 | -------------------------------------------------------------------------------- /components/NoFooter/index.tsx: -------------------------------------------------------------------------------- 1 | const NoFooter = () => ( 2 |
Nobibi ©2019 Created by seawind8888
7 | ); 8 | export default NoFooter; -------------------------------------------------------------------------------- /assets/antd-custom.less: -------------------------------------------------------------------------------- 1 | /* 系统主题颜色 */ 2 | @color-primary: #52c41a; 3 | 4 | @primary-color: @color-primary; 5 | 6 | @layout-header-height: 40px; 7 | @border-radius-base: 2px; 8 | .ant-menu-horizontal { 9 | border-bottom: none; 10 | } 11 | 12 | .avatar-uploader > .ant-upload { 13 | width: 128px; 14 | height: 128px; 15 | } 16 | .ant-upload.ant-upload-select-picture-card { 17 | margin:0 auto !important; 18 | } -------------------------------------------------------------------------------- /redux/actions/channel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_CHANNEL_LIST, 3 | FETCH_CHANNEL_LIST_SUCCESS, 4 | FETCH_CHANNEL_LIST_FAIL 5 | } from '../../constants/ActionTypes'; 6 | 7 | 8 | export const fetchChannelList = () => ({type: FETCH_CHANNEL_LIST}); 9 | 10 | export const fetchChannelListSuccess = (payload) => ({type: FETCH_CHANNEL_LIST_SUCCESS, payload}); 11 | 12 | export const fetchChannelListFail = () => ({type: FETCH_CHANNEL_LIST_FAIL}); -------------------------------------------------------------------------------- /redux/actions/topic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_TOPIC_LIST, 3 | FETCH_TOPIC_LIST_SUCCESS, 4 | FETCH_TOPIC_LIST_FAIL 5 | } from '../../constants/ActionTypes'; 6 | 7 | 8 | export const fetchTopiclList = (payload = {}) => ({type: FETCH_TOPIC_LIST, payload}); 9 | 10 | export const fetchTopicListSuccess = (payload) => ({type: FETCH_TOPIC_LIST_SUCCESS, payload}); 11 | 12 | export const fetchTopicListFail = () => ({type: FETCH_TOPIC_LIST_FAIL}); -------------------------------------------------------------------------------- /redux/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import user from './user'; 3 | import topic from './topic'; 4 | import { User, Topic } from '../../@types' 5 | import channel, { ChannelStateType } from './channel'; 6 | 7 | export interface AppStateType { 8 | user: User, 9 | channel: ChannelStateType, 10 | topic: Topic 11 | } 12 | 13 | export const rootReducer = combineReducers({ 14 | user, 15 | channel, 16 | topic 17 | }); -------------------------------------------------------------------------------- /redux/actions/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GET_USER_INFO, 3 | GET_USER_INFO_SUCCESS, 4 | GET_USER_INFO_FAIL, 5 | USER_SIGN_OUT 6 | } from '../../constants/ActionTypes'; 7 | 8 | 9 | export const getUserInfo = (payload = {}) => ({type: GET_USER_INFO, payload}); 10 | 11 | export const getUserInfoSuccess = (payload) => ({type: GET_USER_INFO_SUCCESS, payload}); 12 | 13 | export const getUserInfoFail = () => ({type: GET_USER_INFO_FAIL}); 14 | 15 | export const userSignOut = () => ({type: USER_SIGN_OUT}); -------------------------------------------------------------------------------- /pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next'; 2 | import ErrorPage from '../components/ErrorPage'; 3 | 4 | interface ErrorProps { 5 | statusCode: number 6 | } 7 | 8 | const Error = (props: ErrorProps) => { 9 | return ( 10 | 11 | ); 12 | } 13 | 14 | Error.getInitialProps = ({ res, err }: NextPageContext) => { 15 | const statusCode = res ? res.statusCode : err ? err.statusCode : null; 16 | return { statusCode }; 17 | } 18 | 19 | export default Error -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "exclude": ["node_modules"], 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/index.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules/ 5 | npm-debug.log 6 | yarn-error.log 7 | yarn.lock 8 | package-lock.json 9 | # Compiled output 10 | build 11 | 12 | # Runtime data 13 | database.sqlite 14 | 15 | # Test coverage 16 | coverage 17 | 18 | # Logs 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | logs/ 23 | 24 | # Editors and IDEs 25 | .idea 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # Misc 33 | .DS_Store 34 | 35 | # custom 36 | .next 37 | out/ 38 | -------------------------------------------------------------------------------- /@types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | _id?: string, 3 | userName?: string, 4 | password?: string, 5 | avatar?: string, 6 | email?: string, 7 | visit?: number[], 8 | status?: string, 9 | refUserRoleCode?: string, 10 | createTime?: string, 11 | updateTime?: string 12 | } 13 | 14 | export interface Topic { 15 | _id?: string, 16 | avatar?:string, 17 | topicTitle?: string, 18 | content?: string, 19 | total?: number, 20 | list?: object[], 21 | page?: number, 22 | type?: string, 23 | commentNum?: number, 24 | praiseNum?: number, 25 | userName?: string, 26 | userAvatar?: string, 27 | updateTime?: string, 28 | categoryName?: string 29 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig文件使用INI格式。斜杠(/)作为路径分隔符,#或者;作为注释。路径支持通配符: 2 | # 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件 3 | root = true 4 | # * 匹配除/之外的任意字符 5 | # ** 匹配任意字符串 6 | # ? 匹配任意单个字符 7 | # [name] 匹配name字符 8 | # [!name] 不匹配name字符 9 | # [s1,s2,s3] 匹配给定的字符串 10 | # [num1..num2] 匹配num1到mun2直接的整数 11 | [*] 12 | # 文件的charset。有以下几种类型:latin1, utf-8, utf-8-bom, utf-16be, utf-16le 13 | charset = utf-8 14 | # 缩进使用 tab 或者 space 15 | indent_style = space 16 | # 缩进为 space 时,缩进的字符数 17 | indent_size = 2 18 | # 缩进为 tab 时,缩进的宽度 19 | # tab_width = 2 20 | # 换行符的类型。lf, cr, crlf三种 21 | end_of_line = lf 22 | # 是否将行尾空格自动删除 23 | trim_trailing_whitespace = true 24 | # 是否使文件以一个空白行结尾 25 | insert_final_newline = true -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { message } from 'antd'; 4 | 5 | export function getRandomColor(){ 6 | return "#" + ("00000" + ((Math.random() * 16777215 + 0.5) >> 0).toString(16)).slice(-6); 7 | } 8 | 9 | export function getBase64(img, callback) { 10 | const reader = new FileReader(); 11 | reader.addEventListener('load', () => callback(reader.result)); 12 | reader.readAsDataURL(img); 13 | } 14 | 15 | export function beforeUpload(file) { 16 | const isJPG = file.type === 'image/jpeg'; 17 | if (!isJPG) { 18 | message.error('只能上传图片!'); 19 | } 20 | const isLt2M = file.size / 48 / 48 < 2; 21 | if (!isLt2M) { 22 | message.error('图片需小于48KB!'); 23 | } 24 | return isJPG && isLt2M; 25 | } -------------------------------------------------------------------------------- /components/TopicEditor/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { NextPage } from 'next'; 4 | import dynamic from 'next/dynamic'; 5 | import 'braft-editor/dist/index.css'; 6 | 7 | const BraftEditor = dynamic( 8 | import('braft-editor'), 9 | { ssr: false } 10 | ); 11 | 12 | interface TopicEditorProps { 13 | editorValue: object, 14 | editorChange: (e:any) => void 15 | } 16 | 17 | const TopicEditor: NextPage = (props) => { 18 | const { editorValue, editorChange } = props; 19 | return ( 20 | 25 | ); 26 | } 27 | 28 | export default TopicEditor; -------------------------------------------------------------------------------- /constants/ActionTypes.ts: -------------------------------------------------------------------------------- 1 | // ================= Home Part ==================== // 2 | export const FETCH_CHANNEL_LIST = 'FETCH_CHANNEL_LIST'; 3 | export const FETCH_CHANNEL_LIST_SUCCESS = 'FETCH_CHANNEL_LIST_SUCCESS'; 4 | export const FETCH_CHANNEL_LIST_FAIL = 'FETCH_CHANNEL_LIST_FAIL'; 5 | export const FETCH_TOPIC_LIST = 'FETCH_TOPIC_LIST'; 6 | export const FETCH_TOPIC_LIST_SUCCESS = 'FETCH_TOPIC_LIST_SUCCESS'; 7 | export const FETCH_TOPIC_LIST_FAIL = 'FETCH_TOPIC_LIST_FAIL'; 8 | 9 | // ================= User Part ==================== // 10 | export const GET_USER_INFO = 'GET_USER_INFO'; 11 | export const GET_USER_INFO_SUCCESS = 'GET_USER_INFO_SUCCESS'; 12 | export const GET_USER_INFO_FAIL = 'GET_USER_INFO_FAIL'; 13 | export const USER_SIGN_OUT = 'USER_SIGN_OUT'; -------------------------------------------------------------------------------- /redux/reducers/channel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_CHANNEL_LIST, 3 | FETCH_CHANNEL_LIST_SUCCESS, 4 | FETCH_CHANNEL_LIST_FAIL 5 | } from '../../constants/ActionTypes'; 6 | 7 | export interface ChannelStateType { 8 | list: object[] 9 | } 10 | 11 | const initialState = { 12 | list: [] 13 | }; 14 | 15 | const channel = (state: ChannelStateType = initialState, { type, payload = {list:[]} }) => { 16 | switch (type) { 17 | case FETCH_CHANNEL_LIST: 18 | case FETCH_CHANNEL_LIST_SUCCESS: 19 | return { 20 | ...state, 21 | list: payload.list 22 | }; 23 | case FETCH_CHANNEL_LIST_FAIL: 24 | return initialState; 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | export default channel; -------------------------------------------------------------------------------- /redux/reducers/topic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_TOPIC_LIST, 3 | FETCH_TOPIC_LIST_SUCCESS, 4 | FETCH_TOPIC_LIST_FAIL 5 | } from '../../constants/ActionTypes'; 6 | 7 | import { Topic } from '../../@types' 8 | 9 | 10 | const initialState = { 11 | list: [], 12 | categoryName: '', 13 | type: '', 14 | total: 0 15 | }; 16 | 17 | const topic = (state: Topic = initialState, { type, payload = {} }) => { 18 | switch (type) { 19 | case FETCH_TOPIC_LIST: 20 | return initialState; 21 | case FETCH_TOPIC_LIST_SUCCESS: 22 | return { 23 | ...state, 24 | ...payload 25 | }; 26 | case FETCH_TOPIC_LIST_FAIL: 27 | return initialState; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | export default topic; -------------------------------------------------------------------------------- /pm2.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'Nobibi', 5 | script: './server.js', 6 | cwd: './', // current workspace 7 | watch: [ 8 | // watch directorys and restart when they change 9 | '.next' 10 | ], 11 | ignore_watch: [ 12 | // ignore watch 13 | 'node_modules', 14 | 'logs', 15 | 'static' 16 | ], 17 | instances: 2, // start 2 instances 18 | node_args: '--harmony', 19 | env: { 20 | NODE_ENV: 'production', 21 | PORT: 3006 22 | }, 23 | out_file: './logs/out.log', // normal log 24 | error_file: './logs/err.log', // error log 25 | merge_logs: true, 26 | log_date_format: 'YYYY-MM-DD HH:mm Z' // date format 27 | } 28 | ] 29 | }; 30 | -------------------------------------------------------------------------------- /redux/reducers/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GET_USER_INFO, 3 | GET_USER_INFO_SUCCESS, 4 | USER_SIGN_OUT 5 | } from '../../constants/ActionTypes'; 6 | import { User } from '../../@types' 7 | 8 | 9 | const initialState = { 10 | _id: '', 11 | userName: '', 12 | avatar: '', 13 | email: '', 14 | visit: [], 15 | status: '', 16 | refUserRoleCode: '', 17 | createTime: '', 18 | updateTime: '' 19 | }; 20 | 21 | const user = (state: User = initialState, { type, payload = {} }) => { 22 | switch (type) { 23 | case GET_USER_INFO: 24 | return state; 25 | case GET_USER_INFO_SUCCESS: 26 | return { 27 | ...payload 28 | }; 29 | case USER_SIGN_OUT:{ 30 | return { 31 | ...initialState 32 | }; 33 | } 34 | default: 35 | return state; 36 | } 37 | 38 | }; 39 | 40 | export default user; -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | // _document is only rendered on the server side and not on the client side 2 | // Event handlers like onClick can't be added to this file 3 | 4 | // ./pages/_document.js 5 | import Document, { Head, Main, NextScript } from 'next/document'; 6 | 7 | export default class MyDocument extends Document { 8 | static async getInitialProps(ctx) { 9 | const initialProps = await Document.getInitialProps(ctx); 10 | return { ...initialProps }; 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | 17 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /components/NoAvatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import { UserOutlined } from '@ant-design/icons'; 3 | import { Avatar } from 'antd'; 4 | 5 | interface NoAvatarProps { 6 | avatar: string, 7 | userName: string 8 | size?: number | "small" | "large" | "default" 9 | } 10 | 11 | const NoAvatar: NextPage = (props) => { 12 | if (!props.avatar) { 13 | return ( 14 | } size={props.size} /> 15 | ); 16 | } 17 | if (props.avatar.length > 7) { 18 | return ( 19 | } src={props.avatar} size={props.size} /> 20 | ); 21 | } 22 | return ( 23 | {props.userName.slice(0, 1)} 32 | ); 33 | }; 34 | 35 | export default NoAvatar; -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 |

Nobibi

2 | 3 | [简体中文](./README.md) | English 4 | 5 | > Nobibi is a lightweight open source forum,Quickly build your own forum 6 | 7 | ## Quick Start 8 | 9 | > Make sure you have started [Nobibi-api](https://github.com/seawind8888/Nobibi-api) 10 | 11 | 1. Clone project code. 12 | 13 | ``` 14 | git clone https://github.com/seawind8888/Nobibi my-project 15 | ``` 16 | 17 | 2. Installation dependence. 18 | 19 | ``` 20 | cd my-porject 21 | npm install or yarn 22 | ``` 23 | 24 | 3. Start Project 25 | 26 | ``` 27 | npm run start 28 | ``` 29 | 30 | ## Associated project 31 | 32 | - [Nobibi-api](https://github.com/seawind8888/Nobibi-api) - Nobibi Api Part 33 | - [Nobibi-admin](https://github.com/seawind8888/Nobibi-admin) - Nobibi Admin 34 | 35 | ## Sreenshot 36 | 37 | - Frontend 38 | ![image](/screenshot/screenshot1.png) 39 | ![image](/screenshot/screenshot2.png) 40 | ![image](/screenshot/screenshot3.png) 41 | - Frontend(mobile) 42 | ![image](/screenshot/mobile.jpg) 43 | - Admin 44 | ![image](/screenshot/demo.gif) 45 | -------------------------------------------------------------------------------- /components/Editor/index.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import PropTypes from 'prop-types'; 3 | import {Form, Button, Input} from 'antd'; 4 | const { TextArea } = Input; 5 | 6 | interface EditorProps { 7 | onChange: (e: any) => void; 8 | onSubmit: (e: React.MouseEvent) => void; 9 | submitting: boolean, 10 | value: any 11 | } 12 | 13 | const Editor: NextPage = ({ onChange, onSubmit, submitting, value }) => ( 14 |
15 | 16 |