├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── assets │ └── icons │ │ ├── check2.svg │ │ ├── x.svg │ │ ├── clipboard.svg │ │ ├── trash.svg │ │ └── pencil-square.svg ├── manifest.json └── index.html ├── src ├── utils │ ├── uid.js │ ├── stringSlice.js │ └── cookie.js ├── setupTests.js ├── component │ ├── Popup.js │ ├── Profile.js │ ├── Toast.js │ └── ToastPanel.js ├── index.css ├── reportWebVitals.js ├── mainPage │ ├── MyPlan.css │ ├── index.js │ ├── MyConfig.js │ ├── PlanDetail │ │ ├── PlanDetailTokens.js │ │ ├── PlanDetailConfig.js │ │ ├── PlanDetailSharedConfig.js │ │ ├── SelectPanel.js │ │ ├── PlanDetailSharelinks.js │ │ └── index.js │ ├── FavorPlan.js │ ├── FavorConfig.js │ ├── MyPlan.js │ └── ConfigDetail │ │ └── index.js ├── overall.js ├── loginPage │ ├── index.js │ ├── LoginPanel.js │ └── RegisterPanel.js ├── index.js ├── logo.svg └── apilib │ └── apiReq.js ├── .gitignore ├── README.md └── package.json /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leafee98/class-schedule-to-icalendar-webfront/master/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leafee98/class-schedule-to-icalendar-webfront/master/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leafee98/class-schedule-to-icalendar-webfront/master/public/logo512.png -------------------------------------------------------------------------------- /src/utils/uid.js: -------------------------------------------------------------------------------- 1 | function genUid() { 2 | if (typeof genUid.__x == 'undefined') { 3 | genUid.__x = 0; 4 | } 5 | return ++genUid.__x; 6 | } 7 | 8 | export default genUid; 9 | -------------------------------------------------------------------------------- /src/utils/stringSlice.js: -------------------------------------------------------------------------------- 1 | function stringSlice(str, n) { 2 | if (str.length <= n) { 3 | return str; 4 | } 5 | 6 | return str.slice(0, n) + "..."; 7 | } 8 | 9 | export default stringSlice; 10 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /public/assets/icons/check2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/component/Popup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class PopupPanel extends React.Component { 4 | render() { 5 | return ( 6 |
7 |
8 | {this.props.children} 9 |
10 | ); 11 | } 12 | } 13 | 14 | export default PopupPanel; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/mainPage/MyPlan.css: -------------------------------------------------------------------------------- 1 | .myplan-table-col-id { 2 | width: 5%; 3 | } 4 | 5 | .myplan-table-col-name { 6 | width: 20%; 7 | } 8 | 9 | .myplan-table-col-remark { 10 | width: 30%; 11 | } 12 | 13 | .myplan-table-col-create-time { 14 | width: 15%; 15 | } 16 | 17 | .myplan-table-col-modify-time { 18 | width: 15%; 19 | } 20 | 21 | .myplan-table-col-operating { 22 | width: 10%; 23 | } 24 | 25 | .myplan-table-input { 26 | width: 100%; 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # vscode config files 26 | /.vscode 27 | -------------------------------------------------------------------------------- /public/assets/icons/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/icons/pencil-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Class Schedule To Icalendar Webfront 2 | 3 | This project is the web front of [Class Schedule To Icalendar Rest-server](https://github.com/leafee98/class-schedule-to-icalendar-restserver). 4 | 5 | ## Before Build 6 | 7 | Modify `overall.apiPath` to your configuration before build. Its path is `/src/overall.js`. 8 | 9 | ## Install 10 | 11 | Build the project. 12 | 13 | ``` 14 | npm run build 15 | ``` 16 | 17 | Then place it in a http server's document root or alias. Make sure it's in the same virtual host as the [Rest-server](https://github.com/leafee98/class-schedule-to-icalendar-restserver). 18 | 19 | ## Special Thanks 20 | 21 | - [React](https://github.com/facebook/react) --- License: [MIT](https://github.com/facebook/react/blob/master/LICENSE) 22 | - [Bootstrap](https://github.com/twbs/bootstrap) --- License: [MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE) 23 | - [PubSubJS](https://github.com/mroderick/PubSubJS) --- License: [MIT](https://github.com/mroderick/PubSubJS/blob/master/LICENSE.md) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "class-schedule-to-icalendar-webfront", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "/", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "bootstrap": "^5.0.0-beta3", 11 | "bootstrap-icons": "^1.4.1", 12 | "pubsub-js": "^1.9.3", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "react-scripts": "4.0.3", 16 | "web-vitals": "^1.0.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/cookie.js: -------------------------------------------------------------------------------- 1 | const cookie = { 2 | expireSec: 3600 * 24 * 7, 3 | __parseCookie: function (cookieStr) { 4 | const res = {} 5 | if (!cookieStr.endsWith(';')) { 6 | cookieStr += ';' 7 | } 8 | 9 | while (cookieStr.length > 1) { 10 | const ie = cookieStr.indexOf('=') 11 | const is = cookieStr.indexOf(';') 12 | 13 | const key = cookieStr.substring(0, ie) 14 | const value = cookieStr.substring(ie + 1, is) 15 | res[key.trim()] = value 16 | 17 | cookieStr = cookieStr.substring(is + 1) 18 | } 19 | return res 20 | }, 21 | get: function (name) { 22 | return this.__parseCookie(document.cookie)[name] 23 | }, 24 | set: function (name, value, expireSeconds) { 25 | let expireDate = null 26 | if (expireSeconds === undefined) { 27 | expireDate = new Date(Date.now() + 7 * 24 * 3600 * 1000) 28 | } else { 29 | expireDate = new Date(Date.now() + expireSeconds) 30 | } 31 | 32 | const tmpCookie = name + '=' + value + '; Expires=' + expireDate + '; SameSite=Strict; Path=/' 33 | document.cookie = tmpCookie 34 | } 35 | } 36 | 37 | export default cookie; 38 | -------------------------------------------------------------------------------- /src/overall.js: -------------------------------------------------------------------------------- 1 | var overall = { 2 | // modify this to your config before build 3 | apiPath: "/api", 4 | 5 | storageKey: { 6 | page: "page", 7 | username: "username", 8 | userId: "userId", 9 | mainPage: "mainPage" 10 | }, 11 | data: { 12 | switchPage: { 13 | loginPage: "switchPage.loginPage", 14 | mainPage: "switchPage.mainPage" 15 | }, 16 | loginPage: { 17 | switch: { 18 | login: "loginPanel.login", 19 | register: "loginPanel.register" 20 | } 21 | }, 22 | mainPage: { 23 | switch: { 24 | myPlan: "mainPage.myPlan", 25 | favorPlan: "mainPage.favorPlan", 26 | myConfig: "mainPage.myConfig", 27 | favorConfig: "mainPage.favorConfig", 28 | 29 | planDetail: "mainPage.planDetail", 30 | configDetail: "mainPage.configDetail" 31 | } 32 | } 33 | }, 34 | topics: { 35 | toast: "toast", 36 | switchPage: "switchPage", 37 | loginPage: { 38 | switch: "loginPage.switch" 39 | }, 40 | mainPage: { 41 | switch: "mainPage.switch", 42 | myPlan: { 43 | planDetail: { 44 | popupShow: "mainpage.myPlan.planDetil.popupShow", 45 | popupHide: "mainpage.myPlan.planDetil.popupHide", 46 | }, 47 | selectPopup: { 48 | show: "mainPage.myPlan.planDetail.selectPopup.show" 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | export default overall; 56 | -------------------------------------------------------------------------------- /src/component/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import apiReq from '../apilib/apiReq'; 5 | import overall from '../overall'; 6 | import cookie from '../utils/cookie'; 7 | 8 | class Profile extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.reqLogout = this.reqLogout.bind(this); 13 | } 14 | 15 | reqLogout() { 16 | // request reset server to revoke token 17 | apiReq.logout().finally( 18 | () => { 19 | // clear page history 20 | localStorage.clear(); 21 | // clear cookie even if network error 22 | cookie.set("token", "", 0); 23 | // switch to login page 24 | PubSub.publish(overall.topics.switchPage, overall.data.switchPage.loginPage); 25 | 26 | // logout can be done even offline totally, so never failed, just toast a success message. 27 | PubSub.publish(overall.topics.toast, { head: "logout", body: "logout success", fine: true }); 28 | } 29 | ); 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 | {/*
*/} 36 | {localStorage.getItem(overall.storageKey.username)} 37 | 42 |
43 | ); 44 | } 45 | } 46 | 47 | export default Profile; 48 | -------------------------------------------------------------------------------- /src/component/Toast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const bootstrap = require('bootstrap'); 4 | 5 | class Toast extends React.Component { 6 | // constructor(props) { 7 | // super(props); 8 | // } 9 | 10 | componentDidMount() { 11 | let e = document.getElementById(`toast-${this.props.uid}`) 12 | e.addEventListener('hidden.bs.toast', () => this.props.onHidden(this.props.uid)); 13 | 14 | let t = new bootstrap.Toast(e); 15 | t.show(); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | 42 |
43 | ) 44 | } 45 | } 46 | 47 | export default Toast; 48 | -------------------------------------------------------------------------------- /src/component/ToastPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | import overall from '../overall'; 4 | import genUid from '../utils/uid'; 5 | import Toast from './Toast'; 6 | 7 | class ToastPanel extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.addToast = this.addToast.bind(this); 12 | this.handleHidden = this.handleHidden.bind(this); 13 | 14 | this.toasts = []; 15 | } 16 | 17 | __subToken = 0; 18 | 19 | componentDidMount() { 20 | this.__subToken = PubSub.subscribe(overall.topics.toast, (msg, data) => { 21 | this.addToast(data.head, data.body, data.fine); 22 | }); 23 | } 24 | 25 | componentWillUnmount() { 26 | PubSub.unsubscribe(this.__subToken); 27 | } 28 | 29 | addToast(head, body, fine=true) { 30 | this.toasts.push({ 31 | head: head, 32 | body: body, 33 | fine: fine, 34 | key: genUid() 35 | }); 36 | this.forceUpdate(); 37 | } 38 | 39 | handleHidden(key) { 40 | let i = this.toasts.map(x => x.key).indexOf(key); 41 | if (i >= 0) { 42 | this.toasts.splice(i, 1); 43 | } 44 | } 45 | 46 | render() { 47 | let t = []; 48 | for (let x of this.toasts) { 49 | t.push( 50 | 52 | ); 53 | } 54 | 55 | return ( 56 |
57 | {t} 58 |
59 | ) 60 | } 61 | } 62 | 63 | export default ToastPanel; 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/loginPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | import LoginPanel from './LoginPanel'; 4 | import RegisterPanel from './RegisterPanel'; 5 | import overall from '../overall'; 6 | 7 | class LoginPage extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.changeStatus = this.changeStatus.bind(this); 12 | 13 | this.state = { 14 | status: 'l' // l: login, r: register 15 | } 16 | } 17 | 18 | __subToken = ""; 19 | 20 | componentDidMount() { 21 | this.__subToken = PubSub.subscribe(overall.topics.loginPage.switch, 22 | (msg, data) => { 23 | switch (data) { 24 | case overall.data.loginPage.switch.login: 25 | this.changeStatus('l'); 26 | break; 27 | case overall.data.loginPage.switch.register: 28 | this.changeStatus('r'); 29 | break; 30 | default: 31 | break; 32 | } 33 | }); 34 | } 35 | 36 | componentWillUnmount() { 37 | PubSub.unsubscribe(this.__subToken); 38 | } 39 | 40 | changeStatus(s) { 41 | this.setState({ 42 | status: s 43 | }); 44 | } 45 | 46 | render() { 47 | return ( 48 |
49 |
50 |
    51 |
  • 52 | 54 |
  • 55 |
  • 56 | 58 |
  • 59 |
60 |
61 | 62 |
63 | {this.state.status === 'l' ? : } 64 |
65 |
66 | ) 67 | } 68 | } 69 | 70 | export default LoginPage; 71 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PubSub from 'pubsub-js'; 4 | import 'bootstrap/dist/css/bootstrap.css'; 5 | 6 | import LoginPage from './loginPage'; 7 | import MainPage from './mainPage'; 8 | import ToastPanel from './component/ToastPanel'; 9 | import apiReq from './apilib/apiReq'; 10 | import overall from './overall'; 11 | 12 | apiReq.networkErrorHandler = () => { 13 | PubSub.publish(overall.topics.toast, 14 | { head: "error", body: "server side error or network error, try to relogin.", fine: false }); 15 | }; 16 | 17 | apiReq.apiPath = overall.apiPath; 18 | 19 | class Global extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.switchPage = this.switchPage.bind(this); 24 | 25 | let tmpPage = localStorage.getItem("page"); 26 | if (tmpPage == null) { 27 | tmpPage = overall.data.switchPage.loginPage; 28 | } 29 | 30 | this.state = { 31 | page: tmpPage 32 | }; 33 | } 34 | 35 | __subToken = ""; 36 | 37 | componentDidMount() { 38 | this.__subToken = PubSub.subscribe(overall.topics.switchPage, this.switchPage); 39 | } 40 | 41 | componentWillUnmount() { 42 | PubSub.unsubscribe(this.__subToken); 43 | } 44 | 45 | switchPage(topic, data) { 46 | localStorage.setItem(overall.storageKey.page, data); 47 | this.setState({page: data}); 48 | } 49 | 50 | render() { 51 | let page = null; 52 | switch (this.state.page) { 53 | case overall.data.switchPage.loginPage: 54 | page = 55 | break; 56 | case overall.data.switchPage.mainPage: 57 | page = 58 | break; 59 | 60 | default: 61 | break; 62 | } 63 | 64 | return ( 65 |
66 | 67 |
68 | {page} 69 |
70 | 71 | 72 |
73 | ) 74 | } 75 | } 76 | 77 | 78 | ReactDOM.render( 79 | 80 | 81 | , 82 | document.getElementById('root') 83 | ); 84 | -------------------------------------------------------------------------------- /src/loginPage/LoginPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | import apiReq from '../apilib/apiReq'; 4 | import overall from '../overall'; 5 | 6 | class LoginPanel extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.reqLogin = this.reqLogin.bind(this); 11 | this.handleReqLogin = this.handleReqLogin.bind(this); 12 | 13 | this.state = { 14 | username: "", 15 | password: "", 16 | }; 17 | } 18 | 19 | reqLogin(event) { 20 | event.preventDefault(); 21 | apiReq.login( 22 | { 23 | username: this.state.username, 24 | password: this.state.password, 25 | tokenDuration: 7, 26 | }, 27 | this.handleReqLogin 28 | ) 29 | } 30 | 31 | handleReqLogin(j) { 32 | if (j["status"] === "ok") { 33 | PubSub.publish(overall.topics.toast, { head: "login", body: "login success", fine: true }); 34 | PubSub.publish(overall.topics.switchPage, overall.data.switchPage.mainPage); 35 | 36 | localStorage.setItem(overall.storageKey.username, this.state.username); 37 | localStorage.setItem(overall.storageKey.userId, j.id); 38 | } else { 39 | PubSub.publish(overall.topics.toast, { head: "login", body: j.data, fine: false }); 40 | } 41 | } 42 | 43 | handleChangle(p, v) { 44 | this.setState({[p]: v}); 45 | } 46 | 47 | render() { 48 | return ( 49 |
50 |
51 |
52 | 53 | this.handleChangle("username", e.target.value)}/> 56 |
57 |
58 | 59 | this.handleChangle("password", e.target.value)}/> 62 |
63 | 64 |
65 | 67 |
68 |
69 |
70 | ) 71 | } 72 | 73 | } 74 | 75 | export default LoginPanel; 76 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/loginPage/RegisterPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PubSub from 'pubsub-js'; 3 | import apiReq from '../apilib/apiReq'; 4 | import overall from '../overall'; 5 | 6 | class RegisterPanel extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.handleChange = this.handleChange.bind(this); 11 | this.reqRegister = this.reqRegister.bind(this); 12 | 13 | this.state = { 14 | email: "", 15 | username: "", 16 | nickname: "", 17 | password: "" 18 | }; 19 | } 20 | 21 | handleChange(key, value) { 22 | this.setState({ 23 | [key]: value 24 | }); 25 | } 26 | 27 | reqRegister(e) { 28 | e.preventDefault(); 29 | apiReq.register( 30 | { 31 | email: this.state.email, 32 | username: this.state.username, 33 | password: this.state.password, 34 | nickname: this.state.nickname 35 | }, 36 | (j) => { 37 | if (j.status === "ok") { 38 | PubSub.publish(overall.topics.toast, { head: "register", body: "register success", fine: true }) 39 | PubSub.publish(overall.topics.loginPage.switch, overall.data.loginPage.switch.login); 40 | } else { 41 | PubSub.publish(overall.topics.toast, { head: "register", body: j.data, fine: false }) 42 | } 43 | } 44 | ) 45 | } 46 | 47 | render() { 48 | return ( 49 |
50 |
51 |
52 | 53 | this.handleChange("email", e.target.value)} /> 55 |
56 |
57 | 58 | this.handleChange("username", e.target.value)} /> 60 |
61 |
62 | 63 | this.handleChange("nickname", e.target.value)} /> 65 |
66 |
67 | 68 | this.handleChange("password", e.target.value)} /> 70 |
71 | 72 |
73 | 74 |
75 |
76 |
77 | ) 78 | } 79 | } 80 | 81 | export default RegisterPanel; 82 | -------------------------------------------------------------------------------- /src/mainPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Profile from '../component/Profile'; 3 | 4 | import MyPlan from './MyPlan'; 5 | import MyConfig from './MyConfig'; 6 | import FavorConfig from './FavorConfig'; 7 | import FavorPlan from './FavorPlan'; 8 | 9 | import cookie from '../utils/cookie'; 10 | import overall from '../overall'; 11 | 12 | class MainPage extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.subPages = { 17 | myPlan: "myPlan", 18 | favorPlan: "favorPlan", 19 | myConfig: "myConfig", 20 | favorConfig: "favorConfig" 21 | } 22 | 23 | this.state = { 24 | subPage: this.subPages.myPlan 25 | } 26 | } 27 | 28 | switchSubPage(e, p) { 29 | e.preventDefault(); 30 | this.setState({ subPage: p }); 31 | } 32 | 33 | render() { 34 | let subPage = null; 35 | let subTitle = null; 36 | 37 | switch (this.state.subPage) { 38 | case this.subPages.myPlan: 39 | subPage = 40 | subTitle = "My Plan"; 41 | break; 42 | 43 | case this.subPages.favorPlan: 44 | subPage = ; 45 | subTitle = "Favor Plan"; 46 | break; 47 | 48 | case this.subPages.myConfig: 49 | subPage = 50 | subTitle = "My Config"; 51 | break; 52 | 53 | case this.subPages.favorConfig: 54 | subPage = 55 | subTitle = "Favor Config"; 56 | break; 57 | 58 | default: 59 | break; 60 | } 61 | 62 | let profile = null; 63 | if (cookie.get("token") != null || localStorage.getItem(overall.storageKey.userId) !== null) 64 | profile = ; 65 | 66 | return ( 67 |
68 | 69 |
70 | 99 |
100 | 101 |
102 | {subPage} 103 |
104 | 105 |
106 | ); 107 | } 108 | } 109 | 110 | export default MainPage; 111 | -------------------------------------------------------------------------------- /src/mainPage/MyConfig.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import ConfigDetail from './ConfigDetail'; 5 | 6 | import overall from '../overall'; 7 | import apiReq from '../apilib/apiReq'; 8 | import stringSlice from '../utils/stringSlice'; 9 | 10 | class MyConfig extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | newConfig: false, 15 | offset: 0, 16 | detailId: -1, 17 | configs: [] 18 | } 19 | } 20 | 21 | componentDidMount() { 22 | this.reqConfigGetList(this.state.offset); 23 | } 24 | 25 | reqConfigGetList(offset) { 26 | apiReq.configGetList( 27 | { sortBy: "id", offset: offset, count: 30 }, 28 | (j) => { 29 | if (j.status === "ok") { 30 | this.setState({ configs: j.data.configs }); 31 | } else { 32 | PubSub.publish(overall.topics.toast, { head: "my config", body: "failed to get config: " + j.data, fine: false }); 33 | } 34 | } 35 | ) 36 | } 37 | 38 | reqConfigRemove(configId) { 39 | apiReq.configRemove( 40 | {id: configId}, 41 | (j) => { 42 | if (j.status === "ok") { 43 | PubSub.publish(overall.topics.toast, { head: "my config", body: "success to remove config: " + configId, fine: true }); 44 | } else { 45 | PubSub.publish(overall.topics.toast, { head: "my config", body: "failed to remove config: " + j.data, fine: false }); 46 | } 47 | 48 | this.reqConfigGetList(); 49 | } 50 | ); 51 | } 52 | 53 | render() { 54 | let rows = []; 55 | 56 | for (let x of this.state.configs) { 57 | let id = x.id; 58 | 59 | let row = ( 60 | this.setState({detailId: id})}> 62 | {id} 63 | 64 | {x.type === 1 && "Global Config"} 65 | {x.type === 1 || "Lesson Config"} 66 | 67 | {stringSlice(x.name, 24)} 68 | {stringSlice(x.remark, 24)} 69 | {x.createTime} 70 | {x.modifyTime} 71 | 72 | 75 | 76 | 77 | ) 78 | 79 | rows.push(row); 80 | } 81 | 82 | if (rows.length <= 0) { 83 | rows.push( 84 | 85 | 86 | Got no config. 87 | 88 | 89 | ) 90 | } 91 | 92 | return ( 93 |
94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {rows} 110 | 111 |
98 | Config Id 99 | TypeNameRemarkCreate TimeModify TimeOperating
112 |
113 | 117 |
118 | 119 | {this.state.detailId === -1 || 120 | this.reqConfigGetList(this.state.offset)} 122 | onHidden={() => this.setState({detailId: -1})} 123 | newConfig={false} />} 124 | {this.state.newConfig && 125 | this.reqConfigGetList(this.state.offset)} 127 | onHidden={() => this.setState({newConfig: false})} 128 | newConfig={true} />} 129 |
130 | ) 131 | } 132 | } 133 | 134 | export default MyConfig; 135 | -------------------------------------------------------------------------------- /src/mainPage/PlanDetail/PlanDetailTokens.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | import apiReq from '../../apilib/apiReq'; 4 | import overall from '../../overall'; 5 | 6 | class PlanDetailTokens extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.reqAddToken = this.reqAddToken.bind(this); 11 | this.reqPlanGetTokenList = this.reqPlanGetTokenList.bind(this); 12 | this.copyGenerateUrl = this.copyGenerateUrl.bind(this); 13 | 14 | // this.props.planId; 15 | this.state = { 16 | tokens: [] 17 | } 18 | } 19 | 20 | componentDidMount() { 21 | this.reqPlanGetTokenList(); 22 | } 23 | 24 | reqPlanGetTokenList() { 25 | apiReq.planGetTokenList( 26 | {id: this.props.planId}, 27 | (j) => { 28 | if (j.status === "ok") { 29 | this.setState({ tokens: j.data.tokens }) 30 | } else { 31 | PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "failed to get tokens: " + j.data, fine: false }); 32 | } 33 | } 34 | ) 35 | } 36 | 37 | reqAddToken() { 38 | apiReq.planCreateToken( 39 | { id: this.props.planId }, 40 | (j) => { 41 | if (j.status === "ok") { 42 | PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "success to add token.", fine: true }); 43 | } else { 44 | PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "failed to create token: " + j.data, fine: false }); 45 | } 46 | 47 | this.reqPlanGetTokenList(); 48 | } 49 | ) 50 | } 51 | 52 | reqPlanRevokeToken(token) { 53 | apiReq.planRevokeToken( 54 | { token: token }, 55 | (j) => { 56 | if (j.status === "ok") { 57 | PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "success to revoke token.", fine: true }); 58 | } else { 59 | PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "failed to revoke token: " + j.data, fine: false }); 60 | } 61 | 62 | this.reqPlanGetTokenList(); 63 | } 64 | ) 65 | } 66 | 67 | copyGenerateUrl(token) { 68 | // this only work in HTTPS 69 | const url = window.location.protocol + "//" + window.location.host + overall.apiPath + "/generate-by-plan-token?token=" + token; 70 | if (navigator.clipboard !== undefined) { 71 | navigator.clipboard.writeText(url).then( 72 | () => { PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "success to copy generate url", fine: true }); }, 73 | (e) => { PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "failed to copy generate url: " + e, fine: false }); }, 74 | ) 75 | } else { 76 | PubSub.publish(overall.topics.toast, { head: "plan tokens", body: "the copy action is only available in https environment.", fine: false }); 77 | } 78 | } 79 | 80 | render() { 81 | let rows = []; 82 | for (let x of this.state.tokens) { 83 | let tmp = ( 84 | 85 | {x.token} 86 | {x.createTime} 87 | 88 | 91 | 94 | 95 | 96 | ); 97 | rows.push(tmp); 98 | } 99 | 100 | if (rows.length === 0) { 101 | rows.push( 102 | 103 | 104 | No tokens now. 105 | 106 | 107 | ); 108 | } 109 | 110 | return ( 111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | {rows} 123 | 124 |
TokenCreate TimeOperating
125 | 126 |
127 | 130 |
131 | 132 |
133 | ) 134 | } 135 | } 136 | 137 | export default PlanDetailTokens; -------------------------------------------------------------------------------- /src/mainPage/PlanDetail/PlanDetailConfig.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import apiReq from '../../apilib/apiReq'; 5 | import overall from '../../overall'; 6 | import stringSlice from '../../utils/stringSlice'; 7 | 8 | class PlanDetailConfig extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | // this.props.planId; 13 | // this.props.configs; 14 | // this.props.shared 15 | // this.props.onModify; 16 | 17 | this.reqAddConfig = this.reqAddConfig.bind(this); 18 | this.reqPlanRemoveConfig = this.reqPlanRemoveConfig.bind(this); 19 | this.btnAddConfig = this.btnAddConfig.bind(this); 20 | this.planAddConfig = this.planAddConfig.bind(this); 21 | } 22 | 23 | componentDidMount() { 24 | // let eles = document.querySelectorAll(".to-show-tooltip"); 25 | // for (let x of eles) { 26 | // new bootstrap.Tooltip(x); 27 | // } 28 | } 29 | 30 | reqAddConfig(configId) { 31 | apiReq.planAddConfig( 32 | { planId: this.props.planId, configId: configId }, 33 | (j) => { 34 | if (j.status === "ok") { 35 | PubSub.publish(overall.topics.toast, {head: "plan detail", body: "success to add config to plan", fine: true}); 36 | } else { 37 | PubSub.publish(overall.topics.toast, {head: "plan detail", body: "failed to add config: " + j.data, fine: false}); 38 | } 39 | 40 | this.props.onModify(); 41 | } 42 | ) 43 | } 44 | 45 | reqPlanRemoveConfig(planId, configId) { 46 | apiReq.planRemoveConfig( 47 | { planId: planId, configId: configId }, 48 | (j) => { 49 | if (j.status === "ok") { 50 | PubSub.publish(overall.topics.toast, { head: "plan detail", body: "removed config: " + configId, fine: true }); 51 | } else { 52 | PubSub.publish(overall.topics.toast, { head: "plan detail", body: "failed to remove config: " + j.data, fine: false }); 53 | } 54 | 55 | this.props.onModify(); 56 | } 57 | ); 58 | } 59 | 60 | btnAddConfig() { 61 | PubSub.publishSync(overall.topics.mainPage.myPlan.planDetail.popupHide); 62 | PubSub.publish(overall.topics.mainPage.myPlan.selectPopup.show, { select: "config", onSelect: this.planAddConfig }); 63 | } 64 | 65 | planAddConfig(configId) { 66 | PubSub.publish(overall.topics.mainPage.myPlan.planDetail.popupShow); 67 | if (configId >= 0) { 68 | this.reqAddConfig(configId); 69 | } 70 | } 71 | 72 | render() { 73 | let configs = []; 74 | for (let x of this.props.configs) { 75 | let tmp = 76 | 77 | {x.id} 78 | {x.type === 1 ? "global" : "lesson"} 79 | {stringSlice(x.name, 24)} 80 | {stringSlice(x.content, 24)} 81 | {stringSlice(x.remark, 24)} 82 | {x.createTime} 83 | {x.modifyTime} 84 | 85 | 88 | 89 | 90 | configs.push(tmp); 91 | } 92 | 93 | if (configs.length === 0) { 94 | configs.push( 95 | 96 | 97 | There's no content now. 98 | 99 | 100 | ) 101 | } 102 | 103 | return ( 104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | {configs} 121 | 122 | 123 |
idtypenamecontentremarkcreateTimemodifyTimeoperating
124 |
125 | 129 |
130 |
131 | ) 132 | } 133 | } 134 | 135 | export default PlanDetailConfig; 136 | -------------------------------------------------------------------------------- /src/mainPage/FavorPlan.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import PlanDetail from './PlanDetail'; 5 | 6 | import apiReq from '../apilib/apiReq'; 7 | import overall from '../overall'; 8 | import stringSlice from '../utils/stringSlice'; 9 | 10 | class FavorPlan extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.reqFavorPlanAdd = this.reqFavorPlanAdd.bind(this); 15 | this.reqFavorPlanRemove = this.reqFavorPlanRemove.bind(this); 16 | this.reqFavorPlanGetList = this.reqFavorPlanGetList.bind(this); 17 | this.btnAddShare = this.btnAddShare.bind(this); 18 | 19 | this.state = { 20 | detailId: -1, 21 | directAddShareId: "", 22 | offset: 0, 23 | shares: [] 24 | } 25 | } 26 | 27 | componentDidMount() { 28 | this.reqFavorPlanGetList(); 29 | } 30 | 31 | reqFavorPlanGetList() { 32 | apiReq.favorPlanGetList( 33 | { offset: this.state.offset, count: 30 }, 34 | (j) => { 35 | if (j.status === "ok") { 36 | this.setState({ shares: j.data.plans }); 37 | } else { 38 | PubSub.publish(overall.topics.toast, { head: "favor plan", body: "failed to get favor plans: " + j.data, fine: false }) 39 | } 40 | } 41 | ) 42 | } 43 | 44 | reqFavorPlanAdd(shareId) { 45 | apiReq.favorPlanAdd( 46 | { id: shareId }, 47 | (j) => { 48 | if (j.status === "ok") { 49 | PubSub.publish(overall.topics.toast, { head: "favor plan", body: "success to add favor plan.", fine: true }); 50 | this.reqFavorPlanGetList(); 51 | } else { 52 | PubSub.publish(overall.topics.toast, { head: "favor plan", body: "failed to add favor plan: " + j.data, fine: false }); 53 | } 54 | } 55 | ) 56 | } 57 | 58 | reqFavorPlanRemove(shareId) { 59 | apiReq.favorPlanRemove( 60 | { id: shareId }, 61 | (j) => { 62 | if (j.status === "ok") { 63 | PubSub.publish(overall.topics.toast, { head: "favor plan", body: "success to remove favor plan.", fine: true }); 64 | this.reqFavorPlanGetList(); 65 | } else { 66 | PubSub.publish(overall.topics.toast, { head: "favor plan", body: "failed to remove favor plan: " + j.data, fine: false }); 67 | } 68 | } 69 | ) 70 | } 71 | 72 | btnAddShare() { 73 | let shareId = parseInt(this.state.directAddShareId); 74 | if (isNaN(shareId)) { 75 | PubSub.publish(overall.topics.toast, { head: "favor plan", body: this.state.directAddShareId + " is not a valid number.", fine: false }); 76 | return; 77 | } 78 | 79 | this.reqFavorPlanAdd(shareId); 80 | } 81 | 82 | render() { 83 | let rows = []; 84 | 85 | for (let x of this.state.shares) { 86 | let id = x.shareId; 87 | 88 | let row = ( 89 | this.setState({ detailId: id })}> 90 | {id} 91 | 92 | {x.type === 1 && "Global"} 93 | {x.type === 1 || "Lesson"} 94 | 95 | {stringSlice(x.name, 24)} 96 | {stringSlice(x.remark, 24)} 97 | {x.createTime} 98 | {x.modifyTime} 99 | 100 | 103 | 104 | 105 | ) 106 | 107 | rows.push(row); 108 | } 109 | 110 | if (rows.length <= 0) { 111 | rows.push( 112 | 113 | 114 | Got no config. 115 | 116 | 117 | ) 118 | } 119 | return ( 120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | {rows} 136 | 137 |
Share IdTypeNameRemarkCreate TimeModify TimeOperating
138 | 139 |
140 |
141 | 145 | this.setState({ directAddShareId: e.target.value })} /> 148 |
149 |
150 | 151 | {this.state.detailId === -1 || 152 | this.setState({ detailId: -1 })} />} 156 |
157 | ) 158 | } 159 | } 160 | 161 | export default FavorPlan; 162 | -------------------------------------------------------------------------------- /src/mainPage/FavorConfig.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import apiReq from '../apilib/apiReq'; 5 | import overall from '../overall'; 6 | import stringSlice from '../utils/stringSlice'; 7 | import ConfigDetail from './ConfigDetail'; 8 | 9 | class FavorConfig extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.reqFavorConfigAdd = this.reqFavorConfigAdd.bind(this); 14 | this.reqFavorConfigRemove = this.reqFavorConfigRemove.bind(this); 15 | this.reqFavorConfigGetList = this.reqFavorConfigGetList.bind(this); 16 | this.btnAddShare = this.btnAddShare.bind(this); 17 | 18 | this.state = { 19 | detailId: -1, 20 | directAddShareId: "", 21 | offset: 0, 22 | shares: [] 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | this.reqFavorConfigGetList(); 28 | } 29 | 30 | reqFavorConfigGetList() { 31 | apiReq.favorConfigGetList( 32 | { offset: this.state.offset, count: 30 }, 33 | (j) => { 34 | if (j.status === "ok") { 35 | this.setState({ shares: j.data.configs }); 36 | } else { 37 | PubSub.publish(overall.topics.toast, { head: "favor config", body: "failed to get favor configs: " + j.data, fine: false }) 38 | } 39 | } 40 | ) 41 | } 42 | 43 | reqFavorConfigAdd(shareId) { 44 | apiReq.favorConfigAdd( 45 | { id: shareId }, 46 | (j) => { 47 | if (j.status === "ok") { 48 | PubSub.publish(overall.topics.toast, { head: "favor config", body: "success to add favor config.", fine: true }); 49 | this.reqFavorConfigGetList(); 50 | } else { 51 | PubSub.publish(overall.topics.toast, { head: "favor config", body: "failed to add favor config: " + j.data, fine: false }); 52 | } 53 | } 54 | ) 55 | } 56 | 57 | reqFavorConfigRemove(shareId) { 58 | apiReq.favorConfigRemove( 59 | { id: shareId }, 60 | (j) => { 61 | if (j.status === "ok") { 62 | PubSub.publish(overall.topics.toast, { head: "favor config", body: "success to remove favor config.", fine: true }); 63 | this.reqFavorConfigGetList(); 64 | } else { 65 | PubSub.publish(overall.topics.toast, { head: "favor config", body: "failed to remove favor config: " + j.data, fine: false }); 66 | } 67 | } 68 | ) 69 | } 70 | 71 | btnAddShare() { 72 | let shareId = parseInt(this.state.directAddShareId); 73 | if (isNaN(shareId)) { 74 | PubSub.publish(overall.topics.toast, { head: "favor config", body: this.state.directAddShareId + " is not a valid number.", fine: false }); 75 | return; 76 | } 77 | 78 | this.reqFavorConfigAdd(shareId); 79 | } 80 | 81 | render() { 82 | let rows = []; 83 | 84 | for (let x of this.state.shares) { 85 | let id = x.shareId; 86 | 87 | let row = ( 88 | this.setState({ detailId: id })}> 89 | {id} 90 | 91 | {x.type === 1 && "Global"} 92 | {x.type === 1 || "Lesson"} 93 | 94 | {stringSlice(x.name, 24)} 95 | {stringSlice(x.remark, 24)} 96 | {x.createTime} 97 | {x.modifyTime} 98 | 99 | 102 | 103 | 104 | ) 105 | 106 | rows.push(row); 107 | } 108 | 109 | if (rows.length <= 0) { 110 | rows.push( 111 | 112 | 113 | Got no config. 114 | 115 | 116 | ) 117 | } 118 | return ( 119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | {rows} 135 | 136 |
Share IdTypeNameRemarkCreate TimeModify TimeOperating
137 | 138 |
139 |
140 | 144 | this.setState({ directAddShareId: e.target.value })} /> 147 |
148 |
149 | 150 | {this.state.detailId === -1 || 151 | this.setState({ detailId: -1 })} 155 | newConfig={false} 156 | shared={true} />} 157 |
158 | ) 159 | } 160 | } 161 | 162 | export default FavorConfig; 163 | -------------------------------------------------------------------------------- /src/mainPage/PlanDetail/PlanDetailSharedConfig.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import apiReq from '../../apilib/apiReq'; 5 | import overall from '../../overall'; 6 | import stringSlice from '../../utils/stringSlice'; 7 | 8 | class PlanDetailSharedConfig extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.reqPlanAddShare = this.reqPlanAddShare.bind(this); 13 | this.reqPlanRemoveShare = this.reqPlanRemoveShare.bind(this); 14 | this.btnAddShare = this.btnAddShare.bind(this); 15 | this.btnDirectAddShare = this.btnDirectAddShare.bind(this); 16 | 17 | // this.props.planId; 18 | // this.props.shares; 19 | // this.props.shared; 20 | // this.props.onModify; 21 | this.state = { 22 | directAddShareId: "" 23 | } 24 | } 25 | 26 | reqPlanRemoveShare(shareId) { 27 | apiReq.planRemoveShare( 28 | { planId: this.props.planId, configShareId: shareId }, 29 | (j) => { 30 | if (j.status === "ok") { 31 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "success to remove share", fine: true }); 32 | this.props.onModify(); 33 | } else { 34 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "failed to remove share: " + j.data, fine: false }); 35 | } 36 | } 37 | ) 38 | } 39 | 40 | reqPlanAddShare(shareId, showPopup=true) { 41 | if (showPopup) 42 | PubSub.publishSync(overall.topics.mainPage.myPlan.planDetail.popupShow); 43 | 44 | if (shareId >= 0) { 45 | apiReq.planAddShare( 46 | { planId: this.props.planId, configShareId: shareId }, 47 | (j) => { 48 | if (j.status === "ok") { 49 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "success to add share", fine: true }); 50 | this.props.onModify(); 51 | } else { 52 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "failed to add share: " + j.data, fine: false }); 53 | } 54 | } 55 | ) 56 | } 57 | } 58 | 59 | btnAddShare() { 60 | PubSub.publishSync(overall.topics.mainPage.myPlan.planDetail.popupHide); 61 | PubSub.publish(overall.topics.mainPage.myPlan.selectPopup.show, { select: "favorConfig", onSelect: this.reqPlanAddShare }) 62 | } 63 | 64 | btnDirectAddShare() { 65 | let shareId = parseInt(this.state.directAddShareId); 66 | if (isNaN(shareId)) { 67 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: this.state.directAddShareId + " is not a valid number.", fine: false }); 68 | return; 69 | } 70 | 71 | this.reqPlanAddShare(shareId, false); 72 | } 73 | 74 | render() { 75 | let shares = []; 76 | for (let x of this.props.shares) { 77 | let tmp = 78 | 79 | {x.id} 80 | {x.type === 1 && "global"}{x.type === 1 || "lesson"} 81 | {stringSlice(x.name, 24)} 82 | {stringSlice(x.content, 24)} 83 | {stringSlice(x.remark, 24)} 84 | {x.createTime} 85 | {x.modifyTime} 86 | 87 | 90 | 91 | 92 | shares.push(tmp); 93 | } 94 | 95 | if (shares.length === 0) { 96 | shares.push( 97 | 98 | 99 | There's no content now. 100 | 101 | 102 | ) 103 | } 104 | 105 | return ( 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | {shares} 123 | 124 | 125 |
Share IdTypeNameContentRemarkCreateTimeModifyTimeOperating
126 |
127 |
128 |
129 | 133 |
134 |
135 |
136 | 140 | this.setState({ directAddShareId: e.target.value })} /> 144 |
145 |
146 |
147 |
148 |
149 | ) 150 | } 151 | } 152 | 153 | export default PlanDetailSharedConfig; 154 | -------------------------------------------------------------------------------- /src/mainPage/PlanDetail/SelectPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import apiReq from '../../apilib/apiReq'; 5 | import overall from '../../overall'; 6 | import stringSlice from '../../utils/stringSlice'; 7 | 8 | const bootstrap = require('bootstrap'); 9 | 10 | class SelectPanel extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.reqItems = this.reqItems.bind(this); 15 | this.select = this.select.bind(this); 16 | 17 | // this.props.select == 'config' || 'favorConfig'; 18 | // this.props.onSelect == function(configId_or_configShareId); 19 | 20 | this.state = { 21 | param: { 22 | select: "", 23 | onSelect: "", 24 | }, 25 | activeId: -1, 26 | offset: 0, 27 | items: [] 28 | } 29 | } 30 | 31 | __subToken = ""; 32 | 33 | componentDidMount() { 34 | let ele = document.getElementById("select-panel-popup"); 35 | this.popup = new bootstrap.Modal(ele, {backdrop: false, keyboard: false}); 36 | 37 | this.__subToken = PubSub.subscribe(overall.topics.mainPage.myPlan.selectPopup.show, (topic, param) => { 38 | this.setState({ param: param }); 39 | this.popup.show(); 40 | this.reqItems(); 41 | 42 | console.log(ele); 43 | }) 44 | } 45 | 46 | componentWillUnmount() { 47 | PubSub.unsubscribe(this.__subToken); 48 | } 49 | 50 | reqItems(offset = 0) { 51 | switch (this.state.param.select) { 52 | case "config": 53 | apiReq.configGetList( 54 | { sortBy: "id", offset: offset, count: 30 }, 55 | (j) => { 56 | if (j.status === "ok") { 57 | this.setState({ items: j.data.configs }); 58 | } else { 59 | PubSub.publish(overall.topics.toast, { head: "select panel", body: "failed to get config: " + j.data, fine: false }); 60 | } 61 | } 62 | ) 63 | break; 64 | 65 | case "favorConfig": 66 | apiReq.favorConfigGetList( 67 | { offset: offset, count: 30 }, 68 | (j) => { 69 | if (j.status === "ok") { 70 | this.setState({ items: j.data.configs }); 71 | } else { 72 | PubSub.publish(overall.topics.toast, { head: "select panel", body: "failed to get favor configs: " + j.data, fine: false }) 73 | } 74 | } 75 | ) 76 | break; 77 | 78 | default: 79 | break; 80 | } 81 | } 82 | 83 | select(submit) { 84 | if (submit) { 85 | if (this.state.activeId < 0) { 86 | PubSub.publish(overall.topics.toast, { head: "select panel", body: "please select an item before submit", fine: false }); 87 | } else { 88 | this.popup.hide(); 89 | this.state.param.onSelect(this.state.activeId); 90 | } 91 | } else { 92 | this.popup.hide(); 93 | this.state.param.onSelect(-1); 94 | } 95 | } 96 | 97 | render() { 98 | let rows = []; 99 | 100 | for (let x of this.state.items) { 101 | let id = -1; 102 | switch (this.state.param.select) { 103 | case "config": 104 | id = x.id; 105 | break; 106 | case "favorConfig": 107 | id = x.shareId; 108 | break; 109 | default: 110 | break; 111 | } 112 | 113 | let row = ( 114 | { this.setState((state, props) => { return { activeId: id === state.activeId ? -1 : id } }) }}> 117 | {id} 118 | 119 | {x.type === 1 && "Global"} 120 | {x.type === 1 || "Lesson"} 121 | 122 | {stringSlice(x.name, 24)} 123 | {stringSlice(x.remark, 24)} 124 | {x.createTime} 125 | {x.modifyTime} 126 | 127 | ) 128 | 129 | rows.push(row); 130 | } 131 | 132 | if (rows.length <= 0) { 133 | rows.push( 134 | 135 | 136 | Got no config. 137 | 138 | 139 | ) 140 | } 141 | 142 | return ( 143 |
144 |
145 |
146 |
147 | {this.state.param.select === "config" && "Select From My Config"} 148 | {this.state.param.select === "favorConfig" && "Select From My Favor Config"} 149 |
150 |
151 | 152 | 153 | 154 | 155 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | {rows} 168 | 169 |
156 | {this.state.param.select === "config" && "Config Id"} 157 | {this.state.param.select === "favorConfig" && "Share Id"} 158 | TypeNameRemarkCreate TimeModify Time
170 | 171 |
172 | 173 | 174 |
175 | 176 |
177 |
178 |
179 |
180 | 181 | ); 182 | } 183 | } 184 | 185 | export default SelectPanel; 186 | -------------------------------------------------------------------------------- /src/mainPage/PlanDetail/PlanDetailSharelinks.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | import apiReq from '../../apilib/apiReq'; 5 | import overall from '../../overall'; 6 | 7 | class PlanDetailSharelinks extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | // this.props.planId; 12 | 13 | this.reqPlanShareGetList = this.reqPlanShareGetList.bind(this); 14 | this.reqPlanShareModify = this.reqPlanShareModify.bind(this); 15 | this.reqPlanShareRevoke = this.reqPlanShareRevoke.bind(this); 16 | this.reqPlanShareCreate = this.reqPlanShareCreate.bind(this); 17 | 18 | this.state = { 19 | newShare: { 20 | exist: false, 21 | remark: "" 22 | }, 23 | editShare: { 24 | id: -1, 25 | remark: "" 26 | }, 27 | shares: [] 28 | }; 29 | } 30 | 31 | componentDidMount() { 32 | this.reqPlanShareGetList(); 33 | } 34 | 35 | reqPlanShareGetList() { 36 | apiReq.planShareGetList( 37 | { id: this.props.planId }, 38 | (j) => { 39 | if (j.status === "ok") { 40 | this.setState({ shares: j.data.shares }); 41 | } else { 42 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "failed to get config share links:" + j.data, fine: false }); 43 | } 44 | } 45 | ) 46 | } 47 | 48 | reqPlanShareModify() { 49 | apiReq.planShareModify( 50 | { id: this.state.editShare.id, remark: this.state.editShare.remark}, 51 | (j) => { 52 | if (j.status === "ok") { 53 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "success to modify plan share links.", fine: true }); 54 | this.setState({ editShare: { id: -1, remark: "" } }); 55 | 56 | this.reqPlanShareGetList(this.props.planId); 57 | } else { 58 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "failed to modify plan share links:" + j.data, fine: false }); 59 | } 60 | } 61 | ); 62 | } 63 | 64 | reqPlanShareRevoke(shareId) { 65 | apiReq.planShareRevoke( 66 | { id: shareId }, 67 | (j) => { 68 | if (j.status === "ok") { 69 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "success to remove share links.", fine: true }); 70 | 71 | this.reqPlanShareGetList(this.props.planId); 72 | } else { 73 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "failed to remove share links:" + j.data, fine: false }); 74 | } 75 | } 76 | ); 77 | } 78 | 79 | reqPlanShareCreate(remark) { 80 | apiReq.planShareCreate( 81 | { id: this.props.planId, remark: remark }, 82 | (j) => { 83 | if (j.status === "ok") { 84 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "success to create share links.", fine: true }); 85 | 86 | this.reqPlanShareGetList(this.props.planId); 87 | } else { 88 | PubSub.publish(overall.topics.toast, { head: "plan detail share", body: "failed to create share links:" + j.data, fine: false }); 89 | } 90 | } 91 | ); 92 | } 93 | 94 | render() { 95 | let rows = []; 96 | for (let x of this.state.shares) { 97 | let tmp = ( 98 | 99 | {x.id} 100 | 101 | {x.id === this.state.editShare.id && 102 | 103 |