├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── bundle.js.map ├── dashboard.png ├── favicon.ico ├── index.html ├── package.json ├── src ├── actions │ ├── auth_actions.js │ ├── crud_actions.js │ ├── dashboard_actions.js │ ├── message_actions.js │ ├── page_actions.js │ ├── popup_actions.js │ ├── tabbar_actions.js │ ├── todo_actions.js │ └── user_actions.js ├── components │ ├── app.js │ ├── auth │ │ ├── require_auth.js │ │ ├── signin.js │ │ ├── signout.js │ │ └── signup.js │ ├── charts │ │ └── line_chart.js │ ├── dashboard │ │ ├── dashboard.js │ │ ├── dashboard_form.js │ │ ├── dashboard_manage.js │ │ ├── search_tool.js │ │ ├── widget.js │ │ ├── widget_form.js │ │ └── widget_tool.js │ ├── feature.js │ ├── footer.js │ ├── grid │ │ └── ag_grid.js │ ├── header.js │ ├── main.js │ ├── popup.js │ ├── popups │ │ └── popup_yesno.js │ ├── tabbar │ │ ├── tab.js │ │ └── tabbar.js │ ├── todo │ │ ├── todo.js │ │ ├── todo_view.js │ │ └── todos.js │ ├── user │ │ ├── user.js │ │ └── users.js │ ├── welcome.js │ └── widgets │ │ ├── aggrid_widget.js │ │ ├── linechart_widget.js │ │ ├── todos_widget.js │ │ └── users_widget.js ├── forms │ ├── form.js │ ├── signin_form.js │ ├── signup_form.js │ └── todo_form.js ├── helpers │ ├── dispatcher.js │ ├── intl.js │ ├── simulate_event.js │ ├── string.js │ ├── test.js │ └── translation.js ├── index.js ├── models │ ├── dashboard_model.js │ ├── todo_model.js │ └── user_model.js ├── resolvers │ ├── auth_resolver.js │ ├── authorization_resolver.js │ ├── crud_resolver.js │ ├── dashboard_resolver.js │ ├── logger_resolver.js │ ├── message_resolver.js │ ├── page_resolver.js │ ├── popup_resolver.js │ ├── tabbar_resolver.js │ ├── test_resolver.js │ ├── thunk_resolver.js │ ├── todo_resolver.js │ └── user_resolver.js ├── services │ ├── auth_service.js │ ├── const_service.js │ ├── crud_service.js │ ├── dashboard_service.js │ ├── message_service.js │ ├── todo_service.js │ ├── user_service.js │ ├── widget_service.js │ └── wss_service.js ├── stores │ ├── auth_store.js │ ├── crud_store.js │ ├── dashboard_store.js │ ├── message_store.js │ ├── page_store.js │ ├── popup_store.js │ ├── register_store.js │ ├── search_store.js │ ├── tabbar_store.js │ ├── todo_store.js │ ├── user_store.js │ └── widget_store.js ├── styles │ ├── _00-config.scss │ ├── _01a-normalize.scss │ ├── _01b-base.scss │ ├── _02-layout.scss │ ├── _03-grids.scss │ ├── _04-tables.scss │ ├── _05-forms.scss │ ├── _06-helpers.scss │ ├── _07-responsive.scss │ ├── _08-print.scss │ ├── _09-misc.scss │ ├── _10-styling.scss │ ├── _11-wordpress.scss │ ├── _20-rp-config.scss │ ├── _21-rp-body.scss │ ├── _22-rp-popup.scss │ ├── _23-rp-table.scss │ ├── _24-rp-widget.scss │ ├── _25-rp-dashboard.scss │ ├── _26-rp-search.scss │ ├── _27-rp-form.scss │ ├── _28-rp-tabbar.scss │ └── knacss.scss └── types │ ├── auth_types.js │ ├── dashboard_types.js │ ├── message_types.js │ ├── popup_types.js │ ├── tabbar_types.js │ ├── todo_types.js │ ├── user_types.js │ └── widget_types.js ├── test ├── auth_test.js ├── base_test.js ├── services │ └── auth_service.js └── test_helper.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "react", "es2015", "stage-3",'airbnb' ], 3 | "plugins": [ "transform-decorators-legacy" , 4 | "transform-class-properties", 5 | "transform-async-to-generator" ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | #dist folder 30 | dist 31 | 32 | #Webstorm metadata 33 | .idea 34 | 35 | #VSCode metadata 36 | .vscode 37 | 38 | # Mac files 39 | .DS_Store 40 | 41 | bundle.js 42 | web.config 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 vinej (Jean-Yves Vinet) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-portal 2 | 3 | A full cycle example of a Dashboard using React, Mobx and ReMux, a Flux pattern inspired by Redux 4 | 5 | ReMux stand for **Re**act **M**obx Fl**ux** pattern and it's inspired by Redux. The react-portal don't use the ReMux pattern exacly as defined here : https://github.com/vinej/react-remux, because the pattern was improved after developing react-portal 6 | 7 | History: 8 | 9 | * 0.6.14 - First stable beta version 10 | * 0.6.15 - Dispatcher optimization, react-grid-layout resize bug 11 | 12 | For a simple starter see : https://github.com/vinej/react-remux 13 | 14 | ## Features 15 | 16 | * A boilerplate for React/Mobx and a REST server 17 | * npm start => development mode 18 | * npm run build => production mode 19 | * npm run test => run mocha test 20 | * npm run dist => create distribution files 21 | * A full CRUD TODO example : view -> action -> middleware -> service(ajax call) -> reducer -> store -> view 22 | * Authentification with a JWT token 23 | * Authorization (which actions the user can do) 24 | * A scalable dispatcher 25 | * Resolvers (logger, thunk, autorization, ...) like reducers/middlewares 26 | * 3 types of resolver (pre , std, post) 27 | * Form validation and abstraction (from Michel Weststrate) 28 | * Movable Popup modal form 29 | * Tab Bar 30 | * Dynamic widgets and dashboards with auto save profile 31 | * Unit test with mocha,chai,chai-jquery and enzyme (only some tests for now) 32 | * test_helper from Stephen Grider 33 | * test_resolver to test action content and dispatch calls 34 | * test promise to test asychrone calls 35 | * MockxxxxService to mock the service layer 36 | * Normalize CSS with knacss (normalize with steoride) 37 | * React-Intl setup for international support (en, fr by default) 38 | * ChartJS, AgGrid integration 39 | 40 | ## Installation 41 | * install nodejs 42 | * npm install -g mocha 43 | * npm install 44 | * npm start 45 | * use the portal 46 | * signup for the first time : after that use 'signin' 47 | * use the option : Create a new Dashboard 48 | * use the option : Add Widgets into the current Dashboard 49 | * use the option : Rename the current Dashboard as needed 50 | * use the option : Show/Hide Dashboards to show or hide dashboards 51 | * move widgets by dragging them around with the green header 52 | * you can remove widgets from a Dashboard with the "X" icon 53 | * you can open a widget into its own tab with the "folder" icon 54 | * you can close a Dashboard with the red small "X" 55 | 56 | Notes: 57 | * You need to install 'react-portal-server' to run the application 58 | 59 | * There are 2 servers 60 | * the REST server on port 3090 npm run dev 61 | * the WebSocket server on port 5000 npm run wss 62 | 63 | * This example is built with the help of Stephen Grider course 'Advanced React with Redux' (excellent) and some Internet information about Mobx. 64 | 65 | ## Goals 66 | 67 | The first goal is to experiment using React/Mobx and a Flux pattern like Redux. The second goal is to implement enough features to have a base Framework to start a real project. After one month of testing and implementing real features with the flux pattern I can say that Mobx fulfills my expectations. 68 | 69 | ## Why use a Flux pattern with Mobx 70 | 71 | There is no obligation to use a Flux pattern with Mobx if your application is simple. During the last year, I worked on a Dashboard implementation and the customer asked me to audit all actions from the users. If you have a Flux pattern, it's very easy to do : you just add a resolver (middleware). Without it, it could become complexe to implement it. So, you must use a Flux pattern when you know that you will deal with this kind of feature. 72 | 73 | ## Compare React/Redux with React/ReMux 74 | 75 | With React/ReMux 76 | 77 | * Stores use mutable data 78 | * Stores contain actions with Mobx @action decorator 79 | * Stores actions are called through resolvers to update the data 80 | * Resolvers are something like reducers from Redux and middlewares. The goal of a resolver is to decide what to do with an action 81 | * Stores are passed as props to Components (must of the time) 82 | * Stores attributes are observables with the @observable decorator 83 | * Stores are singletons or intance classes as needed 84 | 85 | * Components are refreshed with Mobx @obserser decorator 86 | * Components props are validated with 'propTypes' 87 | * Components use model shape to validate entity with React.PropTypes.shape 88 | * Components use 'dispatch' function to dispatch actions creator 89 | * Components must use stores' data in read only mode 90 | 91 | ## Dashboard example 92 | 93 | ![db](https://cloud.githubusercontent.com/assets/3254214/16365417/a86e9fe4-3bcd-11e6-8d1b-4084ef151d41.png) 94 | 95 | JYV 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"bundle.js","sources":["webpack:///bundle.js","webpack:///"],"mappings":"AAAA;;;;;;ACqQA;;;;;;AA6RA;;;;;;AAmqBA;;;;;;AAueA;;;;;;AAktCA;;;;;;AAueA;;;;;;AAmEA;;;;;;AAw9EA;;;;;;AAiFA;;;;;;AAwTA;;;;;;AAk8BA;;;;;;AA+UA;AAy1FA;;;;;;AA8ZA;;;;;;AA2+BA;;;;;;AAoPA;;;;;;AA4oCA;;;;;;AAgCA;;;;;;AAsTA;;;;;;AAoFA;;;;;;AAsqBA;;;;;;AAoSA;;;;;;AAilBA;;;;;;AAoIA;;;;;;AAgLA;AAqOA;;;;;;AAygCA;;;;;;AA6JA;;;;;;AAkUA;;;;;;AAuLA;;;;;;AAq0CA;;;;;;AAuGA;;;;;;AAsjEA;;;;;;AA8KA;;;;;;AAqNA;;;;;;AA6HA;;;;;;AAuhBA;;;;;;AA0GA;;;;;;AAqNA;;;;;;AAkGA;;;;;;AA8DA;;;;;;AAsVA;;;;;;AAqmEA;;;;;;AA+NA;;;;;;AAyKA;;;;;;AAsIA;;;;;;AAiNA;;;;;;AA0EA;;;;;;AAyIA;;;;;;AAgLA;;;;;;AA+CA;;;;;;AAiIA;;;;;;AAuGA;;;;;;AAqQA;;;;;;AAuFA;;;;;;AAoDA;;;;;;AAgFA;AAmpEA;;;;;;;;;;;;;;AA2iDA;;;;;;AAisBA;;;;;;AAsHA;;;;;;AAuDA;;;;;;AAiCA;;;;;;AAuNA;;;;;;AA+MA;;;;;;AAgCA;;;;;;AA2KA;;;;;;AAqRA;;;;;;AA0XA;;;;;;AA+QA;;;;;;AAoFA;;;;;;AAsIA;;;;;;AAuEA;;;;;;AA+DA;;;;;;AA6BA;;;;;;AA6BA;;;;;;AAuFA;;;;;;AA2RA;;;;;;AA+oBA;;;;;;AAoGA;;;;;;AAiHA;;;;;;AAscA;;;;;;AA0GA;;;;;;AAyaA;AAw0EA;;;;;AAgpFA;AA89FA;AAs2FA;;;;;;AAgYA;;;;;;AA8MA;;;;;;AAwCA;;;;;;AAiHA;;;;;;AA+HA;;;;;;AAuGA;;;;;;AA4CA;;;;;;AAoGA;;;;;;AAi/BA;AAm/DA;AA4xDA;AAwsEA;AAggGA;AAsRA;AA0CA;AAAA;AAoqHA;AA4sBA;;;;;AAMA;;;;;AAKA;AAk1BA;AAmsDA;AAu4FA;AA2qFA;AAgxGA","sourceRoot":""} -------------------------------------------------------------------------------- /dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinej/react-portal/dbc499a3469dda8bc50b608176e016b2347d3844/dashboard.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinej/react-portal/dbc499a3469dda8bc50b608176e016b2347d3844/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-portal", 3 | "version": "0.6.15", 4 | "description": "A Dashboard application with React/Mobx and a Flux pattern inspired by Redux", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 8 | "dist": "webpack -p --progress --colors", 9 | "build": "webpack-dev-server -p --progress --colors --history-api-fallback --watch", 10 | "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive ./test" 11 | }, 12 | "keywords": [ 13 | "dashboard", 14 | "react", 15 | "mobx", 16 | "redux", 17 | "flux", 18 | "widget" 19 | ], 20 | "author": "Jean-Yves Vinet", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/vinej/react-portal.git" 24 | }, 25 | "license": "MIT", 26 | "devDependencies": { 27 | "babel-core": "^6.26.0", 28 | "babel-loader": "^7.1.2", 29 | "babel-plugin-transform-async-to-generator": "^6.8.0", 30 | "babel-plugin-transform-class-properties": "^6.9.0", 31 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 32 | "babel-polyfill": "^6.9.1", 33 | "babel-preset-airbnb": "^2.0.0", 34 | "babel-preset-env": "^1.6.0", 35 | "babel-preset-es2015": "^6.9.0", 36 | "babel-preset-react": "^6.5.0", 37 | "babel-preset-react-hmre": "^1.1.1", 38 | "babel-preset-stage-3": "^6.24.1", 39 | "babel-register": "^6.9.0", 40 | "chai": "^3.5.0", 41 | "chai-jquery": "^2.0.0", 42 | "cross-env": "^5.0.5", 43 | "css-loader": "^0.28.7", 44 | "enzyme": "^2.3.0", 45 | "file-loader": "^1.1.4", 46 | "jquery": "^3.0.0", 47 | "jsdom": "^9.2.1", 48 | "mocha": "^2.5.3", 49 | "mocha-phantomjs": "^4.0.2", 50 | "node-sass": "^4.7.2", 51 | "react-addons-test-utils": "^15.1.0", 52 | "sass-loader": "^6.0.7", 53 | "style-loader": "^0.13.1", 54 | "vue-loader": "^13.0.5", 55 | "vue-template-compiler": "^2.4.4", 56 | "webpack": "^3.11.0", 57 | "webpack-dev-server": "^2.9.1" 58 | }, 59 | "dependencies": { 60 | "ag-grid": "^4.2.6", 61 | "ag-grid-react": "^4.2.0", 62 | "ajv": "^6.3.0", 63 | "ajv-keywords": "^3.1.0", 64 | "axios": "0.12.0", 65 | "chart.js": "^2.1.6", 66 | "cross-env": "^5.1.4", 67 | "mobx": "^2.3.2", 68 | "mobx-react": "^3.3.1", 69 | "mobx-react-devtools": "^4.2.0", 70 | "react": "^15.1.0", 71 | "react-dom": "^15.1.0", 72 | "react-grid-layout": "^0.12.6", 73 | "react-intl": "^2.1.3", 74 | "react-router": "^2.4.1", 75 | "socket.io": "^1.4.6", 76 | "socket.io-client": "^1.4.6", 77 | "ws": "^1.1.0" 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/actions/auth_actions.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/auth_types'; 2 | import AuthService from '../services/auth_service'; 3 | 4 | export function authSetAuthorizations(mainComponentsToRender) { 5 | return { 6 | type: t.AUTH_SET_AUTHORIZATIONS, 7 | payload: function() { 8 | const authService = AuthService.getInstance() 9 | authService.setAuthorizations(mainComponentsToRender, authSetAuthorizationsIt , authError); 10 | } 11 | } 12 | } 13 | 14 | export function authSetAuthorizationsIt(mainComponentsToRender, authorizations) { 15 | return { 16 | type: t.AUTH_SET_AUTHORIZATIONS, 17 | render: mainComponentsToRender, 18 | payload: authorizations 19 | } 20 | } 21 | 22 | export function authSignIn({ email, password }) { 23 | return { 24 | type: t.AUTH_SIGN_IN, 25 | payload: function() { 26 | const authService = AuthService.getInstance() 27 | authService.signIn({ email, password }, authSignInIt, authError) 28 | } 29 | } 30 | } 31 | 32 | export function authSignUp({ email, password, name }) { 33 | return { 34 | type: t.AUTH_SIGN_UP, 35 | payload : function() { 36 | const authService = AuthService.getInstance() 37 | authService.signUp({ email, password, name }, authSignUpIt, authError) 38 | } 39 | } 40 | } 41 | 42 | export function authCheckToken(mainComponentsToRender) { 43 | return { 44 | type: t.AUTH_CHECK_TOKEN, 45 | render: mainComponentsToRender 46 | } 47 | } 48 | 49 | export function authSignInIt(token, name) { 50 | return { 51 | type: t.AUTH_SIGN_IN, 52 | payload: { token, name } 53 | } 54 | } 55 | 56 | export function authSignUpIt(token, name) { 57 | return { 58 | type: t.AUTH_SIGN_UP, 59 | payload: { token, name } 60 | }; 61 | } 62 | 63 | export function authSignOut() { 64 | return { type: t.AUTH_SIGN_OUT }; 65 | } 66 | 67 | export function authValidateSignUp(store) { 68 | return { 69 | type: t.AUTH_VALIDATE_SIGN_UP, 70 | store: store 71 | } 72 | } 73 | 74 | export function authValidateSignIn(store) { 75 | return { 76 | type: t.AUTH_VALIDATE_SIGN_IN, 77 | store: store 78 | } 79 | } 80 | 81 | export function authError(error, mainComponentsToRender) { 82 | return { 83 | type: t.AUTH_ERROR, 84 | render: mainComponentsToRender, 85 | payload : error 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/actions/crud_actions.js: -------------------------------------------------------------------------------- 1 | import { registerStore } from '../stores/register_store'; 2 | 3 | /* base action for store crud actions */ 4 | 5 | /** 6 | * The store could be a string or an object 7 | * A string means that we must find the first store of that type 8 | * A objet means that it's already a store objet 9 | */ 10 | function findStore(store) { 11 | if (typeof store !== 'string') { 12 | return store 13 | } else { 14 | const stores = registerStore.getAll(store); 15 | if (stores.length > 0) { 16 | return stores[0] 17 | } else { 18 | return store; 19 | } 20 | } 21 | } 22 | 23 | export function crudDelete(store, record) { 24 | store = findStore(store) 25 | return { 26 | type: `${store.name}_delete_`, 27 | payload: function() { 28 | store.service.delete(store, record, crudDeleteIt, pageError); 29 | } 30 | } 31 | } 32 | 33 | export function crudDeleteIt(store, record) { 34 | return { 35 | type: `${store.name}_delete`, 36 | store: store, 37 | payload: record 38 | }; 39 | } 40 | 41 | export function crudUpdate(store, record) { 42 | store = findStore(store) 43 | return { 44 | type: `${store.name}_update_`, 45 | payload: function() { 46 | store.service.update(store, record, crudUpdateIt, pageError); 47 | } 48 | } 49 | } 50 | 51 | export function crudUpdateIt(store, record) { 52 | return { 53 | type: `${store.name}_update`, 54 | store: store, 55 | payload: record 56 | }; 57 | } 58 | 59 | export function crudAdd(store, record) { 60 | store = findStore(store) 61 | return { 62 | type: `${store.name}_add_`, 63 | payload: function() { 64 | store.service.add(store, record, crudAddIt, pageError); 65 | } 66 | } 67 | } 68 | 69 | export function crudAddIt(store, record) { 70 | return { 71 | type: `${store.name}_add`, 72 | store: store, 73 | payload: record 74 | }; 75 | } 76 | 77 | export function crudGetAll(store) { 78 | store = findStore(store) 79 | return { 80 | type: `${store.name}_get_all_`, 81 | payload: function() { 82 | store.service.getAll(store, crudGetAllIt, pageError); 83 | } 84 | } 85 | } 86 | 87 | export function crudGetAllIt(store, data) { 88 | return { 89 | type: `${store.name}_get_all`, 90 | store : store, 91 | payload: data 92 | } 93 | } 94 | 95 | export function pageError(store, error) { 96 | return { 97 | type: `${store.name}_error`, 98 | store : store, 99 | payload : error 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/actions/dashboard_actions.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/dashboard_types'; 2 | 3 | export function dashboardRemoveWidget(dashboardId, widgetId) { 4 | return { 5 | type: t.DASHBOARD_REMOVE_WIDGET, 6 | payload: { dashboardId, widgetId } 7 | } 8 | } 9 | 10 | export function dashboardAddWidget(widgetName) { 11 | return { 12 | type: t.DASHBOARD_ADD_WIDGET, 13 | payload: widgetName 14 | } 15 | } 16 | 17 | export function dashboardAddDashboard(dashboardName) { 18 | return { 19 | type: t.DASHBOARD_ADD_DASHBOARD, 20 | payload: dashboardName 21 | } 22 | } 23 | 24 | export function dashboardRenameDashboard(dashboardName) { 25 | return { 26 | type: t.DASHBOARD_RENAME_DASHBOARD, 27 | payload: dashboardName 28 | } 29 | } 30 | 31 | export function dashboardShow(dashboardId) { 32 | return { 33 | type: t.DASHBOARD_SHOW, 34 | payload: dashboardId 35 | } 36 | } 37 | 38 | export function dashboardHide(dashboardId) { 39 | return { 40 | type: t.DASHBOARD_HIDE, 41 | payload: dashboardId 42 | } 43 | } 44 | 45 | export function dashboardDelete() { 46 | return { 47 | type: t.DASHBOARD_DELETE_DASHBOARD 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/actions/message_actions.js: -------------------------------------------------------------------------------- 1 | import { messageService } from '../services/message_service'; 2 | import * as t from '../types/message_types'; 3 | 4 | export function messageFetch() { 5 | return { 6 | type: t.MESSAGE_FETCH, 7 | payload: function() { 8 | messageService.fetchMessage(messageFetchIt, messageError); 9 | } 10 | } 11 | } 12 | 13 | export function messageFetchIt(message) { 14 | return { 15 | type: t.MESSAGE_SET, 16 | payload: message 17 | } 18 | } 19 | 20 | export function messageError(error) { 21 | return { 22 | type: t.MESSAGE_ERROR, 23 | payload: error 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/page_actions.js: -------------------------------------------------------------------------------- 1 | import { registerStore } from '../stores/register_store'; 2 | 3 | /* base action for store crud actions */ 4 | 5 | /** 6 | * The store could be a string or an object 7 | * A string means that we must find the first store of that type 8 | * A objet means that it's already a store objet 9 | */ 10 | function findStore(store) { 11 | if (typeof store !== 'string') { 12 | return store 13 | } else { 14 | const stores = registerStore.getAll(store); 15 | if (stores.length > 0) { 16 | return stores[0] 17 | } else { 18 | return store; 19 | } 20 | } 21 | } 22 | 23 | export function pageNext(store) { 24 | store = findStore(store) 25 | return { 26 | type: `${store.name}_next_page`, 27 | store: store 28 | }; 29 | } 30 | 31 | export function pagePrevious(store) { 32 | store = findStore(store) 33 | return { 34 | type: `${store.name}_previous_page`, 35 | store: store 36 | }; 37 | } 38 | 39 | export function pageDeleteRecord(store, record) { 40 | store = findStore(store) 41 | return { 42 | type: `${store.name}_delete_`, 43 | payload: function() { 44 | store.service.delete(store, record, pageDeleteRecordIt, pageError); 45 | } 46 | } 47 | } 48 | 49 | export function pageDeleteRecordIt(store, record) { 50 | return { 51 | type: `${store.name}_delete`, 52 | store: store, 53 | payload: record 54 | }; 55 | } 56 | 57 | export function pageUpdateRecord(store, record) { 58 | store = findStore(store) 59 | return { 60 | type: `${store.name}_update_`, 61 | payload: function() { 62 | store.service.update(store, record, pageUpdateRecordIt, pageError); 63 | } 64 | } 65 | } 66 | 67 | export function pageUpdateRecordIt(store, record) { 68 | return { 69 | type: `${store.name}_update`, 70 | store: store, 71 | payload: record 72 | }; 73 | } 74 | 75 | export function pageAddRecord(store, record) { 76 | store = findStore(store) 77 | return { 78 | type: `${store.name}_add_`, 79 | payload: function() { 80 | store.service.add(store, record, pageAddRecordIt, pageError); 81 | } 82 | } 83 | } 84 | 85 | export function pageAddRecordIt(store, record) { 86 | return { 87 | type: `${store.name}_add`, 88 | store: store, 89 | payload: record 90 | }; 91 | } 92 | 93 | export function pageGetAll(store) { 94 | store = findStore(store) 95 | return { 96 | type: `${store.name}_get_all_`, 97 | payload: function() { 98 | store.service.getAll(store, pageGetAllIt, pageError); 99 | } 100 | } 101 | } 102 | 103 | export function pageGetAllIt(store, data) { 104 | return { 105 | type: `${store.name}_get_all`, 106 | store : store, 107 | payload: data 108 | } 109 | } 110 | 111 | export function pageError(store, error) { 112 | return { 113 | type: `${store.name}_error`, 114 | store : store, 115 | payload : error 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/actions/popup_actions.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/popup_types'; 2 | 3 | // function return component must be inside an abject 4 | export function popupShow(component) { 5 | return { 6 | type: t.POPUP_SHOW, 7 | payload: { component } 8 | } 9 | } 10 | 11 | export function popupClose() { 12 | return { 13 | type: t.POPUP_CLOSE 14 | } 15 | } -------------------------------------------------------------------------------- /src/actions/tabbar_actions.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/tabbar_types'; 2 | 3 | export function tabbarShow(component, componentId, title, type) { 4 | return { 5 | type: t.TABBAR_SHOW, 6 | payload: { component, componentId, title, type } 7 | } 8 | } 9 | 10 | export function tabbarClose(componentId) { 11 | return { 12 | type: t.TABBAR_CLOSE, 13 | payload : componentId 14 | } 15 | } 16 | 17 | export function tabbarCloseAll() { 18 | return { 19 | type: t.TABBAR_CLOSE_ALL 20 | } 21 | } 22 | 23 | export function tabbarSelect(componentId) { 24 | return { 25 | type: t.TABBAR_SELECT, 26 | payload: componentId 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/actions/todo_actions.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/todo_types'; 2 | 3 | export function todoDone(store,record) { 4 | return { 5 | type: t.TODO_DONE, 6 | store: store, 7 | payload: record 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/actions/user_actions.js: -------------------------------------------------------------------------------- 1 | import { userService } from '../services/user_service'; 2 | import * as t from '../types/user_types'; 3 | 4 | export function userDelete(store,user) { 5 | return { 6 | type: t.USER_DELETE, 7 | store: store, 8 | payload: user 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Popup from './popup' 3 | import Header from './header' 4 | import Footer from './footer' 5 | import Dashboard from './dashboard/dashboard' 6 | import DevTools from 'mobx-react-devtools' 7 | import { popupStore } from '../stores/popup_store' 8 | import { observer } from "mobx-react"; 9 | 10 | @observer 11 | class App extends Component { 12 | render() { 13 | return ( 14 |
15 | { popupStore.getStores().map( store => 16 | 17 | { store.component() } 18 | 19 | ) 20 | } 21 |
22 | {this.props.children} 23 | {/* } */} 24 |
26 | ) 27 | } 28 | } 29 | export default App; -------------------------------------------------------------------------------- /src/components/auth/require_auth.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react"; 3 | import { authStore } from '../../stores/auth_store'; 4 | 5 | @observer 6 | export default function(ComposedComponent) { 7 | class Authentication extends Component { 8 | 9 | static contextTypes = { 10 | router: React.PropTypes.object 11 | } 12 | 13 | componentWillMount() { 14 | if (!authStore.authenticated) { 15 | this.context.router.push('/'); 16 | } 17 | } 18 | 19 | componentWillUpdate(nextProps) { 20 | if (!authStore.authenticated) { 21 | this.context.router.push('/'); 22 | } 23 | } 24 | 25 | render() { 26 | return 27 | } 28 | } 29 | 30 | return Authentication; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/auth/signin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer } from "mobx-react" 3 | import { dispatch } from '../../helpers/dispatcher' 4 | import { authSignIn } from '../../actions/auth_actions' 5 | import Form from '../../forms/form' 6 | import AuthStore from '../../stores/auth_store' 7 | import { FormattedMessage } from 'react-intl' 8 | 9 | @observer 10 | export default class Signin extends Component { 11 | async handleSend(event,form) { 12 | event.preventDefault() 13 | await form.validate(); 14 | if (form.valid) { 15 | dispatch(authSignIn( { 16 | email : form.fields.email.value, 17 | password : form.fields.password.value })); 18 | } 19 | } 20 | 21 | static propTypes = { 22 | form: React.PropTypes.instanceOf(Form), 23 | store: React.PropTypes.instanceOf(AuthStore) 24 | } 25 | 26 | render() { 27 | const form = this.props.form 28 | if (!form) { return
} 29 | return ( 30 |
31 |
32 |
33 | 34 | form.fields.email.value = e.target.value}/> 37 | { form.renderError(form.fields.email.errorMessage) } 38 |
39 | 40 |
41 | 42 | form.fields.password.value = e.target.value} /> 46 | { form.renderError(form.fields.password.errorMessage) } 47 |
48 |
49 | 53 |
54 | {form.renderAlert(this.props.store.errorMessage)} 55 |
56 | ) 57 | } 58 | } 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/components/auth/signout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { authSignOut } from '../../actions/auth_actions'; 3 | import { dispatch } from '../../helpers/dispatcher'; 4 | import { FormattedMessage } from 'react-intl' 5 | 6 | class Signout extends Component { 7 | componentWillMount() { 8 | dispatch(authSignOut()); 9 | } 10 | 11 | render() { 12 | return
; 13 | } 14 | } 15 | export default Signout; 16 | -------------------------------------------------------------------------------- /src/components/auth/signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer } from 'mobx-react' 3 | import { authSignUp } from '../../actions/auth_actions' 4 | import { dispatch } from '../../helpers/dispatcher' 5 | import Form from '../../forms/form' 6 | import AuthStore from '../../stores/auth_store' 7 | import { FormattedMessage } from 'react-intl' 8 | 9 | @observer 10 | export default class Signup extends Component { 11 | async handleSend(event,form) { 12 | event.preventDefault(); 13 | await form.validate(); 14 | if (form.valid) { 15 | dispatch(authSignUp( { 16 | email : form.fields.email.value, 17 | name : form.fields.name.value, 18 | password : form.fields.password.value })); 19 | } 20 | } 21 | 22 | static propTypes = { 23 | form: React.PropTypes.instanceOf(Form), 24 | store: React.PropTypes.instanceOf(AuthStore) 25 | } 26 | 27 | render() { 28 | const form = this.props.form 29 | return ( 30 |
31 |
32 |
33 | 34 | form.fields.email.value = e.target.value}/> 37 | { form.renderError(form.fields.email.errorMessage) } 38 |
39 |
40 | 41 | form.fields.password.value = e.target.value}/> 45 | { form.renderError(form.fields.password.errorMessage) } 46 |
47 |
48 | 49 | form.fields.passwordConfirm.value = e.target.value}/> 53 | { form.renderError(form.fields.passwordConfirm.errorMessage) } 54 |
55 |
56 | 57 | form.fields.name.value = e.target.value}/> 60 | { form.renderError(form.fields.name.errorMessage) } 61 |
62 | {form.renderAlert(this.props.store.errorMessage)} 63 |
64 | 66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/components/charts/line_chart.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | require("chart.js") 3 | 4 | export default class LineChart extends Component { 5 | constructor() { 6 | super() 7 | this.chartData = { 8 | labels: ["January", "February", "March", "April", "May", "June", "July"], 9 | datasets: [ 10 | { 11 | label: 'React Portal', 12 | fillColor: "#25BDFF", 13 | strokeColor: "#25BDFF", 14 | pointColor: "#25BDFF", 15 | pointStrokeColor: "#fff", 16 | pointHighlightFill: "#fff", 17 | pointHighlightStroke: "#25BDFF", 18 | data: [28, 48, 40, 19, 86, 27, 90] 19 | } 20 | ] 21 | } 22 | 23 | this.chartOptions = { 24 | bezierCurve : false, 25 | datasetFill : false, 26 | pointDotStrokeWidth: 4, 27 | scaleShowVerticalLines: false, 28 | responsive: false 29 | } 30 | } 31 | 32 | componentDidMount () { 33 | let chartCanvas = this.refs.chart; 34 | 35 | let myChart = new Chart(chartCanvas, { 36 | type: 'line', 37 | data: this.chartData, 38 | options: this.chartOptions, 39 | }); 40 | 41 | this.setState({chart: myChart}); 42 | } 43 | 44 | render () { 45 | return ( 46 | 47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /src/components/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer, action } from "mobx-react"; 3 | import ReactGridLayout from 'react-grid-layout' 4 | import { dashboardStore, getWidgetComponent } from '../../stores/dashboard_store' 5 | import { dispatch } from '../../helpers/dispatcher' 6 | import { crudUpdate } from '../../actions/crud_actions' 7 | import { FormattedMessage } from 'react-intl' 8 | 9 | @observer 10 | class Dashboard extends Component { 11 | constructor() { 12 | super() 13 | this.layout = null 14 | this.handleOnLayoutChange = this.handleOnLayoutChange.bind(this) 15 | } 16 | 17 | static propTypes = { 18 | id: React.PropTypes.string.isRequired, // the id of the dashboard, could be the _id of mongoose 19 | title: React.PropTypes.string.isRequired // the title of the dashboard,use for the tab title 20 | } 21 | 22 | /* 23 | this method unsure that we handle layout changes only if the widget 24 | have really resized or moved 25 | */ 26 | isDifferent(old, nw) { 27 | //debugger 28 | if (!old) return false 29 | if (!nw) return false 30 | var isDiff = false 31 | for(var i = 0; i < old.length; i++) { 32 | if ( old[i].x !== nw[i].x || old[i].y !== nw[i].y || old[i].w !== nw[i].w || old[i].h !== nw[i].h) 33 | { 34 | isDiff = true 35 | break 36 | } 37 | } 38 | return isDiff 39 | } 40 | 41 | updateWidget(old, nw) { 42 | for(var i = 0; i < old.length; i++) { 43 | for(var j = 0; j < nw.length; j++) { 44 | if (old[i].i == nw[j].i) { 45 | old[i].x = nw[i].x 46 | old[i].y = nw[i].y 47 | old[i].w = nw[i].w 48 | old[i].h = nw[i].h 49 | break; 50 | } 51 | } 52 | } 53 | } 54 | 55 | handleOnLayoutChange(layout) { 56 | const id = this.props.id 57 | if (!id) return 58 | if (this.isDifferent(layout, dashboardStore.getWidgets(id)) === true) { 59 | this.updateWidget(dashboardStore.getWidgets(id), layout) 60 | //dashboardStore.getDashboard(id).widgets = layout 61 | dispatch(crudUpdate(dashboardStore, dashboardStore.getDashboard(id))) 62 | } 63 | } 64 | 65 | render() { 66 | var layouts = dashboardStore.getWidgetsLayout(this.props.id) 67 | if (layouts.length == 0) { 68 | return (
) 69 | } else { 70 | return ( 71 | 79 | { layouts.map( widget => 80 |
81 | { getWidgetComponent(widget.name, this.props.id, widget.i) } 82 |
) 83 | } 84 |
85 | ) 86 | } 87 | } 88 | } 89 | export default Dashboard; 90 | -------------------------------------------------------------------------------- /src/components/dashboard/dashboard_form.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { popupClose } from '../../actions/popup_actions' 3 | import { dashboardAddDashboard, dashboardRenameDashboard } from '../../actions/dashboard_actions' 4 | import { dispatch } from '../../helpers/dispatcher' 5 | import { FormattedMessage } from 'react-intl' 6 | 7 | class DashboardForm extends Component { 8 | constructor() { 9 | super() 10 | this.handleFormSubmit = this.handleFormSubmit.bind(this) 11 | this.handleOnChange = this.handleOnChange.bind(this) 12 | this.state = { dashboardTitle : "" } 13 | } 14 | 15 | static propTypes = { 16 | title: React.PropTypes.string, // the title of the dashboard 17 | action: React.PropTypes.oneOf(['add', 'rename']) 18 | } 19 | 20 | handleFormSubmit( event ) { 21 | event.preventDefault(); 22 | if (this.props.action == "add") { 23 | dispatch(dashboardAddDashboard(this.state.dashboardTitle)) 24 | } else { 25 | dispatch(dashboardRenameDashboard(this.state.dashboardTitle)) 26 | } 27 | dispatch(popupClose()) 28 | } 29 | 30 | handleOnChange( event ) { 31 | this.setState( { dashboardTitle : event.target.value } ) 32 | } 33 | 34 | componentDidMount() { 35 | this.refs.nameInput.focus(); 36 | this.setState( { dashboardTitle : this.props.dashboard_name } ) 37 | } 38 | 39 | getHeader() { 40 | return this.props.action === 'add' ? 41 | : 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 |
48 | { this.getHeader() } 49 |
50 |
51 |   52 | 56 |
57 |
58 | 61 | 64 |
65 |
66 | ) 67 | } 68 | } 69 | export default DashboardForm; 70 | -------------------------------------------------------------------------------- /src/components/dashboard/dashboard_manage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer } from "mobx-react"; 3 | import DashboardStore from '../../stores/dashboard_store' 4 | import { dispatch } from '../../helpers/dispatcher' 5 | import { dashboardShow, dashboardHide } from '../../actions/dashboard_actions' 6 | import { tabbarClose } from '../../actions/tabbar_actions' 7 | import { popupClose } from '../../actions/popup_actions' 8 | import DashboardModel from '../../models/dashboard_model' 9 | import { FormattedMessage } from 'react-intl' 10 | 11 | @observer // need observer to update a row when a note is modified 12 | class DashboardManageItem extends Component { 13 | constructor() { 14 | super(); 15 | this.handleClick = this.handleClick.bind(this); 16 | } 17 | 18 | static propTypes = { 19 | dashboard : React.PropTypes.shape(DashboardModel.shape) 20 | } 21 | 22 | handleClick(dashboardId, isHidden) { 23 | console.log(isHidden) 24 | if (isHidden) { 25 | dispatch(dashboardShow(dashboardId)) 26 | } else { 27 | dispatch(tabbarClose(dashboardId)) 28 | } 29 | } 30 | 31 | getBackGroundColor(isHidden) { 32 | return { backgroundColor: (isHidden ? 'white' : 'lightgray') } 33 | } 34 | 35 | render() { 36 | const dashboard = this.props.dashboard 37 | return ( 38 |
this.handleClick(dashboard._id, dashboard.isHidden)}> 40 | {dashboard.title} 41 |
42 | ); 43 | } 44 | }; 45 | 46 | @observer 47 | class DashboardManage extends Component { 48 | static propTypes = { 49 | store: React.PropTypes.instanceOf(DashboardStore) 50 | } 51 | 52 | render() { 53 | return ( 54 |
55 |
56 | { this.props.store.records.map( (dashboard) => 57 | 58 | )} 59 |
60 | 63 |
64 |
65 | ) 66 | } 67 | } 68 | export default DashboardManage; 69 | -------------------------------------------------------------------------------- /src/components/dashboard/search_tool.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class SearchTool extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | } 13 | export default SearchTool; 14 | -------------------------------------------------------------------------------- /src/components/dashboard/widget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Widget extends Component { 4 | render() { 5 | return ( 6 |
7 |
{ this.props.title } 8 | 9 | 10 | 11 |
12 | {this.props.children} 13 |
14 | ) 15 | } 16 | } 17 | export default Widget; 18 | -------------------------------------------------------------------------------- /src/components/dashboard/widget_form.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { popupClose } from '../../actions/popup_actions' 3 | import { dashboardAddWidget } from '../../actions/dashboard_actions' 4 | import { dispatch } from '../../helpers/dispatcher' 5 | import WidgetStore from '../../stores/widget_store' 6 | import { FormattedMessage } from 'react-intl' 7 | 8 | class WidgetForm extends Component { 9 | 10 | handleClick(name) { 11 | dispatch(dashboardAddWidget(name)) 12 | } 13 | 14 | static propTypes = { 15 | store: React.PropTypes.instanceOf(WidgetStore) 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |
22 | { this.props.store.getWidgets().map( (widget) => 23 |
this.handleClick(widget.name)}>{widget.name}
24 | )} 25 | 26 |
27 | 30 |
31 |
32 | ) 33 | } 34 | } 35 | export default WidgetForm; 36 | -------------------------------------------------------------------------------- /src/components/dashboard/widget_tool.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { dashboardStore } from '../../stores/dashboard_store' 3 | import PopupStore, { popupStore } from '../../stores/popup_store' 4 | import { dispatch } from '../../helpers/dispatcher' 5 | import { popupShow } from '../../actions/popup_actions' 6 | import { dashboardDelete } from '../../actions/dashboard_actions' 7 | import WidgetForm from './widget_form' 8 | import PopupYesNo from '../popups/popup_yesno' 9 | import DashboardForm from './dashboard_form' 10 | import DashboardManage from './dashboard_manage' 11 | import { widgetStore } from '../../stores/widget_store' 12 | import { tabbarStore} from '../../stores/tabbar_store' 13 | import { FormattedMessage, injectIntl } from 'react-intl' 14 | 15 | class WidgetTool extends Component { 16 | 17 | constructor(props) { 18 | super(props) 19 | this.handleOnChange = this.handleOnChange.bind(this) 20 | } 21 | 22 | getAskDeleteMsg() { 23 | return 24 | } 25 | 26 | handleOnChange(e) { 27 | switch(e.target.value) { 28 | case 'create' : 29 | dispatch(popupShow( () => )) 30 | break; 31 | case 'rename' : 32 | dispatch(popupShow( () => )) 33 | break; 34 | case 'show_hide' : 35 | dispatch(popupShow( () => )) 36 | break; 37 | case 'delete' : 38 | dispatch(popupShow( () => } 39 | msg={ this.getAskDeleteMsg() } yesAction={ dashboardDelete } 40 | />)) 41 | break; 42 | case 'add_widget' : 43 | dispatch(popupShow( () => )) 44 | break; 45 | } 46 | } 47 | 48 | render() { 49 | const { formatMessage } = this.props.intl; 50 | return ( 51 |
52 | 60 |
61 | ) 62 | } 63 | } 64 | export default injectIntl(WidgetTool); 65 | -------------------------------------------------------------------------------- /src/components/feature.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react"; 3 | import { messageFetch } from '../actions/message_actions'; 4 | import { messageStore } from '../stores/message_store'; 5 | import { dispatch } from '../helpers/dispatcher'; 6 | 7 | @observer 8 | class Feature extends Component { 9 | componentWillMount() { 10 | dispatch(messageFetch()); 11 | } 12 | 13 | render() { 14 | return ( 15 |
{messageStore.message}
16 | ); 17 | } 18 | } 19 | export default Feature; 20 | -------------------------------------------------------------------------------- /src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedDate, FormattedRelative } from 'react-intl' 3 | import { FormattedMessage } from 'react-intl' 4 | 5 | class Footer extends Component { 6 | constructor() { 7 | super() 8 | } 9 | 10 | render() { 11 | const now = (new Date()).getTime() 12 | return ( 13 |
14 |
15 |
16 | version 0.6 17 |
18 |
19 | : 20 |    21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | } 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /src/components/grid/ag_grid.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { AgGridReact } from 'ag-grid-react'; 3 | import { observer } from "mobx-react" 4 | import { pageGetAll, pagePrevious, pageNext } from '../../actions/page_actions' 5 | import { dispatch } from '../../helpers/dispatcher' 6 | import { popupShow } from '../../actions/popup_actions' 7 | import TodoView from '../todo/todo_view' 8 | import { todoForm } from '../../forms/todo_form' 9 | import TodoModel from '../../models/todo_model' 10 | import TodoStore from '../../stores/todo_store' 11 | import { FormattedMessage } from 'react-intl' 12 | 13 | @observer 14 | export default class AgGrid extends Component { 15 | constructor() { 16 | super() 17 | this.handleAdd = this.handleAdd.bind(this) 18 | } 19 | 20 | static propTypes = { 21 | store: React.PropTypes.instanceOf(TodoStore), 22 | isRemoveStore : React.PropTypes.bool // true means that the store must be delete here 23 | // false means that the store will be deleted by the parent 24 | } 25 | 26 | handleAdd() { 27 | var component = () => 28 | dispatch(popupShow(component, TodoStore.getEditFormDimension())) 29 | } 30 | 31 | componentWillMount() { 32 | dispatch(pageGetAll(this.props.store)) 33 | } 34 | 35 | componentWillUnmount() { 36 | if (this.isRemoveStore === true) { 37 | TodoStore.remove(this.props.store); 38 | this.props.store = null; 39 | } 40 | } 41 | 42 | render() { 43 | return ( 44 |
45 | 70 | 71 | 72 | 73 |
74 | ) 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react";; 3 | import { Link } from 'react-router'; 4 | import { authStore } from '../stores/auth_store'; 5 | import TabBar from './tabbar/tabbar' 6 | import WidgetTool from './dashboard/widget_tool' 7 | import SearchTool from './dashboard/search_tool' 8 | import { FormattedMessage } from 'react-intl' 9 | 10 | @observer 11 | class Header extends Component { 12 | 13 | renderLinks() { 14 | if ( authStore.authenticated ) { 15 | // show a link to sign out 16 | return [ 17 | 18 | 19 | 20 | ]; 21 | } else { 22 | // show a link to sign in or sign up 23 | return [ 24 | 25 | 26 | , 27 | 28 | 29 | 30 | ]; 31 | } 32 | } 33 | 34 | renderTools() { 35 | if ( authStore.authenticated ) { 36 | return [ 37 | 38 | 39 | 40 | ]; 41 | } 42 | } 43 | 44 | renderSearchTools() { 45 | if ( authStore.authenticated ) { 46 | return [ 47 | 48 | 49 | 50 | ] 51 | } 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 |
58 |
59 |
:  60 |   61 | {authStore.name} 62 |
63 |
64 |
65 | {this.renderLinks()} 66 | {this.renderTools()} 67 |
68 |
69 | 70 |
71 | ); 72 | } 73 | } 74 | export default Header; 75 | -------------------------------------------------------------------------------- /src/components/main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { observer } from "mobx-react"; 4 | import { tabbarStore } from '../stores/tabbar_store' 5 | import Tab from './tabbar/tab' 6 | import Dashboard from '../components/dashboard/dashboard' 7 | 8 | @observer 9 | class Main extends Component { 10 | render() { 11 | return ( 12 |
13 | { tabbarStore.getStores().map( store => 14 | 15 | { store.component() } 16 | 17 | ) 18 | } 19 |
20 | ) 21 | } 22 | } 23 | 24 | export default Main; -------------------------------------------------------------------------------- /src/components/popup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react";; 3 | import { Link } from 'react-router'; 4 | import { popupStore } from '../stores/popup_store'; 5 | 6 | @observer 7 | class Popup extends Component { 8 | 9 | constructor() { 10 | super() 11 | this.id = 'rp-popup-dragme'+popupStore.getStores().length.toString() 12 | this.state = { mousex : window.innerWidth / 2, mousey: 100 } 13 | this.move = false 14 | this.mousex = 0 15 | this.mousey = 0 16 | this.mousedown = this.mousedown.bind(this) 17 | this.mouseup = this.mouseup.bind(this) 18 | this.mousemove = this.mousemove.bind(this) 19 | this.init = this.init.bind(this) 20 | } 21 | 22 | init() { 23 | var d = document.getElementById(this.id); 24 | d.onmousedown = this.mousedown; 25 | d.onmouseup = this.mouseup; 26 | d.onmousemove = this.mousemove; 27 | } 28 | 29 | isMouseCoordinateInsideHeader(e) { 30 | var targ; 31 | if (!e) var e = window.event; 32 | if (e.target) targ = e.target; 33 | else if (e.srcElement) targ = e.srcElement; 34 | if (targ.nodeType == 3) {// defeat Safari bug 35 | targ = targ.parentNode; 36 | } 37 | return targ.className.includes('rp-popup-header') 38 | } 39 | 40 | mousedown(e) { 41 | if (!e) var e = window.event; 42 | 43 | if (!this.isMouseCoordinateInsideHeader(e)) { return } 44 | 45 | document.getElementById(this.id).style.color = 'red'; 46 | this.move = true; 47 | this.mousex = e.clientX; 48 | this.mousey = e.clientY; 49 | } 50 | 51 | mouseup(e) { 52 | if (this.move) { 53 | if (!e) var e = window.event; 54 | document.getElementById(this.id).style.color = 'black'; 55 | this.move = false; 56 | } 57 | } 58 | 59 | mousemove(e) { 60 | if (this.move) { 61 | if (!e) var e = window.event; 62 | this.ldivx = parseInt(this.ldivx) + parseInt(e.clientX) - parseInt(this.mousex); 63 | this.ldivy = parseInt(this.ldivy) + parseInt(e.clientY) - parseInt(this.mousey); 64 | this.mousex = e.clientX; 65 | this.mousey = e.clientY; 66 | var d = document.getElementById(this.id); 67 | d.style.left = parseInt(this.ldivx) + 'px'; 68 | d.style.top = parseInt(this.ldivy) + 'px'; 69 | this.setState=( { mousex: this.mousex, mousey: this.mousey } ) 70 | } 71 | } 72 | 73 | componentDidMount() { 74 | this.init() 75 | this.ldivx = this.state.mousex 76 | this.ldivy= this.state.mousey 77 | } 78 | 79 | render() { 80 | return ( 81 |
82 |
86 | {this.props.children} 87 |
88 |
89 | ) 90 | } 91 | } 92 | export default Popup; 93 | -------------------------------------------------------------------------------- /src/components/popups/popup_yesno.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { popupClose } from '../../actions/popup_actions' 3 | import { dispatch } from '../../helpers/dispatcher' 4 | import { FormattedMessage } from 'react-intl' 5 | 6 | export default class PopupYesNo extends Component { 7 | render() { 8 | return ( 9 |
10 |
{ this.props.title }
11 |
{ this.props.msg }
12 |
13 | 17 | 18 | 24 |
25 |
26 | ) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/components/tabbar/tab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer } from "mobx-react" 3 | 4 | @observer 5 | class Tab extends Component { 6 | render() { 7 | return ( 8 |
10 | {this.props.children} 11 |
12 | ) 13 | } 14 | } 15 | export default Tab; 16 | -------------------------------------------------------------------------------- /src/components/tabbar/tabbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react";; 3 | import { Link } from 'react-router'; 4 | import { tabbarStore } from '../../stores/tabbar_store'; 5 | import { dispatch } from '../../helpers/dispatcher'; 6 | import { tabbarClose, tabbarSelect } from '../../actions/tabbar_actions'; 7 | 8 | @observer 9 | class TabBarItem extends Component { 10 | constructor() { 11 | super() 12 | this.handleOnClick = this.handleOnClick.bind(this) 13 | this.handleOnClose = this.handleOnClose.bind(this) 14 | } 15 | 16 | handleOnClick(event) { 17 | if (event.target.value == null) { return } 18 | dispatch(tabbarSelect(this.props.componentId)) 19 | } 20 | 21 | handleOnClose() { 22 | dispatch(tabbarClose(this.props.componentId)) 23 | } 24 | 25 | renderClose(visible){ 26 | if (visible === true ){ 27 | return ( 28 | 30 | ) 31 | } 32 | } 33 | 34 | render() { 35 | return ( 36 |
  • 37 | {this.props.title} 38 | { this.renderClose(this.props.visible) } 39 |
  • 40 | ) 41 | } 42 | } 43 | 44 | @observer 45 | class TabBar extends Component { 46 | render() { 47 | const stores = tabbarStore.getStores() 48 | const count = stores.length - 1 49 | return ( 50 |
    51 |
      52 | { stores.map( (store, idx) => 53 | 54 | ) 55 | } 56 |
    57 |
    58 | ) 59 | } 60 | } 61 | export default TabBar; 62 | -------------------------------------------------------------------------------- /src/components/todo/todo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react"; 3 | import { todoDone } from '../../actions/todo_actions'; 4 | import { pageDeleteRecord } from '../../actions/page_actions'; 5 | import { popupShow } from '../../actions/popup_actions'; 6 | import { dispatch } from '../../helpers/dispatcher'; 7 | import TodoView from './todo_view'; 8 | import TodoStore from '../../stores/todo_store'; 9 | import TodoModel from '../../models/todo_model' 10 | import { todoForm } from '../../forms/todo_form' 11 | import { FormattedMessage } from 'react-intl' 12 | 13 | @observer // need observer to update a row when a note is modified 14 | class Todo extends Component { 15 | constructor() { 16 | super(); 17 | this.handleDelete = this.handleDelete.bind(this); 18 | this.handleEdit = this.handleEdit.bind(this); 19 | this.handleDone = this.handleDone.bind(this); 20 | } 21 | 22 | static propTypes = { 23 | store: React.PropTypes.instanceOf(TodoStore), 24 | todo: React.PropTypes.shape(TodoModel.shape()) 25 | } 26 | 27 | handleDone(todo) { 28 | dispatch(todoDone(this.props.store, todo)); 29 | } 30 | 31 | handleDelete(todo) { 32 | dispatch(pageDeleteRecord(this.props.store, todo)); 33 | } 34 | 35 | handleEdit(todo) { 36 | var component = () => 37 | dispatch(popupShow(component, TodoStore.getEditFormDimension())); 38 | } 39 | 40 | getTodoDoneClass(todo) { 41 | if (todo.done) { 42 | return { textDecoration: "line-through", color: 'lightgray' } 43 | } else { 44 | return { textDecoration: "none", color : 'black'} 45 | } 46 | } 47 | 48 | render() { 49 | const todo = this.props.todo; 50 | return ( 51 | 52 | this.handleEdit(todo)} className="fa fa-edit"/> 53 | this.handleDone(todo)}>{todo.description} 54 | this.handleDone(todo)}> 55 | 56 | this.handleDelete(todo)} className="fa fa-trash"/> 57 | 58 | ); 59 | } 60 | }; 61 | export default Todo; -------------------------------------------------------------------------------- /src/components/todo/todo_view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer } from "mobx-react" 3 | import TodoStore from '../../stores/todo_store' 4 | import { pageUpdateRecord, pageAddRecord } from '../../actions/page_actions' 5 | import { popupClose } from '../../actions/popup_actions' 6 | import { dispatch } from '../../helpers/dispatcher' 7 | import TodoModel from '../../models/todo_model' 8 | import Form from '../../forms/form' 9 | import { FormattedMessage, injectIntl } from 'react-intl' 10 | 11 | @observer 12 | class TodoView extends Component { 13 | async handleSend(event,form) { 14 | event.preventDefault() 15 | await form.validate(); 16 | if (form.valid) { 17 | var todo = this.props.todo 18 | TodoModel.setTodoModel(form.fields, todo) 19 | if (todo._id) { 20 | dispatch(pageUpdateRecord(this.props.store, todo)) 21 | } else { 22 | dispatch(pageAddRecord(this.props.store, todo)) 23 | } 24 | dispatch(popupClose()) 25 | } 26 | } 27 | 28 | static propTypes = { 29 | form: React.PropTypes.instanceOf(Form), 30 | store: React.PropTypes.instanceOf(TodoStore), 31 | todo: React.PropTypes.shape(TodoModel.shape()) 32 | } 33 | 34 | componentWillMount() { 35 | TodoModel.setFormFields(this.props.todo, this.props.form.fields) 36 | } 37 | 38 | componentDidMount() { 39 | this.refs.nameInput.focus(); 40 | } 41 | 42 | render() { 43 | const form = this.props.form 44 | const { formatMessage } = this.props.intl; 45 | return ( 46 |
    47 |
    Todo
    48 |
    49 |   50 | form.fields.description.value = e.target.value}/> 54 | { form.renderError(form.fields.description.errorMessage) } 55 |
    56 |
    57 | 58 | 66 | { form.renderError(form.fields.status.errorMessage) } 67 |
    68 |
    69 | 71 | 73 |
    74 | {form.renderAlert(this.props.store.error)} 75 |
    76 | ); 77 | } 78 | } 79 | export default injectIntl(TodoView) 80 | 81 | -------------------------------------------------------------------------------- /src/components/todo/todos.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer } from "mobx-react" 3 | import { pageGetAll, pagePrevious, pageNext } from '../../actions/page_actions' 4 | import { popupShow } from '../../actions/popup_actions' 5 | import { dispatch } from '../../helpers/dispatcher' 6 | import { todoForm } from '../../forms/todo_form' 7 | import Todo from './todo' 8 | import TodoStore from '../../stores/todo_store' 9 | import TodoView from './todo_view' 10 | import TodoModel from '../../models/todo_model' 11 | import { FormattedMessage } from 'react-intl' 12 | 13 | @observer // need observer when we add, delete rows 14 | class Todos extends Component { 15 | constructor() { 16 | super() 17 | this.handleAdd = this.handleAdd.bind(this) 18 | } 19 | 20 | static propTypes = { 21 | store: React.PropTypes.instanceOf(TodoStore), 22 | isRemoveStore : React.PropTypes.bool // true means that the store must be delete here 23 | // false means that the store will be deleted by the parent 24 | } 25 | 26 | componentWillMount() { 27 | dispatch(pageGetAll(this.props.store)) 28 | } 29 | 30 | componentWillUnmount() { 31 | if (this.props.isRemoveStore === true) { 32 | TodoStore.remove(this.props.store) 33 | } 34 | } 35 | 36 | handleAdd() { 37 | var component = () => 38 | dispatch(popupShow(component, TodoStore.getEditFormDimension())) 39 | } 40 | 41 | render() { 42 | const store = this.props.store 43 | return ( 44 |
    45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {/* note: always need a key */} 53 | 54 | { store.page.map( todo => 55 | 56 | ) 57 | } 58 | 59 |
    60 | 61 | 62 | 63 |
    64 | ) 65 | } 66 | }; 67 | export default Todos 68 | 69 | -------------------------------------------------------------------------------- /src/components/user/user.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer } from "mobx-react" 3 | import * as actions from '../../actions/user_actions' 4 | import { dispatch } from '../../helpers/dispatcher' 5 | import UserStore from '../../stores/user_store' 6 | import UserModel from '../../models/user_model' 7 | 8 | @observer // need observer to update a row when a note is modified 9 | export default class User extends Component { 10 | constructor() { 11 | super() 12 | this.handleDelete = this.handleDelete.bind(this) 13 | } 14 | 15 | static propTypes = { 16 | store: React.PropTypes.instanceOf(UserStore), 17 | user: React.PropTypes.shape(UserModel.shape()) 18 | } 19 | 20 | handleDelete(user) { 21 | dispatch(actions.userDelete(this.props.store, user)) 22 | } 23 | 24 | render() { 25 | const user = this.props.user; 26 | return ( 27 | 28 | {user.name} 29 | {user.email} 30 | {user._id} 31 | this.handleDelete(user)} className="fa fa-trash"/> 32 | 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/user/users.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react"; 3 | import UserStore from '../../stores/user_store'; 4 | import { pageGetAll, pagePrevious, pageNext } from '../../actions/page_actions'; 5 | import User from './user'; 6 | import { dispatch } from '../../helpers/dispatcher'; 7 | import { FormattedMessage } from 'react-intl' 8 | 9 | @observer // need observer when we add, delete rows 10 | export default class Users extends Component { 11 | constructor() { 12 | super(); 13 | } 14 | 15 | static propTypes = { 16 | store: React.PropTypes.instanceOf(UserStore), 17 | isRemoveStore : React.PropTypes.bool // true means that the store must be delete here 18 | // false means that the store will be deleted by the parent 19 | } 20 | 21 | componentWillMount() { 22 | dispatch(pageGetAll(this.props.store)); 23 | } 24 | 25 | componentWillUnmount() { 26 | if (this.props.isRemoveStore === true) { 27 | UserStore.remove(this.props.store) 28 | } 29 | } 30 | 31 | render() { 32 | const store = this.props.store 33 | return ( 34 |
    35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {/* note: always need a key */} 43 | 44 | { store.page.map( user => 45 | 46 | ) 47 | } 48 | 49 |
    50 | 51 | 52 |
    53 | ) 54 | } 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/components/welcome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl' 3 | 4 | export default () =>
    ; 5 | -------------------------------------------------------------------------------- /src/components/widgets/aggrid_widget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { dispatch } from '../../helpers/dispatcher' 3 | import { dashboardRemoveWidget } from '../../actions/dashboard_actions' 4 | import { tabbarShow } from '../../actions/tabbar_actions' 5 | import { pageGetAll } from '../../actions/page_actions' 6 | import Widget from '../dashboard/widget' 7 | import AgGrid from '../grid/ag_grid' 8 | import TodoModel from '../../models/todo_model' 9 | import TodoStore from '../../stores/todo_store' 10 | 11 | export default class AgGridWidget extends Component { 12 | componentWillMount() { 13 | this.store = TodoStore.create(); 14 | } 15 | 16 | componentWillUnmount() { 17 | TodoStore.remove(this.store) 18 | this.store = null; 19 | } 20 | 21 | render() { 22 | var component = () => 23 | return ( 24 | dispatch(tabbarShow(component,'aggrid',"AgGrid", 'page'))} 26 | onRemoveWidget={ () => dispatch(dashboardRemoveWidget(this.props.dashboardId, this.props.id))} > 27 | 28 | 29 | ) 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/components/widgets/linechart_widget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { dispatch } from '../../helpers/dispatcher' 3 | import { dashboardRemoveWidget } from '../../actions/dashboard_actions' 4 | import { tabbarShow } from '../../actions/tabbar_actions' 5 | import Widget from '../dashboard/widget' 6 | import LineChart from '../charts/line_chart' 7 | 8 | export default class LineChartWidget extends Component { 9 | render() { 10 | var component = () => 11 | return ( 12 | dispatch(tabbarShow(component,'linechart',"LineChart", 'page'))} 14 | onRemoveWidget={ () => dispatch(dashboardRemoveWidget(this.props.dashboardId, this.props.id))} > 15 | 16 | 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/widgets/todos_widget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Todos from '../todo/todos' 3 | import { dispatch } from '../../helpers/dispatcher' 4 | import { dashboardRemoveWidget } from '../../actions/dashboard_actions' 5 | import { tabbarShow } from '../../actions/tabbar_actions' 6 | import { pageGetAll } from '../../actions/page_actions' 7 | import Widget from '../dashboard/widget' 8 | import TodoModel from '../../models/todo_model' 9 | import TodoStore from '../../stores/todo_store' 10 | import { FormattedMessage } from 'react-intl' 11 | 12 | class TodosWidget extends Component { 13 | 14 | componentWillMount() { 15 | this.store = TodoStore.create(); 16 | } 17 | 18 | componentWillUnmount() { 19 | TodoStore.remove(this.store) 20 | this.store = null; 21 | } 22 | 23 | render() { 24 | var component = () => 25 | return ( 26 | } 27 | onOpenWidgetInTab={() => dispatch(tabbarShow(component,'todo',"Todo", 'page'))} 28 | onRemoveWidget={ () => dispatch(dashboardRemoveWidget(this.props.dashboardId, this.props.id))} 29 | onRefreshWidget= {() => dispatch(pageGetAll("todo"))} > 30 | 31 | 32 | ) 33 | } 34 | } 35 | export default TodosWidget 36 | -------------------------------------------------------------------------------- /src/components/widgets/users_widget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Users from '../user/users' 3 | import { dispatch } from '../../helpers/dispatcher' 4 | import { tabbarShow } from '../../actions/tabbar_actions' 5 | import { dashboardRemoveWidget } from '../../actions/dashboard_actions' 6 | import { pageGetAll } from '../../actions/page_actions' 7 | import Widget from '../dashboard/widget' 8 | import UserStore from '../../stores/user_store'; 9 | import { FormattedMessage } from 'react-intl' 10 | 11 | export default class UsersWidget extends Component { 12 | componentWillMount() { 13 | this.store = UserStore.create(); 14 | } 15 | 16 | componentWillUnmount() { 17 | UserStore.remove(this.store) 18 | this.store = null; 19 | } 20 | 21 | render() { 22 | var component = () => 23 | return ( 24 | } 25 | onOpenWidgetInTab={ () => dispatch(tabbarShow(component , "user", "Users", "page"))} 26 | onRemoveWidget={ () => dispatch(dashboardRemoveWidget(this.props.dashboardId, this.props.id))} 27 | onRefreshWidget= { () => dispatch(pageGetAll("user"))} > 28 | 29 | 30 | ) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/forms/form.js: -------------------------------------------------------------------------------- 1 | // from the Michel Weststrate code 2 | require("babel-polyfill") 3 | import { observable, computed, extendObservable, autorunAsync } from 'mobx' 4 | import { observer } from 'mobx-react' 5 | import React, { Component } from 'react' 6 | 7 | class Field { 8 | @observable _value; 9 | @observable _interacted; 10 | @observable _valid = true; 11 | @observable errorMessage; 12 | isRequired = false; 13 | _originalErrorMessage; 14 | 15 | markAsTouch() { 16 | if (!this._touched) { 17 | this._touched = true; 18 | } 19 | } 20 | 21 | @computed get valid() { 22 | return this._valid; 23 | } 24 | 25 | get value() { 26 | return this._value; 27 | } 28 | 29 | set value(val) { 30 | if (!this._interacted) { 31 | this._interacted = true; 32 | } 33 | this._value = val; 34 | 35 | this.validate(); 36 | } 37 | 38 | init(val) { 39 | this._value = val; 40 | } 41 | 42 | validate(force = false) { 43 | if (!this._validateFn) { 44 | return; 45 | } 46 | 47 | if (!force && !this._interacted) { 48 | // if we're not forcing the validation 49 | // and we haven't interacted with the field 50 | // we asume this field pass the validation status 51 | this._valid = true; 52 | this.errorMessage = ''; 53 | return; 54 | } 55 | const res = this._validateFn(this, this.model.fields); 56 | 57 | // if the function returned a boolean we assume it is 58 | // the flag for the valid state 59 | if (typeof res === 'boolean') { 60 | this._valid = res; 61 | this.errorMessage = res ? '' : this._originalErrorMessage; 62 | return; 63 | } 64 | 65 | // otherwise we asumme we have received a promise 66 | const p = Promise.resolve(res); 67 | return new Promise((resolve) => { // eslint-disable-line consistent-return 68 | p.then( 69 | () => { 70 | this._valid = true; 71 | this.errorMessage = ''; 72 | resolve(); // we use this to chain validators 73 | }, 74 | ({ error } = {}) => { 75 | this.errorMessage = (error || '').trim() || this._originalErrorMessage; 76 | this._valid = false; 77 | resolve(); // we use this to chain validators 78 | }); 79 | }); 80 | } 81 | 82 | constructor(model, value, validatorDescriptor = {}) { 83 | this.model = model; 84 | this.value = value; 85 | this._originalErrorMessage = validatorDescriptor.errorMessage; 86 | this._validateFn = validatorDescriptor.fn || (() => Promise.resolve()); 87 | } 88 | } 89 | 90 | export default class Form { 91 | @observable fields = {}; 92 | @observable validating = false; 93 | @computed get valid() { 94 | if (this.validating) { 95 | return false; // consider the form invalid until the validation process finish 96 | } 97 | const keys = Object.keys(this.fields); 98 | return keys.reduce((seq, key) => { 99 | const field = this.fields[key]; 100 | seq = seq && field.valid; 101 | return seq; 102 | }, true); 103 | } 104 | 105 | fieldKeys() { 106 | return Object.keys(this.fields); 107 | } 108 | 109 | validate() { 110 | this.validating = true; 111 | const p = this.fieldKeys().reduce((seq, key) => { 112 | const field = this.fields[key]; 113 | return seq.then(() => field.validate(true)); 114 | }, Promise.resolve()); 115 | p.then(() => (this.validating = false)); 116 | return p 117 | } 118 | 119 | toJSON() { 120 | const keys = Object.keys(this.fields); 121 | return keys.reduce((seq, key) => { 122 | const field = this.fields[key]; 123 | seq[key] = field.value; 124 | return seq; 125 | }, {}); 126 | } 127 | 128 | // standard to show backend error of the form 129 | renderAlert(message) { 130 | if (message) { 131 | return ( 132 |
    133 | Oops! {message} 134 |
    135 | ); 136 | } 137 | } 138 | 139 | // standard to show validation error of the form 140 | renderError(error) { 141 | if (error && error != '' ) { 142 | return ( 143 | 144 | ) 145 | } 146 | } 147 | 148 | constructor(initialState = {}, validators = {}) { 149 | const keys = Object.keys(initialState); 150 | 151 | keys.forEach((key) => { 152 | extendObservable(this.fields, { 153 | [key]: new Field(this, initialState[key], validators[key]) 154 | }); 155 | }); 156 | 157 | autorunAsync(() => { 158 | this.onChange && this.onChange(this.valid, this.toJSON()); 159 | }, 100); 160 | } 161 | } -------------------------------------------------------------------------------- /src/forms/signin_form.js: -------------------------------------------------------------------------------- 1 | import Form from './form' 2 | 3 | export let signinForm = new Form( 4 | { 5 | email: '', 6 | password: '' 7 | }, 8 | { 9 | email: { 10 | errorMessage: 'Required!', 11 | fn: (field, fields) => { 12 | return (field.value || '').length > 0; 13 | } 14 | }, 15 | password: { 16 | // this is the validation 17 | fn: (field, fields) => { 18 | return new Promise((resolve, reject) => { 19 | // simulate server validation 20 | setTimeout(() => { 21 | if ((field.value || '').length === 0) { 22 | reject({ error: 'Required!'}); 23 | return; 24 | } 25 | if (field.value === fields.email.value) { 26 | reject({ error: 'Password cannot be the same as the email'}); 27 | return; 28 | } 29 | resolve(); 30 | }, 2); 31 | }); 32 | } 33 | } 34 | }) 35 | 36 | -------------------------------------------------------------------------------- /src/forms/signup_form.js: -------------------------------------------------------------------------------- 1 | import Form from './form' 2 | 3 | export let signupForm = new Form( 4 | { 5 | email: '', 6 | name: '', 7 | password: '', 8 | passwordConfirm : '' 9 | } 10 | , 11 | { 12 | email: { 13 | errorMessage: 'Required!', 14 | fn: (field, fields) => { 15 | return (field.value || '').length > 0; 16 | } 17 | }, 18 | name: { 19 | errorMessage: 'Required!', 20 | fn: (field, fields) => { 21 | return (field.value || '').length > 0; 22 | } 23 | }, 24 | password: { 25 | fn: (field, fields) => { 26 | return new Promise((resolve, reject) => { 27 | if ((field.value || '').length === 0) { 28 | reject({ error: 'Required!'}); 29 | return; 30 | } 31 | if (field.value === fields.email.value) { 32 | reject({ error: 'Password cannot be the same as the email'}); 33 | return; 34 | } 35 | resolve(); 36 | }); 37 | } 38 | }, 39 | passwordConfirm : { 40 | errorMessage: 'Both passwords are not equal!', 41 | fn: (field, fields) => { 42 | return new Promise((resolve, reject) => { 43 | if ((field.value || '').length === 0) { 44 | reject({ error: 'Required!'}); 45 | return; 46 | } 47 | if (field.value !== fields.password.value) { 48 | reject({ error: 'Both passwords are not equal!'}); 49 | return; 50 | } 51 | resolve(); 52 | }); 53 | } 54 | } 55 | } 56 | ) 57 | 58 | -------------------------------------------------------------------------------- /src/forms/todo_form.js: -------------------------------------------------------------------------------- 1 | import Form from './form' 2 | 3 | export let todoForm = new Form( 4 | { 5 | description: '', 6 | status: 'waiting' 7 | } 8 | , 9 | { 10 | description: { 11 | errorMessage: 'Required!', 12 | fn: (field, fields) => { 13 | return (field.value || '').length > 0; 14 | } 15 | }, 16 | status: { 17 | errorMessage: 'Required!', 18 | fn: (field, fields) => { 19 | return (field.value || '').length > 0; 20 | } 21 | } 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /src/helpers/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { thunkResolver } from '../resolvers/thunk_resolver'; 2 | import { loggerResolver } from '../resolvers/logger_resolver'; 3 | import { authorizationResolver } from '../resolvers/authorization_resolver'; 4 | import { popupResolver } from '../resolvers/popup_resolver'; 5 | import { tabbarResolver } from '../resolvers/tabbar_resolver'; 6 | 7 | import authResolver from '../resolvers/auth_resolver'; 8 | import userResolver from '../resolvers/user_resolver'; 9 | import todoResolver from '../resolvers/todo_resolver'; 10 | import messageResolver from '../resolvers/message_resolver'; 11 | import dashboardResolver from '../resolvers/dashboard_resolver'; 12 | import testResolver from '../resolvers/test_resolver'; 13 | 14 | /** 15 | * We have 3 type of resolvers 16 | * pre : pre resolver to manage the action before a fonctionnal resolver do something with it 17 | * std : std resolver are standard resolver 18 | * post : post resolver are resolvers that run after all others 19 | * 20 | * We use pre filled dictionnaries of resolvers by action type 21 | * because we want to be sure that only resolvers that want to do 22 | * something with the action will going to be called. 23 | * 24 | * like that the dispatcher is scalable. 25 | * 26 | */ 27 | class Dispatcher { 28 | constructor() { 29 | this.postResolvers = {} 30 | this.preResolvers = {} 31 | this.stdResolvers = {} 32 | 33 | this.postResolversAll = [] 34 | this.preResolversAll = [] 35 | this.stdResolversAll = [] 36 | } 37 | 38 | addResolver( allDict, dict, resolver ) { 39 | if (resolver.filter === "*") { 40 | allDict.push(resolver.fct) 41 | } else { 42 | const actionTypes = resolver.filter.split(',') 43 | actionTypes.forEach( (actionType) => { 44 | if (!dict[actionType]) { 45 | dict[actionType] = [] 46 | } 47 | dict[actionType].push(resolver.fct) 48 | }) 49 | } 50 | } 51 | 52 | addPreResolver(resolver) { 53 | this.addResolver(this.preResolversAll, this.preResolvers, resolver) 54 | } 55 | 56 | addPostResolver(resolver) { 57 | this.addResolver(this.postResolversAll, this.postResolvers, resolver) 58 | } 59 | 60 | addStdResolver(resolver) { 61 | this.addResolver(this.stdResolversAll, this.stdResolvers, resolver) 62 | } 63 | 64 | next(err, result) { 65 | if (err) { 66 | console.log("Error:",err) 67 | return null 68 | } else { 69 | return result 70 | } 71 | } 72 | 73 | dispatch(action) { 74 | const action_type = action.type.substring(0, action.type.indexOf("_")); 75 | 76 | var all = [ ...this.preResolversAll, 77 | ...this.preResolvers[action_type] || [], 78 | ...this.stdResolversAll, 79 | ...this.stdResolvers[action_type] || [], 80 | ...this.postResolversAll, 81 | ...this.postResolvers[action_type] || [] ] 82 | 83 | for(let resolver of all) { 84 | action = resolver(action, this.next); 85 | if (!action) return; 86 | } 87 | } 88 | } 89 | 90 | 91 | 92 | export let dispatcher = new Dispatcher(); 93 | // logger first 94 | dispatcher.addPreResolver( { fct: loggerResolver, filter: "*" }) 95 | // Authorization second 96 | dispatcher.addPreResolver( { fct: authorizationResolver, filter: "*" }) 97 | //thunk third 98 | dispatcher.addPreResolver( { fct: thunkResolver, filter: "*" }) 99 | 100 | // no special order for those 101 | dispatcher.addStdResolver( { fct: popupResolver, filter: "popup" }) 102 | dispatcher.addStdResolver( { fct: tabbarResolver, filter: "tabbar,dashboard" }) 103 | dispatcher.addStdResolver( { fct: dashboardResolver, filter: "tabbar,dashboard" }) 104 | dispatcher.addStdResolver( { fct: authResolver, filter: "auth" }) 105 | dispatcher.addStdResolver( { fct: messageResolver, filter: "message" }) 106 | dispatcher.addStdResolver( { fct: userResolver, filter: "user" }) 107 | dispatcher.addStdResolver( { fct: todoResolver, filter: "todo" }) 108 | 109 | // logger last, post log. we set the test resolver for unit test 110 | // dispatcher.addPostResolver( { fct: loggerResolver, filter: "*" }) 111 | dispatcher.addPostResolver( { fct: testResolver, filter: "*" }) 112 | 113 | export const dispatch = dispatcher.dispatch.bind(dispatcher) 114 | 115 | -------------------------------------------------------------------------------- /src/helpers/intl.js: -------------------------------------------------------------------------------- 1 | export let qs = (function(a) { 2 | if (a == "") return {}; 3 | var b = {}; 4 | for (var i = 0; i < a.length; ++i) 5 | { 6 | var p=a[i].split('=', 2); 7 | if (p.length == 1) 8 | b[p[0]] = ""; 9 | else 10 | b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " ")); 11 | } 12 | return b; 13 | })(window.location.search.substr(1).split('&')); 14 | 15 | -------------------------------------------------------------------------------- /src/helpers/simulate_event.js: -------------------------------------------------------------------------------- 1 | import { pageNext } from '../actions/page_actions'; 2 | import { registerStore } from '../stores/register_store'; 3 | import { dispatch } from '../helpers/dispatcher'; 4 | 5 | 6 | class SimulateEvent { 7 | setIntervalX(callback, delay, repetitions) { 8 | var x = 0; 9 | var intervalID = window.setInterval(function () { 10 | callback(); 11 | if (++x === repetitions) { 12 | window.clearInterval(intervalID); 13 | } 14 | }, delay); 15 | } 16 | 17 | simulateNextPage() { 18 | this.setIntervalX( () => { 19 | // do it for all 'user' stores 20 | const stores = registerStore.getAll('user'); 21 | stores.forEach( (store) => 22 | dispatch( pageeNext(store)) ) 23 | }, 2000, 5); 24 | } 25 | } 26 | export let simulateEvent = new SimulateEvent(); 27 | -------------------------------------------------------------------------------- /src/helpers/string.js: -------------------------------------------------------------------------------- 1 | if(typeof String.prototype.startsWith != 'function'){ 2 | String.prototype.startsWith = function(str){ 3 | if(str == null) return false; 4 | var i = str.length; 5 | if(this.length < i) return false; 6 | for(--i; (i >= 0) && (this[i] === str[i]); --i) continue; 7 | return i < 0; 8 | } 9 | } 10 | 11 | if (!String.prototype.endsWith) { 12 | String.prototype.endsWith = function(searchString, position) { 13 | var subjectString = this.toString(); 14 | if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { 15 | position = subjectString.length; 16 | } 17 | position -= searchString.length; 18 | var lastIndex = subjectString.indexOf(searchString, position); 19 | return lastIndex !== -1 && lastIndex === position; 20 | }; 21 | } 22 | 23 | if (typeof String.prototype.splitEqual != 'function') { 24 | String.prototype.splitEqual = function(actiontype, sep) { 25 | var res = this.split(sep) 26 | for(let f of res) { 27 | if (f === "*" || f == actiontype) { 28 | return true 29 | } 30 | } 31 | return false 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/helpers/test.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = {}; 4 | const defaultProps = {}; 5 | 6 | class Test extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | return ( 13 |
    14 | ); 15 | } 16 | } 17 | 18 | Test.propTypes = propTypes; 19 | Test.defaultProps = defaultProps; 20 | 21 | export default Test; -------------------------------------------------------------------------------- /src/helpers/translation.js: -------------------------------------------------------------------------------- 1 | export const app = { 2 | // output by server conditional 3 | 'fr': { 4 | 'welcome.welcome' : 'Bienvenue au Portail React', 5 | 'app.portal' : 'Portail React', 6 | 'app.welcome' : 'Bienvenue', 7 | 'app.signin' : 'Se connecter', 8 | 'app.signup' : "S'inscrire", 9 | 'app.signout' : 'Se déconnecter', 10 | 'app.sorry' : 'Désolé de vous voir partir..', 11 | 'db.dashboard' : 'Tableau de bord', 12 | 'db.create' : 'Créer un nouveau tableau de bord', 13 | 'db.rename' : 'Renommer le tableau de bord courant', 14 | 'db.delete' : 'Effacer le tableau de bord courant', 15 | 'db.askdelete' : 'Voulez-vous effacer le tableau de bord courant : {name} ?', 16 | 'db.showhide' : 'Afficher/cacher des tableaux de bord', 17 | 'db.addwidgets' : 'Ajouter des widgets au tableau de bord courant', 18 | 'db.nowidget' : 'Aucun widget', 19 | 'db.btnrename' : 'Renommer', 20 | 'db.btncreate' : 'Créer', 21 | 'db.name' : 'Nom du tableau de bord', 22 | 'form.close' : 'Quitter', 23 | 'form.ok' : 'OK', 24 | 'form.yes' : 'Oui', 25 | 'form.no' : 'Non', 26 | 'form.id' : 'Id', 27 | 'form.del' : 'EFF', 28 | 'form.next' : 'prochain', 29 | 'form.prev' : 'précédent', 30 | 'form.add' : 'ajouter', 31 | 'form.cancel' : 'Annuler', 32 | 'form.save' : 'Enregister', 33 | 'auth.email' : 'courriel', 34 | 'auth.password' : 'mot de passe', 35 | 'auth.confirm' : 'confirmer mot passe', 36 | 'auth.name' : 'nom', 37 | 'todo.description' : 'Description', 38 | 'todo.status' : 'Statut', 39 | 'todo.waiting' : 'En attente', 40 | 'todo.suspended' : 'Suspendue', 41 | 'todo.freezed' : 'Gelée', 42 | 'todo.completed' : 'Complétée', 43 | 'todo.mytodo' : 'Mes Todos', 44 | 'user.name' : 'Nom', 45 | 'user.desc' : 'Desc', 46 | 'user.userlist' : 'Liste des utilisateurs' 47 | }, 48 | 'en': { 49 | 'welcome.welcome' : 'Welcome to React Portal', 50 | 'app.portal' : 'React Portal', 51 | 'app.welcome' : 'Welcome', 52 | 'app.sorry' : 'Sorry to see you go..', 53 | 'app.signin' : 'Signin', 54 | 'app.signup' : 'Signup', 55 | 'app.signout' : 'Signout', 56 | 'db.dashboard' : 'Dashboard', 57 | 'db.create' : 'Create a new dashboard', 58 | 'db.rename' : 'Rename the current dasboard', 59 | 'db.delete' : 'Delete the current dashboard', 60 | 'db.askdelete' : 'Do you want to delete the current dashboard : {name} ?', 61 | 'db.showhide' : 'Show/Hide dashboards', 62 | 'db.addwidgets' : 'Add widgets to the current dasboard', 63 | 'db.nowidget' : 'No widget', 64 | 'db.btnrename' : 'Rename', 65 | 'db.btncreate' : 'Create', 66 | 'db.name' : 'Dashboard name', 67 | 'form.createdb' : 'Create a new dashboard', 68 | 'form.renamedb' : 'Rename the current dRenommer le tableau de bord courant', 69 | 'form.close' : 'Close', 70 | 'form.ok' : 'OK', 71 | 'form.yes' : 'Yes', 72 | 'form.no' : 'No', 73 | 'form.id' : 'Id', 74 | 'form.del' : 'DEL', 75 | 'form.next' : 'next', 76 | 'form.prev' : 'prev', 77 | 'form.add' : 'add', 78 | 'form.cancel' : 'Cancel', 79 | 'form.save' : 'Save', 80 | 'auth.email' : 'email', 81 | 'auth.password' : 'password', 82 | 'auth.confirm' : 'confirm password', 83 | 'auth.name' : 'name', 84 | 'todo.description' : 'Description', 85 | 'todo.status' : 'Status', 86 | 'todo.waiting' : 'Waiting', 87 | 'todo.suspended' : 'Suspended', 88 | 'todo.freezed' : 'Freezed', 89 | 'todo.completed' : 'Completed', 90 | 'todo.mytodo' : 'My Todos', 91 | 'user.name' : 'Name', 92 | 'user.desc' : 'Desc', 93 | 'user.userlist' : 'Users List' 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { addLocaleData, IntlProvider } from 'react-intl' 4 | import { Router, Route, IndexRoute, browserHistory } from 'react-router' 5 | import { authCheckToken, authSetActions } from './actions/auth_actions' 6 | import App from './components/app' 7 | import Dashboard from './components/dashboard/dashboard' 8 | import Main from './components/main' 9 | import Signin from './components/auth/signin' 10 | import Signout from './components/auth/signout' 11 | import Signup from './components/auth/signup' 12 | import Feature from './components/feature' 13 | import Users from './components/user/users' 14 | import RequireAuth from './components/auth/require_auth' 15 | import Welcome from './components/welcome' 16 | import { simulateEvent} from './helpers/simulate_event' 17 | import { dispatch } from './helpers/dispatcher' 18 | 19 | import { authStore } from './stores/auth_store' 20 | import { signinForm } from './forms/signin_form' 21 | import { signupForm } from './forms/signup_form' 22 | import { app } from './helpers/translation' 23 | import { qs } from './helpers/intl' 24 | 25 | require("./helpers/translation.js") 26 | var locale = qs["locale"] 27 | if (!locale) { 28 | locale = window.navigator.userLanguage || window.navigator.language 29 | } 30 | var localePrefix = locale.slice(0, locale.indexOf('-')); 31 | if (localePrefix !== 'en' && localePrefix !== 'fr') { 32 | locale = 'en-US' 33 | localePrefix = 'en' 34 | } 35 | const defaultApp = app['en']; 36 | import en from 'react-intl/locale-data/en'; 37 | import fr from 'react-intl/locale-data/fr' 38 | addLocaleData([...en, ...fr]); 39 | 40 | // launch the web socket service client side 41 | //require("./services/wss_service"); 42 | require("./helpers/string.js") 43 | require("./styles/knacss.scss") 44 | require("../node_modules/react-grid-layout/css/styles.css") 45 | require("../node_modules/react-resizable/css/styles.css") 46 | require("../node_modules/ag-grid/dist/styles/ag-grid.css") 47 | require("../node_modules/ag-grid/dist/styles/theme-bootstrap.css") 48 | 49 | var mainComponentsToRender = function() { 50 | ReactDOM.render( 51 | 52 | 53 | 54 | 55 | 57 | {children} } /> 58 | 59 | 61 | {children} } /> 62 | 63 | 64 | 65 | 66 | 67 | 68 | , document.querySelector('#app')); 69 | } 70 | 71 | // check the token of current user 72 | // we must fetch the user autorizations before rendering 73 | // the main. It's for this reason that we pass the function 74 | // to render to the authCheckToken 75 | dispatch(authCheckToken(mainComponentsToRender)) 76 | -------------------------------------------------------------------------------- /src/models/dashboard_model.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class DashboardModel { 4 | 5 | static shape() { 6 | return { 7 | _id : React.PropTypes.string, 8 | title : React.PropTypes.string, 9 | isHidden : React.PropTypes.bool, 10 | widgets : React.PropTypes.array 11 | } 12 | } 13 | 14 | static create(title) { 15 | return { 16 | _id : null, 17 | title : title, 18 | isHidden : false, 19 | widgets : [] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/models/todo_model.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage, injectIntl } from 'react-intl' 3 | 4 | export default class TodoModel { 5 | static shape() { 6 | return { 7 | _id : React.PropTypes.string, 8 | description : React.PropTypes.string, 9 | status : React.PropTypes.string, 10 | done : React.PropTypes.bool 11 | } 12 | } 13 | 14 | static create() { 15 | return { 16 | _id : null, 17 | description : "", 18 | status : "waiting", 19 | done : false 20 | } 21 | } 22 | 23 | static setFormFields(todo, editTodo) { 24 | editTodo.description.init(todo.description) 25 | editTodo.status.init(todo.status) 26 | //editTodo.done.int(todo.done) 27 | } 28 | 29 | 30 | static setTodoModel(editTodo, todo ) { 31 | todo.description = editTodo.description.value 32 | todo.status = editTodo.status.value 33 | //todo.done =editTodo.done.value 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/models/user_model.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class UserModel { 4 | static shape() { 5 | return { 6 | _id : React.PropTypes.string, 7 | name : React.PropTypes.string, 8 | email : React.PropTypes.string 9 | } 10 | } 11 | 12 | static create() { 13 | return { 14 | _id : null, 15 | name : "", 16 | email : "" 17 | } 18 | } 19 | 20 | static setFormFields(user, editUser) { 21 | editUser.name.init(user.name) 22 | editUser.email.init(user.email) 23 | } 24 | 25 | 26 | static setUserModel(editUser, user ) { 27 | user.name = editUser.name.value 28 | user.email = editUser.email.value 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/resolvers/auth_resolver.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/auth_types'; 2 | import { authStore } from '../stores/auth_store'; 3 | import { authSetAuthorizations } from '../actions/auth_actions'; 4 | 5 | export default function(action, next) { 6 | switch(action.type) { 7 | case t.AUTH_SET_AUTHORIZATIONS: 8 | authStore.setAuthorizations(action.payload, action.render) 9 | break; 10 | case t.AUTH_CHECK_TOKEN: 11 | authStore.checkToken(action.render) 12 | break; 13 | case t.AUTH_SIGN_IN: 14 | case t.AUTH_SIGN_UP: 15 | authStore.signInOrUp(action.payload.token,action.payload.name) 16 | break; 17 | case t.AUTH_SIGN_OUT: 18 | authStore.signOut() 19 | break; 20 | case t.AUTH_ERROR: 21 | authStore.authError(action.payload, action.render) 22 | break; 23 | case t.AUTH_VALIDATE_SIGN_UP: 24 | store.validateSignUp(); 25 | break; 26 | } 27 | return next(null, action); 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/resolvers/authorization_resolver.js: -------------------------------------------------------------------------------- 1 | import { authStore } from '../stores/auth_store' 2 | 3 | export function authorizationResolver(action, next) { 4 | // need to verify if the user can exexcute the action 5 | // for the current project. The same validation must be 6 | // done in the backend. 7 | // Note, autorization are not saved into the localStorage 8 | // because we want to checked them more often 9 | // 10 | if ( action && action.type && !action.type.startsWith("auth_") && 11 | !authStore.isActionAvailable(action.type)) { 12 | return next("Access denied", action); 13 | } else { 14 | return next(null, action); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/resolvers/crud_resolver.js: -------------------------------------------------------------------------------- 1 | import { registerStore } from '../stores/register_store'; 2 | 3 | export function resolveCrudAction(action, next) { 4 | 5 | let store = action.store 6 | const idx = action.type.indexOf("_") 7 | const type = action.type.substring(idx+1) 8 | 9 | if (typeof store === "string") { 10 | // get the real store. it's supposed to have only on of this kind 11 | store = registerStore.getAll(store)[0]; 12 | } 13 | switch(type) { 14 | case 'add': 15 | // need to update all related stores 16 | store.add(action.payload) 17 | break; 18 | case 'delete': 19 | // need to update all related stores 20 | store.delete(action.payload) 21 | break; 22 | case 'update': 23 | // need to update all related stores 24 | store.update(action.payload) 25 | break; 26 | case 'get_all': 27 | // need to update all related stores 28 | store.setAll(action.payload) 29 | break; 30 | } 31 | } -------------------------------------------------------------------------------- /src/resolvers/dashboard_resolver.js: -------------------------------------------------------------------------------- 1 | import * as d from '../types/dashboard_types' 2 | import * as t from '../types/tabbar_types' 3 | 4 | import { resolveCrudAction } from './crud_resolver' 5 | import { dashboardStore } from '../stores/dashboard_store' 6 | 7 | export default function(action, next) { 8 | resolveCrudAction(action); 9 | 10 | switch(action.type) { 11 | case t.TABBAR_CLOSE: 12 | dashboardStore.hideDashboard(action.payload) 13 | break 14 | case d.DASHBOARD_GET_ALL: 15 | dashboardStore.showAllUserDashboard() 16 | break 17 | case d.DASHBOARD_REMOVE_WIDGET: 18 | dashboardStore.removeWidget(action.payload.dashboardId, action.payload.widgetId) 19 | break 20 | case d.DASHBOARD_ADD_WIDGET: 21 | dashboardStore.addWidget(action.payload) 22 | break 23 | case d.DASHBOARD_ADD_DASHBOARD: 24 | dashboardStore.addDashboard(action.payload) 25 | break 26 | case d.DASHBOARD_DELETE_DASHBOARD: 27 | dashboardStore.deleteDashboard(action.payload) 28 | break 29 | case d.DASHBOARD_RENAME_DASHBOARD: 30 | dashboardStore.renameDashboard(action.payload) 31 | break 32 | case d.DASHBOARD_ADD: 33 | dashboardStore.showLastDashboard() 34 | break 35 | case d.DASHBOARD_SHOW: 36 | dashboardStore.showDashboard(action.payload) 37 | break 38 | case d.DASHBOARD_HIDE: 39 | dashboardStore.hideDashboard(action.payload) 40 | break 41 | } 42 | 43 | return next(null, action); 44 | } -------------------------------------------------------------------------------- /src/resolvers/logger_resolver.js: -------------------------------------------------------------------------------- 1 | export function loggerResolver(action, next) { 2 | console.log("action", action.type, action); 3 | return next(null, action); 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/resolvers/message_resolver.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/message_types'; 2 | import { messageStore } from '../stores/message_store'; 3 | 4 | export default function(action, next) { 5 | switch(action.type) { 6 | case t.MESSAGE_FETCH: 7 | messageStore.message = action.payload; 8 | break; 9 | case t.MESSAGE_ERROR: 10 | messageStore.error = action.payload; 11 | break; 12 | } 13 | return next(null, action); 14 | } 15 | -------------------------------------------------------------------------------- /src/resolvers/page_resolver.js: -------------------------------------------------------------------------------- 1 | import { registerStore } from '../stores/register_store'; 2 | 3 | export function resolvePageAction(action, next) { 4 | const store = action.store; 5 | const idx = action.type.indexOf("_"); 6 | const type = action.type.substring(idx+1); 7 | var name = null 8 | if (typeof store === "string") { 9 | name = store 10 | } else { 11 | name = store.name 12 | } 13 | const stores = registerStore.getAll(name); 14 | 15 | switch(type) { 16 | case 'add': 17 | // need to update all related stores 18 | stores.forEach( (tstore) => tstore.add(action.payload) ) 19 | break; 20 | case 'delete': 21 | // need to update all related stores 22 | stores.forEach( (tstore) => tstore.delete(action.payload) ) 23 | break; 24 | case 'update': 25 | // need to update all related stores 26 | stores.forEach( (tstore) => tstore.update(action.payload) ) 27 | break; 28 | case 'next_page': 29 | // ONLY CHANGE THE PAGE OF THE CURRENT STORE 30 | store.nextPage(); 31 | break; 32 | case 'previous_page': 33 | // ONLY CHANGE THE PAGE OF THE CURRENT STORE 34 | store.previousPage(); 35 | break; 36 | case 'get_all': 37 | // need to update all related stores 38 | store.setAll(action.payload) 39 | break; 40 | } 41 | } -------------------------------------------------------------------------------- /src/resolvers/popup_resolver.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/popup_types'; 2 | import { popupStore } from '../stores/popup_store'; 3 | 4 | export function popupResolver(action, next) { 5 | switch(action.type) { 6 | case t.POPUP_CLOSE : 7 | popupStore.close() 8 | break; 9 | case t.POPUP_SHOW : 10 | popupStore.show(action.payload.component) 11 | break; 12 | } 13 | return next(null, action); 14 | } 15 | -------------------------------------------------------------------------------- /src/resolvers/tabbar_resolver.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/tabbar_types' 2 | import * as d from '../types/dashboard_types' 3 | import { tabbarStore } from '../stores/tabbar_store' 4 | 5 | export function tabbarResolver(action, next) { 6 | switch(action.type) { 7 | case d.DASHBOARD_RENAME_DASHBOARD: 8 | tabbarStore.rename(action.payload) 9 | break 10 | case t.TABBAR_CLOSE: 11 | tabbarStore.close(action.payload) 12 | break 13 | case t.TABBAR_CLOSE_ALL: 14 | tabbarStore.closeAll() 15 | break 16 | case t.TABBAR_SHOW: 17 | tabbarStore.show(action.payload.component, action.payload.componentId, action.payload.title, action.payload.type) 18 | break 19 | case t.TABBAR_SELECT: 20 | tabbarStore.select(action.payload) 21 | break 22 | } 23 | return next(null, action) 24 | } 25 | -------------------------------------------------------------------------------- /src/resolvers/test_resolver.js: -------------------------------------------------------------------------------- 1 | // coul be set by a text function 2 | // 3 | var testFunction = null 4 | 5 | export function setTestFunction(testFct) { 6 | testFunction = testFct; 7 | } 8 | 9 | export default function(action, next) { 10 | if (testFunction) { 11 | testFunction(action) 12 | } 13 | 14 | return next(null, action); 15 | } -------------------------------------------------------------------------------- /src/resolvers/thunk_resolver.js: -------------------------------------------------------------------------------- 1 | export function thunkResolver(action, next) { 2 | if (typeof action.payload === 'function') { 3 | return action.payload(); 4 | } else { 5 | return next(null, action); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/resolvers/todo_resolver.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/todo_types'; 2 | import { resolvePageAction } from './page_resolver'; 3 | import { todoStore } from '../stores/todo_store' 4 | 5 | export default function(action, next) { 6 | // do the specific action first, this way 7 | // you can easily override the base action 8 | const store = action.store; 9 | switch(action.type) { 10 | case t.TODO_DONE: 11 | action.store.done(action.store, action.payload) 12 | break; 13 | case t.TODO_VALIDATE: 14 | // do something special with this one 15 | // TODO 16 | return next(null, action); 17 | } 18 | 19 | resolvePageAction(action) 20 | return next(null, action); 21 | } -------------------------------------------------------------------------------- /src/resolvers/user_resolver.js: -------------------------------------------------------------------------------- 1 | import * as t from '../types/user_types'; 2 | import { resolvePageAction } from './page_resolver'; 3 | 4 | export default function(action, next) { 5 | // do the specific actions first, this way 6 | // you can easily override the base action 7 | const store = action.store; 8 | switch(action.type) { 9 | case t.USER_VALIDATE: 10 | // do something special with this one 11 | return next(null, action); 12 | } 13 | 14 | resolvePageAction(action); 15 | 16 | return next(null, action); 17 | } -------------------------------------------------------------------------------- /src/services/auth_service.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { ROOT_URL, HEADERS, PARAMETERS } from './const_service' 3 | import { dispatch } from '../helpers/dispatcher' 4 | 5 | export default class AuthService { 6 | constructor() { 7 | this.instanceService = null 8 | } 9 | 10 | static setInstance(instanceService) { 11 | this.instanceService = instanceService 12 | } 13 | 14 | static getInstance() { 15 | if (!this.instanceService) { 16 | this.instanceService = new AuthService() 17 | } 18 | return this.instanceService 19 | } 20 | 21 | signIn({ email, password }, next, err) { 22 | axios.post(`${ROOT_URL}/auth/signin?${PARAMETERS()}`, { email, password }) 23 | .then(response => { 24 | dispatch(next(response.data.token, response.data.name)); 25 | }) 26 | .catch(response => dispatch(err(response.data))); 27 | } 28 | 29 | signUp({ email, password, name }, next, err) { 30 | axios.post(`${ROOT_URL}/auth/signup?${PARAMETERS()}`, { email, password, name }) 31 | .then(response => { 32 | dispatch(next(response.data.token, name)); 33 | }) 34 | .catch(response => dispatch(err(response.data))); 35 | } 36 | 37 | setAuthorizations(render, next, err) { 38 | axios.get(`${ROOT_URL}/api/actions?${PARAMETERS()}`, HEADERS()) 39 | .then(response => { 40 | dispatch(next(render, response.data)) 41 | }) 42 | .catch(response => { 43 | dispatch(err(response.data, render)) 44 | }) 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/services/const_service.js: -------------------------------------------------------------------------------- 1 | export const ROOT_URL = 'http://localhost:3090' 2 | export const WSS_URL = 'ws://localhost:5000' 3 | export const API_URL = 'http://localhost:3090/api' 4 | export const HEADERS = function() { 5 | return { headers: { Authorization: localStorage.getItem('react-portal-token') } } 6 | } 7 | export const PARAMETERS = function() { 8 | return 'project=all&locale=en-US' 9 | } -------------------------------------------------------------------------------- /src/services/crud_service.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL, HEADERS, PARAMETERS } from './const_service'; 3 | import { dispatch } from '../helpers/dispatcher'; 4 | 5 | export default class CrudService { 6 | constructor() { 7 | this.service = "need_to_be_set_by_the_extend_class"; 8 | } 9 | 10 | add(store, entity, next, err) { 11 | axios.post(`${API_URL}/${this.service}?${PARAMETERS()}`, entity, HEADERS()) 12 | .then(response => { 13 | dispatch(next(store, response.data)); 14 | }) 15 | .catch(response => dispatch(err(store, response.data))); 16 | }; 17 | 18 | delete(store, entity, next, err) { 19 | axios.delete(`${API_URL}/${this.service}/${entity._id}?${PARAMETERS()}`, HEADERS()) 20 | .then(response => { 21 | dispatch(next(store, entity)); 22 | }) 23 | .catch(response => dispatch(err(store, response.data))); 24 | }; 25 | 26 | update(store, entity, next, err) { 27 | axios.put(`${API_URL}/${this.service}?${PARAMETERS()}`, entity, HEADERS()) 28 | .then(response => { 29 | dispatch(next(store, response.data)); 30 | }) 31 | .catch(response => dispatch(err(store, response.data))); 32 | }; 33 | 34 | getAll(store, next, err) { 35 | axios.get(`${API_URL}/${this.service}?${PARAMETERS()}`, HEADERS()) 36 | .then(response => { 37 | dispatch(next(store, response.data)); 38 | }) 39 | .catch(response => dispatch(err(store, response.data))); 40 | }; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/services/dashboard_service.js: -------------------------------------------------------------------------------- 1 | import CrudService from './crud_service' 2 | 3 | export default class DashboardService extends CrudService { 4 | constructor() { 5 | super(); 6 | this.service= "dashboards"; 7 | this.instanceService = null 8 | } 9 | 10 | static setInstance(instanceService) { 11 | this.instanceService = instanceService 12 | } 13 | 14 | static getInstance() { 15 | if (!this.instanceService) { 16 | this.instanceService = new DashboardService() 17 | } 18 | return this.instanceService 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/services/message_service.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ROOT_URL, HEADERS, PARAMETERS } from './const_service'; 3 | import { messageSet, messageError } from '../actions/message_actions'; 4 | import { dispatch } from '../helpers/dispatcher'; 5 | 6 | 7 | class MessageService { 8 | fetchMessage(next, err) { 9 | axios.get(`${ROOT_URL}?${PARAMETERS()}`, HEADERS()) 10 | .then(response => { 11 | dispatch(next(response.data.message)); 12 | }) 13 | .catch((error) => { 14 | dispatch(err(error)); 15 | }); 16 | }; 17 | } 18 | export let messageService = new MessageService(); 19 | -------------------------------------------------------------------------------- /src/services/todo_service.js: -------------------------------------------------------------------------------- 1 | import CrudService from './crud_service'; 2 | 3 | export default class TodoService extends CrudService { 4 | constructor() { 5 | super(); 6 | this.service= "todos"; 7 | this.instanceService = null 8 | } 9 | 10 | static setInstance(instanceService) { 11 | this.instanceService = instanceService 12 | } 13 | 14 | static getInstance() { 15 | if (!this.instanceService) { 16 | this.instanceService = new TodoService() 17 | } 18 | return this.instanceService 19 | } 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/services/user_service.js: -------------------------------------------------------------------------------- 1 | import CrudService from './crud_service'; 2 | 3 | export default class UserService extends CrudService { 4 | constructor() { 5 | super(); 6 | this.service= "users"; 7 | this.instanceService = null 8 | } 9 | 10 | static setInstance(instanceService) { 11 | this.instanceService = instanceService 12 | } 13 | 14 | static getInstance() { 15 | if (!this.instanceService) { 16 | this.instanceService = new UserService() 17 | } 18 | return this.instanceService 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/services/widget_service.js: -------------------------------------------------------------------------------- 1 | import CrudService from './crud_service'; 2 | 3 | export default class WidgetService extends CrudService { 4 | constructor() { 5 | super(); 6 | this.service= "widgets"; 7 | this.instanceService = null 8 | } 9 | 10 | static setInstance(instanceService) { 11 | this.instanceService = instanceService 12 | } 13 | 14 | static getInstance() { 15 | if (!this.instanceService) { 16 | this.instanceService = new WidgetService() 17 | } 18 | return this.instanceService 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/services/wss_service.js: -------------------------------------------------------------------------------- 1 | import { WSS_URL } from './const_service' 2 | import { registerStore } from '../stores/register_store' 3 | import { dispatch } from '../helpers/dispatcher' 4 | import * as actions from '../actions/base_actions' 5 | 6 | // use the browser websocket service. 7 | var ws = new WebSocket(WSS_URL); 8 | 9 | function f(type, store, payload) { 10 | return { type, store, payload }; 11 | } 12 | 13 | ws.onmessage = function (event) { 14 | const data = eval("("+event.data+")"); 15 | 16 | if (data.function) { 17 | const stores = registerStore.getAll(data.store); 18 | stores.forEach( (store) => 19 | dispatch( eval(`${data.function}`) ) ) 20 | } else if (data.store) { 21 | const stores = registerStore.getAll(data.store); 22 | stores.forEach( (store) => 23 | dispatch( f(data.type, store, data.payload) ) 24 | ); 25 | } else { 26 | dispatch( f(data.type, store, data.payload) ) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/stores/auth_store.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observable, action, transaction } from 'mobx' 3 | import { browserHistory } from 'react-router' 4 | import { authSetAuthorizations } from '../actions/auth_actions' 5 | import { tabbarCloseAll } from '../actions/tabbar_actions' 6 | import { crudGetAll } from '../actions/crud_actions' 7 | import { dispatch } from '../helpers/dispatcher' 8 | import { dashboardStore } from './dashboard_store' 9 | 10 | export default class AuthStore { 11 | @observable email = "" 12 | @observable name = "" 13 | @observable authenticated = false 14 | @observable errorMessage = '' 15 | 16 | isAutorizationInit = false 17 | authorizations = [] 18 | 19 | isActionAvailable(actiontype) { 20 | return true 21 | // if (actiontype.endsWith("_")) { 22 | // actiontype = actiontype.substr(0, actiontype.length - 1); 23 | // } 24 | // return this.actions.indexOf(actiontype) > -1 25 | } 26 | 27 | getError() { 28 | return this.errorMessage 29 | } 30 | 31 | setAuthorizations(authorizations, mainComponentsToRender) { 32 | transaction( () => { 33 | this.isAutorizationInit = true 34 | this.authorizations = authorizations 35 | }) 36 | // now we can render main, but it could be null 37 | if (mainComponentsToRender) { 38 | mainComponentsToRender() 39 | } 40 | browserHistory.push('/main'+window.location.search) 41 | dispatch(crudGetAll(dashboardStore)) 42 | } 43 | 44 | checkToken(mainComponentsToRender) { 45 | const token = localStorage.getItem('react-portal-token') 46 | if (token != null && token != '') { 47 | const name = localStorage.getItem('react-portal-name') 48 | transaction( () => { 49 | this.authenticated = true 50 | this.name = name 51 | this.errorMessage = '' 52 | dispatch(authSetAuthorizations(mainComponentsToRender)) 53 | }) 54 | } else { 55 | // render the main, but stay on root to 56 | // SignUn or SignUp 57 | mainComponentsToRender() 58 | browserHistory.push('/'+window.location.search) 59 | } 60 | } 61 | 62 | signInOrUp(token, name) { 63 | localStorage.setItem('react-portal-token', token); 64 | localStorage.setItem('react-portal-name', name); 65 | transaction( () => { 66 | this.authenticated = true; 67 | this.name = name; 68 | this.errorMessage = ''; 69 | }); 70 | dispatch(authSetAuthorizations(null)) 71 | } 72 | 73 | signOut() { 74 | localStorage.removeItem('react-portal-token'); 75 | localStorage.removeItem('react-portal-name'); 76 | transaction(() => { 77 | this.authenticated = false; 78 | this.name = ''; 79 | this.errorMessage = ''; 80 | }); 81 | dispatch(tabbarCloseAll()) 82 | } 83 | 84 | authError(error, mainComponentsToRender) { 85 | transaction(() => { 86 | if (mainComponentsToRender) { 87 | this.errorMessage = '' 88 | } else { 89 | if (typeof error === 'object') { 90 | this.errorMessage = error.error; 91 | } else { 92 | this.errorMessage = error; 93 | } 94 | } 95 | this.authenticated = false; 96 | this.name = ''; 97 | }) 98 | if (mainComponentsToRender) { 99 | // token is not good or an error with authentification 100 | // the first time don't show an error 101 | mainComponentsToRender() 102 | browserHistory.push('/'+window.location.search) 103 | } 104 | } 105 | } 106 | export let authStore = new AuthStore() -------------------------------------------------------------------------------- /src/stores/crud_store.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | export default class CrudStore { 4 | constructor() { 5 | this.key = -1 // key for new records 6 | this.name = 'need_to_be_set_by_extend_class'; 7 | this.service = null; // need to be set by the extend class 8 | // it's use by the base action to know 9 | // witch service to call 10 | } 11 | 12 | @observable records = []; 13 | @observable error = ''; 14 | 15 | @action 16 | setAll(records) { 17 | this.records = records; 18 | } 19 | 20 | @action 21 | add(record) { 22 | this.records.push(record); 23 | } 24 | 25 | @action 26 | update(record) { 27 | const idx = this.records.findIndex( (r) => r._id === record._id ); 28 | this.records[idx] = record; 29 | } 30 | 31 | @action 32 | delete(record) { 33 | const idx = this.records.findIndex( (r) => r._id === record._id ); 34 | this.records.splice(idx,1); 35 | } 36 | 37 | setError(error) { 38 | if (typeof error === 'object') { 39 | this.error = error["error"]; 40 | } else { 41 | this.error = error; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/stores/dashboard_store.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observable, action } from 'mobx' 3 | import CrudStore from './crud_store'; 4 | import { tabbarStore } from './tabbar_store' 5 | import { registerStore } from './register_store'; 6 | import DashboardService from '../services/dashboard_service' 7 | import { dispatch } from '../helpers/dispatcher' 8 | import { crudUpdate, crudAdd, crudDelete } from '../actions/crud_actions' 9 | import { tabbarShow, tabbarSelect, tabbarClose } from '../actions/tabbar_actions' 10 | import Dashboard from '../components/dashboard/dashboard' 11 | import DashboardModel from '../models/dashboard_model' 12 | 13 | // need to import all available widgets that can be included into a dashboard 14 | import UsersWidget from '../components/widgets/users_widget' 15 | import TodosWidget from '../components/widgets/todos_widget' 16 | import AgGridWidget from '../components/widgets/aggrid_widget' 17 | import LineChartWidget from '../components/widgets/linechart_widget' 18 | 19 | export default class DashboardStore extends CrudStore { 20 | constructor() { 21 | super(); 22 | this.service = DashboardService.getInstance(); 23 | this.name = 'dashboard'; 24 | this.refCount = 0 25 | } 26 | 27 | showAllUserDashboard() { 28 | this.records.forEach( (dashboard, idx) => { 29 | if (dashboard.isHidden === false) { 30 | var component = () => 31 | dispatch(tabbarShow(component, dashboard._id, dashboard.title, 'dashboard')) 32 | } 33 | }) 34 | } 35 | 36 | showLastDashboard() { 37 | const dashboard = this.records[this.records.length - 1] 38 | var component = () => 39 | dispatch(tabbarShow(component, dashboard._id, dashboard.title, 'dashboard')) 40 | } 41 | 42 | showDashboard(dashboardId) { 43 | const idx = this.records.findIndex( (r) => r._id === dashboardId ); 44 | if (idx > -1) { 45 | const dashboard = this.records[idx] 46 | dashboard.isHidden = false 47 | dispatch(crudUpdate(this, dashboard)) 48 | var component = () => 49 | dispatch(tabbarShow(component, dashboard._id, dashboard.title, 'dashboard')) 50 | } 51 | } 52 | 53 | removeWidget(dashboardId, widgetId) { 54 | const idx = this.records.findIndex( (r) => r._id === dashboardId ); 55 | const indexWidget = this.records[idx].widgets.findIndex( (r) => r.i === widgetId ); 56 | this.records[idx].widgets.splice(indexWidget,1); 57 | dispatch(crudUpdate(this, this.records[idx])) 58 | } 59 | 60 | createWidget(name) { 61 | this.refCount = this.refCount - 1 62 | return { 63 | i: 'ref'+this.refCount, x: 0, y: 0, w: 4, h: 21, name: name 64 | } 65 | } 66 | 67 | addDashboard(dashboardName) { 68 | dispatch(crudAdd(this, DashboardModel.create(dashboardName))) 69 | } 70 | 71 | deleteDashboard() { 72 | const dashboardId = tabbarStore.getCurrentComponentId() 73 | const idx = this.records.findIndex( (r) => r._id === dashboardId ) 74 | dispatch(crudDelete(this, this.records[idx])) 75 | dispatch(tabbarClose(dashboardId)) 76 | } 77 | 78 | hideDashboard(dashboardId) { 79 | const idx = this.records.findIndex( (r) => r._id === dashboardId ) 80 | if (idx > -1) { 81 | this.records[idx].isHidden = true 82 | dispatch(crudUpdate(this, this.records[idx])) 83 | } 84 | } 85 | 86 | renameDashboard(dashboardName) { 87 | const dashboardId = tabbarStore.getCurrentComponentId() 88 | const idx = this.records.findIndex( (r) => r._id === dashboardId ) 89 | if (idx > -1) { 90 | this.records[idx].title = dashboardName; 91 | dispatch(crudUpdate(this, this.records[idx])) 92 | } 93 | } 94 | 95 | addWidget(widgetName) { 96 | const dashboardId = tabbarStore.getCurrentComponentId() 97 | const idx = this.records.findIndex( (r) => r._id === dashboardId ) 98 | this.records[idx].widgets.push(this.createWidget(widgetName)); 99 | dispatch(crudUpdate(this, this.records[idx])) 100 | } 101 | 102 | getDashboardTitle() { 103 | const dashboardId = tabbarStore.getCurrentComponentId() 104 | const idx = this.records.findIndex( (r) => r._id === dashboardId ); 105 | return this.records[idx].title 106 | } 107 | 108 | getDashboard(dashboardId) { 109 | const idx = this.records.findIndex( (r) => r._id === dashboardId ); 110 | return this.records[idx] 111 | } 112 | 113 | getWidgets(dashboardId) { 114 | const idx = this.records.findIndex( (r) => r._id === dashboardId ); 115 | return this.records[idx].widgets 116 | } 117 | 118 | getWidgetsLayout(dashboardId) { 119 | const idx = this.records.findIndex( (r) => r._id === dashboardId ); 120 | if (idx > -1) { 121 | var layouts = [] 122 | if (this.records[idx].widgets) { 123 | this.records[idx].widgets.forEach( (w) => layouts.push( { i: w.i, x: w.x, y: w.y, w: w.w, h: w.h, name: w.name } ) ) 124 | } 125 | return layouts 126 | } else { 127 | return [] 128 | } 129 | } 130 | 131 | static getFormManageDimension() { 132 | return { 133 | width: '50%', 134 | height: '500px', 135 | left: '50%', 136 | top: '100px' 137 | } 138 | } 139 | } 140 | export let dashboardStore = new DashboardStore() 141 | 142 | 143 | export function getWidgetComponent(name, dashboardId, widgetId) { 144 | switch (name) { 145 | case 'TodosWidget': 146 | return 147 | case 'UsersWidget': 148 | return 149 | case 'LineChartWidget': 150 | return 151 | case 'AgGridWidget': 152 | return 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/stores/message_store.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | export var messageStore = { 4 | @observable message : '', 5 | @observable error : '' 6 | } -------------------------------------------------------------------------------- /src/stores/page_store.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | export default class PageStore { 4 | constructor() { 5 | this.key = -1 // key for new records 6 | this.name = 'need_to_be_set_by_extend_class'; 7 | this.service = null; // need to be set by the extend class 8 | // it's use by the base action to know 9 | // witch service to call 10 | } 11 | 12 | @observable page = []; 13 | @observable error = ''; 14 | 15 | records = []; 16 | pageStart = 0; 17 | pageEnd = 10; 18 | pageSize = 10; 19 | 20 | @action 21 | setAll(records) { 22 | this.records = records; 23 | this.setCurrentPage(); 24 | } 25 | 26 | @action 27 | add(record) { 28 | this.records.push(record); 29 | if (this.pageEnd < this.pageSize) { 30 | this.pageEnd = this.pageEnd + 1 31 | } 32 | this.setCurrentPage(); 33 | } 34 | 35 | @action 36 | update(record) { 37 | const idx = this.records.findIndex( (r) => r._id === record._id ); 38 | this.records[idx] = record; 39 | this.setCurrentPage(); 40 | } 41 | 42 | @action 43 | delete(record) { 44 | const idx = this.records.findIndex( (r) => r._id === record._id ); 45 | this.records.splice(idx,1); 46 | this.setCurrentPage(); 47 | } 48 | 49 | setCurrentPage() { 50 | this.page = this.records.slice(this.pageStart,this.pageEnd); 51 | } 52 | 53 | @action 54 | nextPage() { 55 | if (this.pageEnd < this.records.length) { 56 | this.pageStart = this.pageStart + this.pageSize; 57 | this.pageEnd = this.pageEnd + this.pageSize; 58 | this.setCurrentPage(); 59 | } 60 | } 61 | 62 | @action 63 | previousPage() { 64 | if (this.pageStart > 0) { 65 | this.pageStart = Math.max(this.pageStart - this.pageSize,0); 66 | this.pageEnd = Math.max(this.pageEnd - this.pageSize, this.pageStart + this.pageSize); 67 | this.setCurrentPage(); 68 | } 69 | } 70 | 71 | @action 72 | firstPage() { 73 | if (this.pageStart > 0) { 74 | this.pageStart = 0; 75 | this.pageEnd = this.pageSize; 76 | this.setCurrentPage(); 77 | } 78 | } 79 | 80 | @action 81 | lastPage() { 82 | if (this.pageEnd < this.records.length) { 83 | this.pageEnd = this.records.length; 84 | this.pageStart = this.pageEnd - this.pageSize; 85 | this.setCurrentPage(); 86 | } 87 | } 88 | 89 | setError(error) { 90 | if (typeof error === 'object') { 91 | this.error = error["error"]; 92 | } else { 93 | this.error = error; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/stores/popup_store.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | export default class PopupStore { 5 | 6 | @observable popupStores = [] 7 | 8 | constructor() { 9 | this.current = -1 10 | } 11 | 12 | static getStandardDimension() { 13 | return { width: '50%', height: '200px', left: '50%', top: '100px' } 14 | } 15 | 16 | static getSmallDimension() { 17 | return { width: '30%', height: '200px', left: '60%', top: '100px' } 18 | } 19 | 20 | getCurrentId() { 21 | return this.current 22 | } 23 | 24 | getCurrentStore() { 25 | return this.popupStores[this.current] 26 | } 27 | 28 | getStores() { 29 | return this.popupStores 30 | } 31 | 32 | @action 33 | show(component) { 34 | this.current = this.current + 1 35 | if (this.current > 1) { 36 | this.popupStores[this.current - 1].display = 'none'; 37 | } 38 | const store = PopupStore.createStore() 39 | this.popupStores.push(store) 40 | 41 | store.id = this.current 42 | store.display = 'block'; 43 | store.component = component; 44 | } 45 | 46 | @action 47 | close() { 48 | this.current = this.current - 1 49 | this.popupStores.pop() 50 | if (this.current > -1) { 51 | this.popupStores[this.current].display = 'block'; 52 | } 53 | } 54 | 55 | static createStore() { 56 | return { 57 | @observable display : 'none', 58 | height : '250px', 59 | width : '50%', 60 | left : '50%', 61 | top : '100px', 62 | component : null, 63 | id : 0, 64 | } 65 | } 66 | } 67 | export let popupStore = new PopupStore() -------------------------------------------------------------------------------- /src/stores/register_store.js: -------------------------------------------------------------------------------- 1 | class RegisterStore { 2 | 3 | constructor() { 4 | this._nextId = 0 5 | this.stores = [] 6 | } 7 | 8 | nextId() { 9 | this._nextId = this._nextId + 1 10 | return this._nextId 11 | } 12 | 13 | add( store ) { 14 | this.stores.push(store); 15 | return store; 16 | } 17 | 18 | remove( store ) { 19 | const idx = this.stores.findIndex( (s) => s === store); 20 | this.stores.splice(idx,1); 21 | } 22 | 23 | /** 24 | * Get all the store with the specific name 25 | * @param {[type]} storeName [description] 26 | * @return {[type]} [description] 27 | */ 28 | getAll( storeName ) { 29 | return this.stores.filter( (store) => store.name === storeName); 30 | } 31 | } 32 | export let registerStore = new RegisterStore(); -------------------------------------------------------------------------------- /src/stores/search_store.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | export var searchStore = { 4 | @observable display : 'none' 5 | } -------------------------------------------------------------------------------- /src/stores/tabbar_store.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class TabbarStore { 4 | @observable tabbarStores = [] 5 | @observable current = -1 6 | 7 | getCurrentId() { 8 | return this.current 9 | } 10 | 11 | getCurrentComponentId() { 12 | return this.tabbarStores[this.current].componentId 13 | } 14 | 15 | getComponentId(idx) { 16 | return this.tabbarStores[idx].componentId 17 | } 18 | 19 | getCurrentTitle() { 20 | return this.tabbarStores[this.current].title 21 | } 22 | 23 | getCurrentStore() { 24 | return this.tabbarStores[this.current] 25 | } 26 | 27 | getStores() { 28 | return this.tabbarStores 29 | } 30 | 31 | @action 32 | closeAll() { 33 | this.tabbarStores = [] 34 | this.current = -1 35 | } 36 | 37 | @action 38 | rename(title) { 39 | this.tabbarStores[this.current].title = title 40 | } 41 | 42 | @action 43 | select(componentId) { 44 | const idx = this.tabbarStores.findIndex( (r) => r.componentId === componentId ); 45 | if (idx > -1) { 46 | this.tabbarStores[this.current].display = 'none' 47 | this.tabbarStores[idx].display = 'block' 48 | this.current = Number(idx) 49 | } 50 | } 51 | 52 | @action 53 | show(component, componentId, title, type) { 54 | const idx = this.tabbarStores.findIndex( (r) => r.componentId === componentId ); 55 | // check if already there 56 | if (idx !== -1) { 57 | this.select(componentId) 58 | return 59 | } 60 | 61 | if (this.current > -1) { 62 | this.tabbarStores[this.current].display = 'none' 63 | } 64 | this.tabbarStores.push(TabbarStore.createStore()) 65 | this.current = this.tabbarStores.length - 1 66 | this.tabbarStores[this.current].id = this.current 67 | this.tabbarStores[this.current].title = title ? title : 'na' 68 | this.tabbarStores[this.current].type = type ? type : 'dashboard' 69 | this.tabbarStores[this.current].componentId = componentId ? componentId : '0' 70 | this.tabbarStores[this.current].component = component 71 | this.tabbarStores[this.current].display = 'block' 72 | } 73 | 74 | @action 75 | close(componentId) { 76 | const idx = this.tabbarStores.findIndex( (r) => r.componentId === componentId ); 77 | if (this.current != idx) 78 | { // must be selected before closing 79 | this.select(componentId) 80 | } 81 | 82 | if (idx > -1) { 83 | this.tabbarStores[idx].display = 'none' 84 | this.tabbarStores.splice(idx,1) 85 | this.current = idx - 1 86 | if (this.current == -1 && this.tabbarStores.length > 0) { 87 | this.current = 0 88 | } 89 | // change the ID of the other tab 90 | if (this.current > -1) { 91 | this.tabbarStores[this.current].display = 'block' 92 | } 93 | } 94 | } 95 | 96 | static createStore() { 97 | return { 98 | @observable display : 'none', 99 | @observable title : '', 100 | type: '', // dashboard , page 101 | id : 0, 102 | componentId : '', 103 | component : null 104 | } 105 | } 106 | } 107 | export let tabbarStore = new TabbarStore() -------------------------------------------------------------------------------- /src/stores/todo_store.js: -------------------------------------------------------------------------------- 1 | import PageStore from './page_store'; 2 | import TodoService from '../services/todo_service'; 3 | import { registerStore } from './register_store'; 4 | import { observable, action } from 'mobx'; 5 | import { dispatch } from '../helpers/dispatcher' 6 | import { pageUpdateRecord } from '../actions/page_actions' 7 | 8 | export default class TodoStore extends PageStore { 9 | static create() { 10 | return registerStore.add( new TodoStore() ); 11 | } 12 | 13 | static remove(store) { 14 | registerStore.remove( store ); 15 | store = null; 16 | } 17 | 18 | constructor() { 19 | super() 20 | this.service = TodoService.getInstance() 21 | this.name = 'todo' 22 | this.id = 0 23 | this.error = null 24 | } 25 | 26 | done(store, record) { 27 | record.done = !record.done 28 | dispatch(pageUpdateRecord(store, record)) 29 | } 30 | 31 | static getEditFormDimension() { 32 | return { 33 | width: '50%', 34 | height: '245px', 35 | left: '50%', 36 | top: '100px' 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/stores/user_store.js: -------------------------------------------------------------------------------- 1 | import PageStore from './page_store'; 2 | import UserService from '../services/user_service'; 3 | import { registerStore } from './register_store'; 4 | 5 | export default class UserStore extends PageStore { 6 | static create() { 7 | return registerStore.add( new UserStore() ); 8 | } 9 | 10 | static remove(store) { 11 | registerStore.remove( store ); 12 | store = null; 13 | } 14 | 15 | constructor() { 16 | super(); 17 | this.service = UserService.getInstance(); 18 | this.name = 'user'; 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/stores/widget_store.js: -------------------------------------------------------------------------------- 1 | export default class WidgetStore { 2 | constructor() { 3 | this.records = [ 4 | { _id: '12', name : "TodosWidget", title : "Todos" }, 5 | { _id: '22', name : "UsersWidget", title : "Users"}, 6 | { _id: '32', name : "AgGridWidget", title : "Ag-Grid"}, 7 | { _id: '42', name : "LineChartWidget", title : "LineChart"} 8 | ] 9 | } 10 | 11 | getWidgets() { 12 | return this.records 13 | } 14 | } 15 | export let widgetStore = new WidgetStore() 16 | 17 | -------------------------------------------------------------------------------- /src/styles/_00-config.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * www.KNACSS.com V4.4.5 (1er avril 2016) @author: Raphael Goetter, Alsacreations 3 | * Licence WTFPL http://www.wtfpl.net/ 4 | */ 5 | 6 | // Config file and project variables 7 | 8 | // font sizes 9 | $base-font-size : 1.4rem !default; // ex. 1.4rem would be "14px" equivalent 10 | $line-height : 1.5 !default; // equiv line-height 1.5 11 | $h1-size : 3.2rem !default; // equiv "32px" 12 | $h2-size : 2.8rem !default; // equiv "28px" 13 | $h3-size : 2.4rem !default; // equiv "24px" 14 | $h4-size : 2.0rem !default; // equiv "20px" 15 | $h5-size : 1.8rem !default; // equiv "18px" 16 | $h6-size : 1.6rem !default; // equiv "16px" 17 | 18 | // font stacks 19 | $font-stack-common : sans-serif !default; // common font 20 | $font-stack-headings : sans-serif !default; // headings font 21 | $font-stack-monospace : consolas, courier, monospace !default; // monospace font 22 | 23 | // font colors 24 | $base-color : #000 !default; // text color on body and content 25 | $primary-color : #000 !default; // text color on primary elements 26 | $secondary-color : #000 !default; // text color on secondary elements 27 | $headings-color : #000 !default; // text color on headings 28 | $headings-1-color : #000 !default; // text color on headings level 1 29 | $headings-2-color : #000 !default; // text color on headings level 2 30 | $headings-3-color : #000 !default; // text color on headings level 3 31 | $base-color-link : #333 !default; // base links color 32 | $base-color-link-hover : #000 !default; // base hovered/focused links color 33 | 34 | // backgrounds 35 | $base-background : #fff !default; // body background color 36 | $primary-background : #fff !default; // primary elements background color 37 | $secondary-background : #fff !default; // secondary elements background color 38 | 39 | // spacings (choose unit you prefer) 40 | $tiny-value : .5rem !default; // tiny value for margins / paddings 41 | $tiny-plus-value : .7rem !default; // tiny+ value for margins / paddings 42 | $small-value : 1rem !default; // small value for margins / paddings 43 | $small-plus-value : 1.5rem !default; // small+ value for margins / paddings 44 | $medium-value : 2rem !default; // medium value for margins / paddings 45 | $medium-plus-value : 3rem !default; // medium+ value for margins / paddings 46 | $large-value : 4rem !default; // large value for margins / paddings 47 | $large-plus-value : 6rem !default; // large value for margins / paddings 48 | $extra-large-value : 8rem !default; // extra large value for margins / paddings 49 | $extra-large-plus-value : 12rem !default; // extra large value for margins / paddings 50 | $ultra-large-value : 16rem !default; // ultra large value for margins / paddings 51 | $ultra-large-plus-value : 20rem !default; // ultra large value for margins / paddings 52 | 53 | // breakpoints (choose unit you prefer) 54 | $tiny-screen : 320px !default; // tiny screens media query (less-equal than 320px) 55 | $tiny-plus-screen : 480px !default; // screens between 321px and 480px 56 | $small-screen : 640px !default; // screens between 481px and 640px 57 | $small-plus-screen : 768px !default; // screens between 641px and 768px 58 | $medium-screen : 960px !default; // screens between 769px and 960px 59 | $medium-plus-screen : 1024px !default; // screens between 961px and 1024px 60 | $large-screen : 1280px !default; // screens between 1025px and 1280px 61 | $large-plus-screen : 1440px !default; // screens between 1281px and 1440px 62 | $extra-large-screen : 1600px !default; // screens between 1441px and 1600px 63 | $ultra-large-screen : 1920px !default; // ultra large screens 64 | 65 | // grids variables (choose unit you prefer) 66 | $gutter: 2rem !default; // gutter value for grid layouts. Unit can be: %, px, em, rem 67 | $number: 4 !default; // number of equal columns 68 | $left: 2 !default; // left side of uneven columns 69 | $right: 1 !default; // right side of uneven columns 70 | 71 | //kna-namespace (default : null) 72 | $kna-namespace: kna-; 73 | -------------------------------------------------------------------------------- /src/styles/_01a-normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * 1. Remove the gray background on active links in IE 10. 89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 90 | */ 91 | 92 | a { 93 | background-color: transparent; /* 1 */ 94 | -webkit-text-decoration-skip: objects; /* 2 */ 95 | } 96 | 97 | /** 98 | * Remove the outline on focused links when they are also active or hovered 99 | * in all browsers (opinionated). 100 | */ 101 | 102 | a:active, 103 | a:hover { 104 | outline-width: 0; 105 | } 106 | 107 | /* Text-level semantics 108 | ========================================================================== */ 109 | 110 | /** 111 | * 1. Remove the bottom border in Firefox 39-. 112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: none; /* 1 */ 117 | text-decoration: underline; /* 2 */ 118 | text-decoration: underline dotted; /* 2 */ 119 | } 120 | 121 | /** 122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: inherit; 128 | } 129 | 130 | /** 131 | * Add the correct font weight in Chrome, Edge, and Safari. 132 | */ 133 | 134 | b, 135 | strong { 136 | font-weight: bolder; 137 | } 138 | 139 | /** 140 | * Add the correct font style in Android 4.3-. 141 | */ 142 | 143 | dfn { 144 | font-style: italic; 145 | } 146 | 147 | /** 148 | * Correct the font size and margin on `h1` elements within `section` and 149 | * `article` contexts in Chrome, Firefox, and Safari. 150 | */ 151 | 152 | h1 { 153 | font-size: 2em; 154 | margin: 0.67em 0; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Remove the border on images inside links in IE 10-. 200 | */ 201 | 202 | img { 203 | border-style: none; 204 | } 205 | 206 | /** 207 | * Hide the overflow in IE. 208 | */ 209 | 210 | svg:not(:root) { 211 | overflow: hidden; 212 | } 213 | 214 | /* Grouping content 215 | ========================================================================== */ 216 | 217 | /** 218 | * 1. Correct the inheritance and scaling of font size in all browsers. 219 | * 2. Correct the odd `em` font sizing in all browsers. 220 | */ 221 | 222 | code, 223 | kbd, 224 | pre, 225 | samp { 226 | font-family: monospace, monospace; /* 1 */ 227 | font-size: 1em; /* 2 */ 228 | } 229 | 230 | /** 231 | * Add the correct margin in IE 8. 232 | */ 233 | 234 | figure { 235 | margin: 1em 40px; 236 | } 237 | 238 | /** 239 | * 1. Add the correct box sizing in Firefox. 240 | * 2. Show the overflow in Edge and IE. 241 | */ 242 | 243 | hr { 244 | box-sizing: content-box; /* 1 */ 245 | height: 0; /* 1 */ 246 | overflow: visible; /* 2 */ 247 | } 248 | 249 | /* Forms 250 | ========================================================================== */ 251 | 252 | /** 253 | * 1. Change font properties to `inherit` in all browsers (opinionated). 254 | * 2. Remove the margin in Firefox and Safari. 255 | */ 256 | 257 | button, 258 | input, 259 | select, 260 | textarea { 261 | font: inherit; /* 1 */ 262 | margin: 0; /* 2 */ 263 | } 264 | 265 | /** 266 | * Restore the font weight unset by the previous rule. 267 | */ 268 | 269 | optgroup { 270 | font-weight: bold; 271 | } 272 | 273 | /** 274 | * Show the overflow in IE. 275 | * 1. Show the overflow in Edge. 276 | */ 277 | 278 | button, 279 | input { /* 1 */ 280 | overflow: visible; 281 | } 282 | 283 | /** 284 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 285 | * 1. Remove the inheritance of text transform in Firefox. 286 | */ 287 | 288 | button, 289 | select { /* 1 */ 290 | text-transform: none; 291 | } 292 | 293 | /** 294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 295 | * controls in Android 4. 296 | * 2. Correct the inability to style clickable types in iOS and Safari. 297 | */ 298 | 299 | button, 300 | html [type="button"], /* 1 */ 301 | [type="reset"], 302 | [type="submit"] { 303 | -webkit-appearance: button; /* 2 */ 304 | } 305 | 306 | /** 307 | * Remove the inner border and padding in Firefox. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | [type="button"]::-moz-focus-inner, 312 | [type="reset"]::-moz-focus-inner, 313 | [type="submit"]::-moz-focus-inner { 314 | border-style: none; 315 | padding: 0; 316 | } 317 | 318 | /** 319 | * Restore the focus styles unset by the previous rule. 320 | */ 321 | 322 | button:-moz-focusring, 323 | [type="button"]:-moz-focusring, 324 | [type="reset"]:-moz-focusring, 325 | [type="submit"]:-moz-focusring { 326 | outline: 1px dotted ButtonText; 327 | } 328 | 329 | /** 330 | * Change the border, margin, and padding in all browsers (opinionated). 331 | */ 332 | 333 | fieldset { 334 | border: 1px solid #c0c0c0; 335 | margin: 0 2px; 336 | padding: 0.35em 0.625em 0.75em; 337 | } 338 | 339 | /** 340 | * 1. Correct the text wrapping in Edge and IE. 341 | * 2. Correct the color inheritance from `fieldset` elements in IE. 342 | * 3. Remove the padding so developers are not caught out when they zero out 343 | * `fieldset` elements in all browsers. 344 | */ 345 | 346 | legend { 347 | box-sizing: border-box; /* 1 */ 348 | color: inherit; /* 2 */ 349 | display: table; /* 1 */ 350 | max-width: 100%; /* 1 */ 351 | padding: 0; /* 3 */ 352 | white-space: normal; /* 1 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 404 | */ 405 | 406 | ::-webkit-input-placeholder { 407 | color: inherit; 408 | opacity: 0.54; 409 | } 410 | 411 | /** 412 | * 1. Correct the inability to style clickable types in iOS and Safari. 413 | * 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; /* 1 */ 418 | font: inherit; /* 2 */ 419 | } -------------------------------------------------------------------------------- /src/styles/_01b-base.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* == soft reset */ 3 | /* ----------------------------- */ 4 | 5 | /* switching to border-box model for all elements */ 6 | html { 7 | box-sizing: border-box; 8 | } 9 | 10 | * { 11 | box-sizing: inherit; 12 | } 13 | 14 | ul, 15 | ol { 16 | padding-left: 2em; 17 | } 18 | 19 | img { 20 | vertical-align: middle; 21 | } 22 | 23 | /* height auto only for non SVG images */ 24 | img:not([src$=".svg"]) { 25 | height: auto; 26 | } 27 | 28 | blockquote, 29 | figure { 30 | margin-left: 0; 31 | margin-right: 0; 32 | } 33 | 34 | /* ----------------------------- */ 35 | /* == typography */ 36 | /* ----------------------------- */ 37 | 38 | html { 39 | 40 | /* set base font-size to equiv "10px", which is adapted to rem unit */ 41 | font-size: 62.5%; 42 | 43 | /* IE9-IE11 math fixing. See http://bit.ly/1g4X0bX */ 44 | /* thanks to @guardian, @victorbritopro and @eQRoeil */ 45 | font-size: calc(1em * 0.625); 46 | } 47 | 48 | body { 49 | font-size: $base-font-size; 50 | 51 | background-color: $base-background; 52 | color: $base-color; 53 | font-family: $font-stack-common; 54 | line-height: $line-height; 55 | } 56 | 57 | a { 58 | color: $base-color-link; 59 | &:hover, &:focus, &:active { 60 | color: $base-color-link-hover; 61 | } 62 | } 63 | 64 | /* font-sizing for content */ 65 | p, 66 | .#{$kna-namespace}p-like, 67 | ul, 68 | ol, 69 | dl, 70 | blockquote, 71 | pre, 72 | td, 73 | th, 74 | label, 75 | textarea, 76 | caption, 77 | details, 78 | figure { 79 | margin-top: 0.75em; 80 | margin-bottom: 0; 81 | line-height: $line-height; 82 | } 83 | 84 | h1, .#{$kna-namespace}h1-like { 85 | font-size: $h1-size; 86 | font-family: $font-stack-headings; 87 | } 88 | 89 | h2, .#{$kna-namespace}h2-like { 90 | font-size: $h2-size; 91 | font-family: $font-stack-headings; 92 | } 93 | 94 | h3, .#{$kna-namespace}h3-like { 95 | font-size: $h3-size; 96 | } 97 | 98 | h4, .#{$kna-namespace}h4-like { 99 | font-size: $h4-size; 100 | } 101 | 102 | h5, .#{$kna-namespace}h5-like { 103 | font-size: $h5-size; 104 | } 105 | 106 | h6, .#{$kna-namespace}h6-like { 107 | font-size: $h6-size; 108 | } 109 | 110 | /* alternate font-sizing */ 111 | .#{$kna-namespace}smaller { 112 | font-size: 0.6em; 113 | } 114 | 115 | .#{$kna-namespace}small { 116 | font-size: 0.8em; 117 | } 118 | 119 | .#{$kna-namespace}big { 120 | font-size: 1.2em; 121 | } 122 | 123 | .#{$kna-namespace}bigger { 124 | font-size: 1.5em; 125 | } 126 | 127 | .#{$kna-namespace}biggest { 128 | font-size: 2em; 129 | } 130 | 131 | code, 132 | pre, 133 | samp, 134 | kbd { 135 | /* IE fix */ 136 | white-space: pre-line; 137 | white-space: pre-wrap; 138 | font-family: $font-stack-monospace; 139 | line-height: normal; 140 | } 141 | 142 | em, 143 | .#{$kna-namespace}italic, 144 | address, 145 | cite, 146 | i, 147 | var { 148 | font-style: italic; 149 | } 150 | 151 | /* ----------------------------- */ 152 | /* == browsers consistency */ 153 | /* ----------------------------- */ 154 | 155 | /* avoid top margins on first content element */ 156 | p, 157 | .#{$kna-namespace}p-like, 158 | ul, 159 | ol, 160 | dl, 161 | blockquote, 162 | pre, 163 | h1, 164 | .#{$kna-namespace}h1-like, 165 | h2, 166 | .#{$kna-namespace}h2-like, 167 | h3, 168 | .#{$kna-namespace}h3-like, 169 | h4, 170 | .#{$kna-namespace}h4-like, 171 | h5, 172 | .#{$kna-namespace}h5-like, 173 | h6, 174 | .#{$kna-namespace}h6-like { 175 | &:first-child { 176 | margin-top: 0; 177 | } 178 | } 179 | 180 | /* avoid margins on nested elements */ 181 | li p, 182 | li .#{$kna-namespace}p-like, 183 | li ul, 184 | li ol { 185 | margin-top: 0; 186 | margin-bottom: 0; 187 | } 188 | 189 | /* max values */ 190 | img, 191 | table, 192 | td, 193 | blockquote, 194 | code, 195 | pre, 196 | textarea, 197 | input, 198 | video, 199 | svg { 200 | max-width: 100%; 201 | } 202 | 203 | /* margin-bottom on tables */ 204 | table { 205 | margin-bottom: $medium-value; 206 | } 207 | -------------------------------------------------------------------------------- /src/styles/_02-layout.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==layout and modules */ 3 | /* ----------------------------- */ 4 | 5 | /* module, gains superpower "BFC" Block Formating Context */ 6 | .mod, 7 | .bfc { 8 | overflow: hidden; 9 | } 10 | 11 | /* blocks that needs to be placed under floats */ 12 | .clear, 13 | .#{$kna-namespace}line, 14 | .#{$kna-namespace}row { 15 | clear: both; 16 | } 17 | 18 | /* blocks that must contain floats */ 19 | .clearfix, 20 | .#{$kna-namespace}line { 21 | &::after { 22 | content: ""; 23 | display: table; 24 | clear: both; 25 | border-collapse: collapse; 26 | } 27 | } 28 | 29 | /* simple blocks alignment */ 30 | .#{$kna-namespace}left { 31 | margin-right: auto; 32 | } 33 | 34 | .#{$kna-namespace}right { 35 | margin-left: auto; 36 | } 37 | 38 | .#{$kna-namespace}center { 39 | margin-left: auto; 40 | margin-right: auto; 41 | } 42 | 43 | /* text and contents alignment */ 44 | .#{$kna-namespace}txtleft { 45 | text-align: left; 46 | } 47 | 48 | .#{$kna-namespace}txtright { 49 | text-align: right; 50 | } 51 | 52 | .#{$kna-namespace}txtcenter { 53 | text-align: center; 54 | } 55 | 56 | /* floating elements */ 57 | .#{$kna-namespace}fl { 58 | float: left; 59 | } 60 | 61 | img.#{$kna-namespace}fl { 62 | margin-right: $small-value; 63 | } 64 | 65 | .#{$kna-namespace}fr { 66 | float: right; 67 | } 68 | 69 | img.#{$kna-namespace}fr { 70 | margin-left: $small-value; 71 | } 72 | 73 | img.#{$kna-namespace}fl, 74 | img.#{$kna-namespace}fr { 75 | margin-bottom: $tiny-value; 76 | } 77 | 78 | /* table layout */ 79 | .#{$kna-namespace}row { 80 | display: table; 81 | table-layout: fixed; 82 | width: 100%; 83 | } 84 | 85 | .#{$kna-namespace}row > *, 86 | .#{$kna-namespace}col { 87 | display: table-cell; 88 | vertical-align: top; 89 | } 90 | 91 | /* no table-cell for script tag when body is a .row */ 92 | body > script { 93 | display: none !important; 94 | } 95 | 96 | /* inline-block */ 97 | .#{$kna-namespace}inbl { 98 | display: inline-block; 99 | vertical-align: top; 100 | } 101 | 102 | /* flexbox layout 103 | http://www.alsacreations.com/tuto/lire/1493-css3-flexbox-layout-module.html 104 | */ 105 | 106 | [class*="#{$kna-namespace}flex-container"] { 107 | display : flex; 108 | flex-wrap: wrap; 109 | } 110 | 111 | .#{$kna-namespace}flex-container-h { 112 | flex-direction: row; 113 | } 114 | 115 | .#{$kna-namespace}flex-container-v { 116 | flex-direction: column; 117 | } 118 | 119 | .#{$kna-namespace}flex-item-fluid { 120 | flex: 1; 121 | min-width: 0; 122 | } 123 | 124 | .#{$kna-namespace}flex-item-first { 125 | order : -1; 126 | } 127 | 128 | .#{$kna-namespace}flex-item-medium { 129 | order : 0; 130 | } 131 | 132 | .#{$kna-namespace}flex-item-last { 133 | order : 1; 134 | } 135 | 136 | .#{$kna-namespace}flex-item-center { 137 | margin: auto; 138 | } 139 | -------------------------------------------------------------------------------- /src/styles/_03-grids.scss: -------------------------------------------------------------------------------- 1 | /* ---------------------------------- */ 2 | /* ==Grillade */ 3 | /* ---------------------------------- */ 4 | 5 | // Tuto : http://www.alsacreations.com/tuto/lire/1659-une-grille-responsive-avec-flexbox-et-LESS.html 6 | // Demo : http://codepen.io/raphaelgoetter/pen/ZYjwEB 7 | 8 | // Usage in vanilla CSS: 9 | // -
    for an equal fourth columns grid container 10 | // -
    for an uneven columns grid container 11 | 12 | // Usage with preprocessors : if you're using Sass, you can config grids variables : 13 | // n = number of columns (default = 4) / g = gutter value (default = 1em) 14 | // example : .grid-perso { @include grid(12, 10px); } 15 | // ... or uneven grids : 16 | // left = left ratio column (default = 2) / right = right ratio column (default = 1) 17 | // example : .grid-perso { @include uneven-grid(2, 1, 10px); } 18 | 19 | /* grid container */ 20 | [class*="#{$kna-namespace}grid-"] { 21 | display: flex; 22 | flex-direction: row; 23 | flex-wrap: wrap; 24 | margin-left: -$gutter; 25 | 26 | /* inline-block fallback for IE9 generation */ 27 | letter-spacing: -0.31em; 28 | } 29 | 30 | /* grid childs */ 31 | [class*="#{$kna-namespace}grid-"] > * { 32 | box-sizing: border-box; 33 | flex: 0 0 auto; 34 | width: calc(100% * 1 / #{$number} - #{$gutter} - .01px); 35 | min-width: 0; 36 | margin-left: $gutter; 37 | 38 | /* inline-block fallback for IE9 generation */ 39 | display: inline-block; 40 | vertical-align: top; 41 | letter-spacing: normal; 42 | } 43 | 44 | // Sass mixins for *equal* columns grid container 45 | // example : .grid-perso { @include grid(12); } 46 | @mixin grid($number:$number,$newgutter:$gutter) { 47 | @if $newgutter != $gutter { 48 | margin-left: -$newgutter; 49 | } 50 | & > * { 51 | width: calc(100% * 1 / #{$number} - #{$newgutter} - .01px); 52 | @if $newgutter != $gutter { 53 | margin-left: $newgutter; 54 | } 55 | } 56 | & > .#{$kna-namespace}flex-item-double { 57 | width: calc(100% * 2 / #{$number} - #{$newgutter}); 58 | } 59 | } 60 | 61 | // Examples : will be compiled in CSS 62 | 63 | [class*="#{$kna-namespace}grid-2"] { 64 | @include grid(2); 65 | } 66 | 67 | [class*="#{$kna-namespace}grid-3"] { 68 | @include grid(3); 69 | } 70 | 71 | [class*="#{$kna-namespace}grid-4"] { 72 | @include grid(4); 73 | } 74 | 75 | [class*="#{$kna-namespace}grid-5"] { 76 | @include grid(5); 77 | } 78 | 79 | [class*="#{$kna-namespace}grid-6"] { 80 | @include grid(6); 81 | } 82 | 83 | [class*="#{$kna-namespace}grid-7"] { 84 | @include grid(7); 85 | } 86 | 87 | [class*="#{$kna-namespace}grid-8"] { 88 | @include grid(8); 89 | } 90 | 91 | [class*="#{$kna-namespace}grid-10"] { 92 | @include grid(10); 93 | } 94 | 95 | [class*="#{$kna-namespace}grid-12"] { 96 | @include grid(12); 97 | } 98 | 99 | /* Responsive grid */ 100 | // "small-2" = 2 columns when small screen 101 | // example : .grid-4-small-2 will be 4 then 2 columns 102 | @media (max-width: $small-screen) { 103 | [class*="-small-4"] > * { 104 | width: calc(100% * 1 / 4 - #{$gutter} - .01px); 105 | } 106 | [class*="-small-4"] > .flex-item-double { 107 | width: calc(100% * 1 / 2 - #{$gutter} - .01px); 108 | } 109 | [class*="-small-3"] > * { 110 | width: calc(100% * 1 / 3 - #{$gutter} - .01px); 111 | } 112 | [class*="-small-3"] > .flex-item-double { 113 | width: calc(100% * 2 / 3 - #{$gutter} - .01px); 114 | } 115 | [class*="-small-2"] > * { 116 | width: calc(100% * 1 / 2 - #{$gutter} - .01px); 117 | } 118 | [class*="-small-2"] > .flex-item-double { 119 | width: calc(100% - #{$gutter} - .01px); 120 | } 121 | [class*="-small-1"] > * { 122 | width: calc(100% - #{$gutter} - .01px); 123 | } 124 | [class*="-small-1"] > .flex-item-double { 125 | width: calc(100% - #{$gutter} - .01px); 126 | } 127 | } 128 | // "tiny-1" = 1 column when tiny screen 129 | // example : .grid-4-small-2-tiny-1 will be 4 then 2 columns then 1 column 130 | @media (max-width: $tiny-screen) { 131 | [class*="-tiny-2"] > * { 132 | width: calc(100% * 1 / 2 - #{$gutter} - .01px); 133 | } 134 | [class*="-tiny-2"] > .flex-item-double { 135 | width: calc(100% - #{$gutter} - .01px); 136 | } 137 | [class*="-tiny-1"] > * { 138 | width: calc(100% - #{$gutter} - .01px); 139 | } 140 | [class*="-tiny-1"] > .flex-item-double { 141 | width: calc(100% - #{$gutter} - .01px); 142 | } 143 | } 144 | 145 | // Sass mixins for *unequal* columns grid container 146 | // example : .grid-perso { @include uneven-grid(2, 1); } 147 | @mixin uneven-grid($left:$left, $right:$right, $newgutter:$gutter) { 148 | @if $newgutter != $gutter { 149 | margin-left: -$newgutter; 150 | } 151 | > * { 152 | @if $newgutter != $gutter { 153 | margin-left: $newgutter; 154 | } 155 | &:nth-child(odd) { 156 | $size: ($left / ($left + $right)) * 100%; 157 | width: calc(#{$size} - #{$newgutter}); 158 | } 159 | &:nth-child(even) { 160 | $size: ($right / ($left + $right)) * 100%; 161 | width: calc(#{$size} - #{$newgutter}); 162 | } 163 | } 164 | @media (max-width: $small-screen) { 165 | & > *:nth-child(n) { 166 | width: calc(100% - #{$newgutter}); 167 | } 168 | } 169 | } 170 | 171 | 172 | // Examples : will be compiled in CSS 173 | 174 | .#{$kna-namespace}grid-2-1 { 175 | @include uneven-grid(2,1); 176 | } 177 | 178 | .#{$kna-namespace}grid-1-2 { 179 | @include uneven-grid(1,2); 180 | } 181 | 182 | .#{$kna-namespace}grid-3-1 { 183 | @include uneven-grid(3,1); 184 | } 185 | 186 | .#{$kna-namespace}grid-1-3 { 187 | @include uneven-grid(1,3); 188 | } 189 | 190 | .#{$kna-namespace}grid-3-2 { 191 | @include uneven-grid(3,2); 192 | } 193 | 194 | .#{$kna-namespace}grid-2-3 { 195 | @include uneven-grid(2,3); 196 | } 197 | 198 | .#{$kna-namespace}grid-4-1 { 199 | @include uneven-grid(4,1); 200 | } 201 | 202 | .#{$kna-namespace}grid-1-4 { 203 | @include uneven-grid(1,4); 204 | } 205 | 206 | .#{$kna-namespace}pull { 207 | margin-right: auto; 208 | } 209 | .#{$kna-namespace}push { 210 | margin-left: auto; 211 | } 212 | -------------------------------------------------------------------------------- /src/styles/_04-tables.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==tables */ 3 | /* ----------------------------- */ 4 | 5 | table, 6 | .#{$kna-namespace}table { 7 | width: 100%; 8 | max-width: 100%; 9 | table-layout: fixed; 10 | border-collapse: collapse; 11 | vertical-align: top; 12 | } 13 | 14 | .#{$kna-namespace}table { 15 | display: table; 16 | } 17 | 18 | #recaptcha_table, 19 | table.#{$kna-namespace}table-auto { 20 | table-layout:auto; 21 | } 22 | 23 | td, 24 | th { 25 | vertical-align: top; 26 | min-width: $medium-value; 27 | cursor: default; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/_05-forms.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==forms */ 3 | /* ----------------------------- */ 4 | 5 | /* thanks to HTML5boilerplate, 6 | * github.com/nathansmith/formalize and www.sitepen.com 7 | */ 8 | 9 | /* buttons */ 10 | .#{$kna-namespace}btn { 11 | display: inline-block; 12 | } 13 | 14 | /* forms items */ 15 | form, 16 | fieldset { 17 | border: none; 18 | } 19 | 20 | input, 21 | button, 22 | select, 23 | label, 24 | .#{$kna-namespace}btn { 25 | font-family: inherit; 26 | font-size: inherit; 27 | } 28 | 29 | button, 30 | input, 31 | optgroup, 32 | select, 33 | textarea { 34 | color: $base-color; 35 | } 36 | 37 | label { 38 | vertical-align: middle; 39 | cursor: pointer; 40 | } 41 | 42 | legend { 43 | border: 0; 44 | white-space: normal; 45 | } 46 | 47 | textarea { 48 | min-height: 5em; 49 | vertical-align: top; 50 | font-family: inherit; 51 | font-size: inherit; 52 | resize: vertical; 53 | } 54 | 55 | select { 56 | -webkit-appearance: menulist-button; 57 | } 58 | 59 | /* if select styling bugs on WebKit */ 60 | /* select { -webkit-appearance: none; } */ 61 | 62 | /* 'x' appears on right of search input when text is entered. This removes it */ 63 | input[type="search"]::-webkit-search-decoration, 64 | input[type="search"]::-webkit-search-cancel-button, 65 | input[type="search"]::-webkit-search-results-button, 66 | input[type="search"]::-webkit-search-results-decoration { 67 | display: none; 68 | } 69 | 70 | ::-webkit-input-placeholder { 71 | color: #777; 72 | } 73 | 74 | input:-moz-placeholder, 75 | textarea:-moz-placeholder { 76 | color: #777; 77 | } 78 | 79 | .#{$kna-namespace}btn:focus, 80 | input[type="button"]:focus, 81 | button:focus { 82 | -webkit-tap-highlight-color: transparent; 83 | -webkit-user-select: none; 84 | -moz-user-select: none; 85 | -ms-user-select: none; 86 | user-select: none; 87 | } 88 | 89 | /* unstyled forms */ 90 | 91 | button.#{$kna-namespace}unstyled, 92 | input[type="button"].#{$kna-namespace}unstyled, 93 | input[type="submit"].#{$kna-namespace}unstyled, 94 | input[type="reset"].#{$kna-namespace}unstyled { 95 | padding: 0; 96 | border: none; 97 | line-height: 1; 98 | text-align: left; 99 | background: none; 100 | border-radius: 0; 101 | box-shadow: none; 102 | -webkit-appearance: none; 103 | -moz-appearance: none; 104 | appearance: none; 105 | 106 | &:focus { 107 | box-shadow: none; 108 | outline: none; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/styles/_06-helpers.scss: -------------------------------------------------------------------------------- 1 | /* ---------------------------------- */ 2 | /* ==state helpers */ 3 | /* ---------------------------------- */ 4 | 5 | /* invisible for all */ 6 | .is-hidden, 7 | [hidden] { 8 | display: none; 9 | } 10 | 11 | /* hidden but not for an assistive technology like a screen reader, Yahoo! method */ 12 | .visually-hidden { 13 | position: absolute !important; 14 | border: 0 !important; 15 | height: 1px !important; 16 | width: 1px !important; 17 | padding: 0 !important; 18 | overflow: hidden !important; 19 | clip: rect(0, 0, 0, 0) !important; 20 | } 21 | 22 | .is-disabled, 23 | [disabled] { 24 | opacity: 0.5; 25 | pointer-events: none; 26 | cursor: not-allowed; 27 | filter: grayscale(1); 28 | } 29 | 30 | ul.is-unstyled, 31 | ul.unstyled { 32 | list-style: none; 33 | padding-left: 0; 34 | } 35 | 36 | /* ---------------------------------- */ 37 | /* ==visual helpers */ 38 | /* .. use them with parcimony ! */ 39 | /* ---------------------------------- */ 40 | 41 | /* blocks widths (percentage and pixels) */ 42 | .#{$kna-namespace}w10 { 43 | width: 10%; 44 | } 45 | 46 | .#{$kna-namespace}w20 { 47 | width: 20%; 48 | } 49 | 50 | .#{$kna-namespace}w25 { 51 | width: 25%; 52 | } 53 | 54 | .#{$kna-namespace}w30 { 55 | width: 30%; 56 | } 57 | 58 | .#{$kna-namespace}w33 { 59 | width: 33.3333%; 60 | } 61 | 62 | .#{$kna-namespace}w40 { 63 | width: 40%; 64 | } 65 | 66 | .#{$kna-namespace}w50 { 67 | width: 50%; 68 | } 69 | 70 | .#{$kna-namespace}w60 { 71 | width: 60%; 72 | } 73 | 74 | .#{$kna-namespace}w66 { 75 | width: 66.6666%; 76 | } 77 | 78 | .#{$kna-namespace}w70 { 79 | width: 70%; 80 | } 81 | 82 | .#{$kna-namespace}w75 { 83 | width: 75%; 84 | } 85 | 86 | .#{$kna-namespace}w80 { 87 | width: 80%; 88 | } 89 | 90 | .#{$kna-namespace}w90 { 91 | width: 90%; 92 | } 93 | 94 | .#{$kna-namespace}w100 { 95 | width: 100%; 96 | } 97 | 98 | .#{$kna-namespace}w50p { 99 | width: 50px; 100 | } 101 | 102 | .#{$kna-namespace}w100p { 103 | width: 100px; 104 | } 105 | 106 | .#{$kna-namespace}w150p { 107 | width: 150px; 108 | } 109 | 110 | .#{$kna-namespace}w200p { 111 | width: 200px; 112 | } 113 | 114 | .#{$kna-namespace}w300p { 115 | width: 300px; 116 | } 117 | 118 | .#{$kna-namespace}w400p { 119 | width: 400px; 120 | } 121 | 122 | .#{$kna-namespace}w500p { 123 | width: 500px; 124 | } 125 | 126 | .#{$kna-namespace}w600p { 127 | width: 600px; 128 | } 129 | 130 | .#{$kna-namespace}w700p { 131 | width: 700px; 132 | } 133 | 134 | .#{$kna-namespace}w800p { 135 | width: 800px; 136 | } 137 | 138 | .#{$kna-namespace}w960p { 139 | width: 960px; 140 | } 141 | 142 | .#{$kna-namespace}mw960p { 143 | max-width: 960px; 144 | } 145 | 146 | .#{$kna-namespace}w1140p { 147 | width: 1140px; 148 | } 149 | 150 | .#{$kna-namespace}mw1140p { 151 | max-width: 1140px; 152 | } 153 | 154 | .#{$kna-namespace}wauto { 155 | width: auto; 156 | } 157 | 158 | /* spacing helpers 159 | p,m = padding,margin 160 | a,t,r,b,l = all,top,right,bottom,left 161 | s,m,l,n = small, medium, large, none 162 | */ 163 | .#{$kna-namespace}man, 164 | .#{$kna-namespace}ma0 { 165 | margin: 0; 166 | } 167 | 168 | .#{$kna-namespace}pan, 169 | .#{$kna-namespace}pa0 { 170 | padding: 0; 171 | } 172 | 173 | .#{$kna-namespace}mas { 174 | margin: $small-value; 175 | } 176 | 177 | .#{$kna-namespace}mam { 178 | margin: $medium-value; 179 | } 180 | 181 | .#{$kna-namespace}mal { 182 | margin: $large-value; 183 | } 184 | 185 | .#{$kna-namespace}pas { 186 | padding: $small-value; 187 | } 188 | 189 | .#{$kna-namespace}pam { 190 | padding: $medium-value; 191 | } 192 | 193 | .#{$kna-namespace}pal { 194 | padding: $large-value; 195 | } 196 | 197 | .#{$kna-namespace}mtn, 198 | .#{$kna-namespace}mt0 { 199 | margin-top: 0; 200 | } 201 | 202 | .#{$kna-namespace}mts { 203 | margin-top: $small-value; 204 | } 205 | 206 | .#{$kna-namespace}mtm { 207 | margin-top: $medium-value; 208 | } 209 | 210 | .#{$kna-namespace}mtl { 211 | margin-top: $large-value; 212 | } 213 | 214 | .#{$kna-namespace}mrn, 215 | .#{$kna-namespace}mr0 { 216 | margin-right: 0; 217 | } 218 | 219 | .#{$kna-namespace}mrs { 220 | margin-right: $small-value; 221 | } 222 | 223 | .#{$kna-namespace}mrm { 224 | margin-right: $medium-value; 225 | } 226 | 227 | .#{$kna-namespace}mrl { 228 | margin-right: $large-value; 229 | } 230 | 231 | .#{$kna-namespace}mbn, 232 | .#{$kna-namespace}mb0 { 233 | margin-bottom: 0; 234 | } 235 | 236 | .#{$kna-namespace}mbs { 237 | margin-bottom: $small-value; 238 | } 239 | 240 | .#{$kna-namespace}mbm { 241 | margin-bottom: $medium-value; 242 | } 243 | 244 | .#{$kna-namespace}mbl { 245 | margin-bottom: $large-value; 246 | } 247 | 248 | .#{$kna-namespace}mln, 249 | .#{$kna-namespace}ml0 { 250 | margin-left: 0; 251 | } 252 | 253 | .#{$kna-namespace}mls { 254 | margin-left: $small-value; 255 | } 256 | 257 | .#{$kna-namespace}mlm { 258 | margin-left: $medium-value; 259 | } 260 | 261 | .#{$kna-namespace}mll { 262 | margin-left: $large-value; 263 | } 264 | 265 | .#{$kna-namespace}mauto { 266 | margin: auto; 267 | } 268 | 269 | .#{$kna-namespace}mtauto { 270 | margin-top: auto; 271 | } 272 | 273 | .#{$kna-namespace}mrauto { 274 | margin-right: auto; 275 | } 276 | 277 | .#{$kna-namespace}mbauto { 278 | margin-bottom: auto; 279 | } 280 | 281 | .#{$kna-namespace}mlauto { 282 | margin-left: auto; 283 | } 284 | 285 | .#{$kna-namespace}ptn, 286 | .#{$kna-namespace}pt0 { 287 | padding-top: 0; 288 | } 289 | 290 | .#{$kna-namespace}pts { 291 | padding-top: $small-value; 292 | } 293 | 294 | .#{$kna-namespace}ptm { 295 | padding-top: $medium-value; 296 | } 297 | 298 | .#{$kna-namespace}ptl { 299 | padding-top: $large-value; 300 | } 301 | 302 | .#{$kna-namespace}prn, 303 | .#{$kna-namespace}pr0 { 304 | padding-right: 0; 305 | } 306 | 307 | .#{$kna-namespace}prs { 308 | padding-right: $small-value; 309 | } 310 | 311 | .#{$kna-namespace}prm { 312 | padding-right: $medium-value; 313 | } 314 | 315 | .#{$kna-namespace}prl { 316 | padding-right: $large-value; 317 | } 318 | 319 | .#{$kna-namespace}pbn, 320 | .#{$kna-namespace}pb0 { 321 | padding-bottom: 0; 322 | } 323 | 324 | .#{$kna-namespace}pbs { 325 | padding-bottom: $small-value; 326 | } 327 | 328 | .#{$kna-namespace}pbm { 329 | padding-bottom: $medium-value; 330 | } 331 | 332 | .#{$kna-namespace}pbl { 333 | padding-bottom: $large-value; 334 | } 335 | 336 | .#{$kna-namespace}pln, 337 | .#{$kna-namespace}pl0 { 338 | padding-left: 0; 339 | } 340 | 341 | .#{$kna-namespace}pls { 342 | padding-left: $small-value; 343 | } 344 | 345 | .#{$kna-namespace}plm { 346 | padding-left: $medium-value; 347 | } 348 | 349 | .#{$kna-namespace}pll { 350 | padding-left: $large-value; 351 | } 352 | -------------------------------------------------------------------------------- /src/styles/_07-responsive.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==desktop and HD devices */ 3 | /* ----------------------------- */ 4 | 5 | @media (min-width: ($medium-screen + 1)) { 6 | /* rules for big resources and big screens like: background-images, font-faces, etc. */ 7 | } 8 | 9 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx) { 10 | /* style adjustments for high density devices */ 11 | } 12 | 13 | /* ---------------------------------- */ 14 | /* ==Responsive large */ 15 | /* ---------------------------------- */ 16 | 17 | @media (min-width: ($medium-screen + 1)) { 18 | 19 | /* layouts for large screens */ 20 | .#{$kna-namespace}large-hidden { 21 | display: none !important; 22 | } 23 | 24 | .#{$kna-namespace}large-visible { 25 | display: block !important; 26 | } 27 | 28 | .#{$kna-namespace}large-no-float { 29 | float: none; 30 | } 31 | 32 | .#{$kna-namespace}large-inbl { 33 | display: inline-block; 34 | float: none; 35 | vertical-align: top; 36 | } 37 | .#{$kna-namespace}large-row { 38 | display: table; 39 | table-layout: fixed; 40 | width: 100% !important; 41 | } 42 | .#{$kna-namespace}large-col { 43 | display: table-cell; 44 | vertical-align: top; 45 | } 46 | 47 | /* widths for large screens */ 48 | .#{$kna-namespace}large-w25 { 49 | width: 25% !important; 50 | } 51 | 52 | .#{$kna-namespace}large-w33 { 53 | width: 33.3333% !important; 54 | } 55 | 56 | .#{$kna-namespace}large-w50 { 57 | width: 50% !important; 58 | } 59 | 60 | .#{$kna-namespace}large-w66 { 61 | width: 66.6666% !important; 62 | } 63 | 64 | .#{$kna-namespace}large-w75 { 65 | width: 75% !important; 66 | } 67 | 68 | .#{$kna-namespace}large-w100, 69 | .#{$kna-namespace}large-wauto { 70 | display: block !important; 71 | float: none !important; 72 | clear: none !important; 73 | width: auto !important; 74 | margin-left: 0 !important; 75 | margin-right: 0 !important; 76 | border: 0; 77 | } 78 | 79 | /* margins for large screens */ 80 | .#{$kna-namespace}large-man, 81 | .#{$kna-namespace}large-ma0 { 82 | margin: 0 !important; 83 | } 84 | } 85 | 86 | /* ---------------------------------- */ 87 | /* ==Responsive medium */ 88 | /* ---------------------------------- */ 89 | 90 | @media (min-width: ($small-screen + 1)) and (max-width: $medium-screen) { 91 | 92 | /* layouts for medium screens */ 93 | .#{$kna-namespace}medium-hidden { 94 | display: none !important; 95 | } 96 | 97 | .#{$kna-namespace}medium-visible { 98 | display: block !important; 99 | } 100 | 101 | .#{$kna-namespace}medium-no-float { 102 | float: none; 103 | } 104 | 105 | .#{$kna-namespace}medium-inbl { 106 | display: inline-block; 107 | float: none; 108 | vertical-align: top; 109 | } 110 | 111 | .#{$kna-namespace}medium-row { 112 | display: table; 113 | table-layout: fixed; 114 | width: 100% !important; 115 | } 116 | 117 | .#{$kna-namespace}medium-col { 118 | display: table-cell; 119 | vertical-align: top; 120 | } 121 | 122 | /* widths for medium screens */ 123 | .#{$kna-namespace}medium-w25 { 124 | width: 25% !important; 125 | } 126 | 127 | .#{$kna-namespace}medium-w33 { 128 | width: 33.3333% !important; 129 | } 130 | 131 | .#{$kna-namespace}medium-w50 { 132 | width: 50% !important; 133 | } 134 | 135 | .#{$kna-namespace}medium-w66 { 136 | width: 66.6666% !important; 137 | } 138 | 139 | .#{$kna-namespace}medium-w75 { 140 | width: 75% !important; 141 | } 142 | 143 | .#{$kna-namespace}medium-w100, 144 | .#{$kna-namespace}medium-wauto { 145 | display: block !important; 146 | float: none !important; 147 | clear: none !important; 148 | width: auto !important; 149 | margin-left: 0 !important; 150 | margin-right: 0 !important; 151 | border: 0; 152 | } 153 | 154 | /* margins for medium screens */ 155 | .#{$kna-namespace}medium-man, 156 | .#{$kna-namespace}medium-ma0 { 157 | margin: 0 !important; 158 | } 159 | } 160 | 161 | /* ---------------------------------- */ 162 | /* ==Responsive small */ 163 | /* ---------------------------------- */ 164 | 165 | @media (min-width: ($tiny-screen + 1)) and (max-width: $small-screen) { 166 | 167 | /* quick reset in small resolution and less */ 168 | .#{$kna-namespace}w600p, 169 | .#{$kna-namespace}w700p, 170 | .#{$kna-namespace}w800p, 171 | .#{$kna-namespace}w960p, 172 | .#{$kna-namespace}mw960p { 173 | width: auto; 174 | float: none; 175 | } 176 | 177 | /* layouts for small screens */ 178 | .#{$kna-namespace}small-hidden { 179 | display: none !important; 180 | } 181 | 182 | .#{$kna-namespace}small-visible { 183 | display: block !important; 184 | } 185 | 186 | .#{$kna-namespace}small-no-float { 187 | float: none; 188 | } 189 | 190 | .#{$kna-namespace}small-inbl { 191 | display: inline-block; 192 | float: none; 193 | vertical-align: top; 194 | } 195 | 196 | .#{$kna-namespace}small-row { 197 | display: table !important; 198 | table-layout: fixed !important; 199 | width: 100% !important; 200 | } 201 | 202 | .#{$kna-namespace}small-col { 203 | display: table-cell !important; 204 | vertical-align: top !important; 205 | } 206 | 207 | /* widths for small screens */ 208 | .#{$kna-namespace}small-w25 { 209 | width: 25% !important; 210 | } 211 | 212 | .#{$kna-namespace}small-w33 { 213 | width: 33.3333% !important; 214 | } 215 | 216 | .#{$kna-namespace}small-w50 { 217 | width: 50% !important; 218 | } 219 | 220 | .#{$kna-namespace}small-w66 { 221 | width: 66.6666% !important; 222 | } 223 | 224 | .#{$kna-namespace}small-w75 { 225 | width: 75% !important; 226 | } 227 | 228 | .#{$kna-namespace}small-w100, 229 | .#{$kna-namespace}small-wauto { 230 | display: block !important; 231 | float: none !important; 232 | clear: none !important; 233 | width: auto !important; 234 | margin-left: 0 !important; 235 | margin-right: 0 !important; 236 | border: 0; 237 | } 238 | 239 | /* margins for small screens */ 240 | .#{$kna-namespace}small-man, 241 | .#{$kna-namespace}small-ma0 { 242 | margin: 0 !important; 243 | } 244 | 245 | .#{$kna-namespace}small-pan, 246 | .#{$kna-namespace}small-pa0 { 247 | padding: 0 !important; 248 | } 249 | 250 | } 251 | 252 | /* ---------------------------------- */ 253 | /* ==Responsive tiny */ 254 | /* ---------------------------------- */ 255 | 256 | @media (max-width: $tiny-screen) { 257 | 258 | /* quick small resolution reset */ 259 | .#{$kna-namespace}mod, 260 | .#{$kna-namespace}col, 261 | fieldset { 262 | display: block !important; 263 | float: none !important; 264 | clear: none !important; 265 | width: auto !important; 266 | margin-left: 0 !important; 267 | margin-right: 0 !important; 268 | border: 0; 269 | } 270 | 271 | .#{$kna-namespace}flex-container { 272 | flex-direction: column; 273 | } 274 | 275 | .#{$kna-namespace}w300p, 276 | .#{$kna-namespace}w400p, 277 | .#{$kna-namespace}w500p { 278 | width: auto; 279 | float: none; 280 | } 281 | 282 | .#{$kna-namespace}row { 283 | display: block !important; 284 | width: 100% !important; 285 | } 286 | 287 | /* layouts for tiny screens */ 288 | .#{$kna-namespace}tiny-hidden { 289 | display: none !important; 290 | } 291 | 292 | .#{$kna-namespace}tiny-visible { 293 | display: block !important; 294 | } 295 | 296 | .#{$kna-namespace}tiny-no-float { 297 | float: none; 298 | } 299 | 300 | .#{$kna-namespace}tiny-inbl { 301 | display: inline-block; 302 | float: none; 303 | vertical-align: top; 304 | } 305 | 306 | .#{$kna-namespace}tiny-row { 307 | display: table !important; 308 | table-layout: fixed !important; 309 | width: 100% !important; 310 | } 311 | 312 | .#{$kna-namespace}tiny-col { 313 | display: table-cell !important; 314 | vertical-align: top !important; 315 | } 316 | 317 | th, 318 | td { 319 | display: block; 320 | width: auto; 321 | text-align: left; 322 | } 323 | 324 | thead { 325 | display: none; 326 | } 327 | 328 | /* widths for tiny screens */ 329 | .#{$kna-namespace}tiny-w25 { 330 | width: 25% !important; 331 | } 332 | 333 | .#{$kna-namespace}tiny-w33 { 334 | width: 33.3333% !important; 335 | } 336 | 337 | .#{$kna-namespace}tiny-w50 { 338 | width: 50% !important; 339 | } 340 | 341 | .#{$kna-namespace}tiny-w66 { 342 | width: 66.6666% !important; 343 | } 344 | 345 | .#{$kna-namespace}tiny-w75 { 346 | width: 75% !important; 347 | } 348 | 349 | .#{$kna-namespace}tiny-w100, 350 | .#{$kna-namespace}tiny-wauto { 351 | display: block !important; 352 | float: none !important; 353 | clear: none !important; 354 | width: auto !important; 355 | margin-left: 0 !important; 356 | margin-right: 0 !important; 357 | border: 0; 358 | } 359 | 360 | /* margins for tiny screens */ 361 | .#{$kna-namespace}tiny-man, 362 | .#{$kna-namespace}tiny-ma0 { 363 | margin: 0 !important; 364 | } 365 | 366 | .#{$kna-namespace}tiny-pan, 367 | .#{$kna-namespace}tiny-pa0 { 368 | padding: 0 !important; 369 | } 370 | 371 | } 372 | -------------------------------------------------------------------------------- /src/styles/_08-print.scss: -------------------------------------------------------------------------------- 1 | /* quick print reset */ 2 | @media print { 3 | * { 4 | background: transparent !important; 5 | box-shadow: none !important; 6 | text-shadow: none !important; 7 | } 8 | 9 | body { 10 | width: auto; 11 | margin: auto; 12 | font-family: serif; 13 | font-size: 12pt; 14 | } 15 | 16 | p, 17 | .#{$kna-namespace}p-like, 18 | h1, 19 | .#{$kna-namespace}h1-like, 20 | h2, 21 | .#{$kna-namespace}h2-like, 22 | h3, 23 | .#{$kna-namespace}h3-like, 24 | h4, 25 | .#{$kna-namespace}h4-like, 26 | h5, 27 | .#{$kna-namespace}h5-like, 28 | h6, 29 | .#{$kna-namespace}h6-like, 30 | blockquote, 31 | ul, 32 | ol { 33 | color: #000; 34 | margin: auto; 35 | } 36 | 37 | .#{$kna-namespace}print { 38 | display: block; 39 | } 40 | 41 | .#{$kna-namespace}no-print { 42 | display: none; 43 | } 44 | 45 | /* no orphans, no widows */ 46 | p, 47 | .#{$kna-namespace}p-like, 48 | blockquote { 49 | orphans: 3; 50 | widows: 3; 51 | } 52 | 53 | /* no breaks inside these elements */ 54 | blockquote, 55 | ul, 56 | ol { 57 | page-break-inside: avoid; 58 | } 59 | 60 | /* page break before main headers 61 | h1, 62 | .h1-like { 63 | page-break-before: always; 64 | } 65 | */ 66 | 67 | /* no breaks after these elements */ 68 | h1, 69 | .#{$kna-namespace}h1-like, 70 | h2, 71 | .#{$kna-namespace}h2-like, 72 | h3, 73 | .#{$kna-namespace}h3-like, 74 | caption { 75 | page-break-after: avoid; 76 | } 77 | 78 | a { 79 | color: #000; 80 | } 81 | 82 | /* displaying URLs 83 | a[href]::after { 84 | content: " (" attr(href) ")"; 85 | } 86 | */ 87 | 88 | a[href^="javascript:"]::after, 89 | a[href^="#"]::after { 90 | content: ""; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/styles/_09-misc.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==misc rules */ 3 | /* ----------------------------- */ 4 | 5 | /* styling skip links */ 6 | .#{$kna-namespace}skip-links { 7 | position: absolute; 8 | 9 | & a { 10 | position: absolute; 11 | overflow: hidden; 12 | clip: rect(1px, 1px, 1px, 1px); 13 | padding: 0.5em; 14 | background: black; 15 | color: white; 16 | text-decoration: none; 17 | 18 | &:focus { 19 | position: static; 20 | overflow: visible; 21 | clip: auto; 22 | } 23 | } 24 | } 25 | 26 | // hyphens on small screens 27 | @media (max-width: $small-screen) { 28 | /* you shall not pass */ 29 | div, 30 | textarea, 31 | table, 32 | td, 33 | th, 34 | code, 35 | pre, 36 | samp { 37 | word-wrap: break-word; 38 | hyphens: auto; 39 | } 40 | } 41 | 42 | // use .no-wrapping to disallow hyphens on small screens 43 | @media (max-width: $small-screen) { 44 | .no-wrapping { 45 | word-wrap: normal; 46 | hyphens: manual; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/_10-styling.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==minor stylings */ 3 | /* ----------------------------- */ 4 | 5 | /* styling elements */ 6 | code, kbd, mark { 7 | border-radius: 2px; 8 | } 9 | 10 | kbd { 11 | padding: 0 2px; 12 | border: 1px solid #999; 13 | } 14 | 15 | code { 16 | padding: 2px 4px; 17 | background: rgba(0,0,0,0.04); 18 | color: #b11; 19 | } 20 | 21 | pre code { 22 | padding: 0; 23 | background: none; 24 | color: inherit; 25 | border-radius: 0; 26 | } 27 | 28 | mark { 29 | padding:2px 4px; 30 | } 31 | 32 | sup, 33 | sub { 34 | vertical-align: 0; 35 | } 36 | 37 | sup { 38 | bottom: 1ex; 39 | } 40 | 41 | sub { 42 | top: 0.5ex; 43 | } 44 | 45 | blockquote { 46 | position: relative; 47 | padding-left: 3em; 48 | } 49 | 50 | blockquote::before { 51 | content: "\201C"; 52 | position: absolute; 53 | left: 0; 54 | top: 0; 55 | font-family: georgia, serif; 56 | font-size: 5em; 57 | line-height: 0.9; 58 | color: rgba(0, 0, 0, .3); 59 | } 60 | 61 | blockquote > footer { 62 | margin-top: .75em; 63 | font-size: 0.9em; 64 | color: rgba(0, 0, 0, .7); 65 | } 66 | 67 | blockquote > footer::before { 68 | content: "\2014 \0020"; 69 | } 70 | 71 | q { 72 | font-style: normal; 73 | } 74 | 75 | q, 76 | .#{$kna-namespace}q { 77 | quotes: "“\00a0" "\00a0”"; 78 | } 79 | 80 | q:lang(fr), 81 | .#{$kna-namespace}q:lang(fr) { 82 | quotes: "«\00a0" "\00a0»"; 83 | } 84 | 85 | hr { 86 | display: block; 87 | clear: both; 88 | height: 1px; 89 | margin: 1em 0 2em; 90 | padding: 0; 91 | border: 0; 92 | color: #ccc; 93 | background-color: #ccc; 94 | } 95 | 96 | /* tables */ 97 | table, 98 | .#{$kna-namespace}table { 99 | border: 1px solid #ccc; 100 | } 101 | 102 | caption { 103 | padding: $small-value; 104 | color: #555; 105 | font-style: italic; 106 | } 107 | 108 | td, 109 | th { 110 | padding: 0.3em 0.8em; 111 | border: 1px #aaa dotted; 112 | text-align: left; 113 | } 114 | -------------------------------------------------------------------------------- /src/styles/_11-wordpress.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==WordPress reset */ 3 | /* ----------------------------- */ 4 | 5 | /* 6 | Author: Geoffrey Crofte, Alsacréations 7 | Contributors: Automattic, Geoffrey Crofte 8 | Description: Reset styles for WordPress usage of KNACSS 9 | */ 10 | 11 | /* ----------------------------- */ 12 | /* ==Menus */ 13 | /* ----------------------------- */ 14 | 15 | // current menu elements 16 | .current_page_item > a { 17 | } 18 | .current-menu-item > a { 19 | } 20 | .current_page_ancestor > a { 21 | } 22 | 23 | // blocks of content navigation 24 | .comment-navigation, 25 | .paging-navigation, 26 | .post-navigation { 27 | margin: 0 0 1.5em; 28 | overflow: hidden; 29 | } 30 | 31 | .comment-navigation .nav-previous, 32 | .paging-navigation .nav-previous, 33 | .post-navigation .nav-previous { 34 | float: left; 35 | width: 50%; 36 | } 37 | 38 | .comment-navigation .nav-next, 39 | .paging-navigation .nav-next, 40 | .post-navigation .nav-next { 41 | float: right; 42 | text-align: right; 43 | width: 50%; 44 | } 45 | 46 | /* ----------------------------- */ 47 | /* ==Alignments */ 48 | /* ----------------------------- */ 49 | 50 | // class in img elements 51 | .alignnone { 52 | margin: .25em 1.5em 1.5em 0; 53 | } 54 | 55 | .aligncenter { 56 | clear: both; 57 | display: block; 58 | margin: 1.5em auto; 59 | } 60 | 61 | .alignleft { 62 | float: left; 63 | margin: 0 1.5em .25em 0; 64 | } 65 | 66 | .alignright { 67 | float: right; 68 | margin: 0 0 .25em 1.5em; 69 | } 70 | 71 | /* ----------------------------- */ 72 | /* ==Clearings */ 73 | /* ----------------------------- */ 74 | 75 | .entry-content, 76 | .comment-content { 77 | clear: both; 78 | 79 | &::after, &::before { 80 | content: ""; 81 | display: table; 82 | } 83 | } 84 | 85 | /* ----------------------------- */ 86 | /* ==Widgets */ 87 | /* ----------------------------- */ 88 | 89 | .widget + .widget { 90 | margin: 1.5em 0 0; 91 | } 92 | 93 | // usage example: 94 | .widget select { 95 | max-width: 100%; 96 | } 97 | 98 | /* ----------------------------- */ 99 | /* ==Posts and pages */ 100 | /* ----------------------------- */ 101 | 102 | /* === 5.1 Posts - post_class === */ 103 | 104 | // featured content 105 | .sticky { 106 | } 107 | 108 | // attachment post 109 | .attachment { 110 | } 111 | 112 | // format of post 113 | .format- { 114 | &aside { 115 | } 116 | &gallery { 117 | } 118 | &link { 119 | } 120 | &image { 121 | } 122 | "e { 123 | } 124 | &status { 125 | } 126 | &video { 127 | } 128 | &chat { 129 | } 130 | } 131 | 132 | // class for a tag 133 | .tag- { 134 | &name-of-tag { 135 | } 136 | } 137 | 138 | // class for categorie 139 | .category- { 140 | &name-of-category { 141 | } 142 | } 143 | 144 | /* === 5.2 Pages - body_class === */ 145 | 146 | // front page 147 | .home { 148 | // if display posts 149 | &.blog { 150 | } 151 | // if static page 152 | &.page { 153 | } 154 | } 155 | 156 | // page displays posts 157 | .blog { 158 | // if is frontpage 159 | &.home { 160 | } 161 | // if static page 162 | &.page { 163 | } 164 | } 165 | 166 | // simple page 167 | .page { 168 | } 169 | 170 | // page of single post 171 | .single { 172 | } 173 | 174 | // page of archives 175 | .archive { 176 | } 177 | 178 | // page of search 179 | .search { 180 | // if has results 181 | .search-results { 182 | } 183 | // if has no results 184 | .search-no-results { 185 | } 186 | } 187 | 188 | // page 404 189 | .error404 { 190 | } 191 | 192 | // user logged in 193 | .logged-in { 194 | } 195 | 196 | // text direction if right-to-left 197 | // prefer rtl.css: http://codex.wordpress.org/Right-to-Left_Language_Support 198 | .rtl { 199 | } 200 | 201 | /* === 5.3 Posts and Pages - Contents === */ 202 | 203 | .hentry { 204 | margin: 0 0 1.5em; 205 | } 206 | 207 | .page-content, 208 | .entry-content, 209 | .entry-summary { 210 | margin: 1.5em 0 0; 211 | } 212 | 213 | .page-links { 214 | clear: both; 215 | margin: 0 0 1.5em; 216 | } 217 | 218 | /* ----------------------------- */ 219 | /* ==Comments */ 220 | /* ----------------------------- */ 221 | 222 | .comment-content a { 223 | word-wrap: break-word; 224 | } 225 | 226 | .bypostauthor { 227 | // some make-the-logo-bigger styles 228 | } 229 | 230 | /* ----------------------------- */ 231 | /* ==Media */ 232 | /* ----------------------------- */ 233 | 234 | img.wp-smiley { 235 | margin-bottom: 0; 236 | margin-top: 0; 237 | padding: 0; 238 | border: none; 239 | } 240 | 241 | /* ----------------------------- */ 242 | /* ==Captions */ 243 | /* ----------------------------- */ 244 | 245 | .wp-caption { 246 | max-width: 100%; 247 | margin-bottom: 1.5em; 248 | } 249 | 250 | .wp-caption img { 251 | display: block; 252 | margin: 0 auto; 253 | } 254 | 255 | .wp-caption-text { 256 | margin: 1em 0; 257 | text-align: center; 258 | } 259 | 260 | /* ----------------------------- */ 261 | /* ==Galleries */ 262 | /* ----------------------------- */ 263 | 264 | .gallery { 265 | margin-bottom: 1.5em; 266 | } 267 | 268 | .gallery-item { 269 | display: inline-block; 270 | width: 100%; 271 | text-align: center; 272 | vertical-align: top; 273 | 274 | @for $i from 2 through 9 { 275 | .gallery-columns-#{$i} & { 276 | $w: floor(10000/$i)/100; 277 | max-width: unquote($w + '%'); 278 | } 279 | } 280 | } 281 | 282 | .gallery-caption { 283 | display: block; 284 | } 285 | -------------------------------------------------------------------------------- /src/styles/_20-rp-config.scss: -------------------------------------------------------------------------------- 1 | /* config, set your color preferences */ 2 | 3 | $appbar-color: #20B2AA; // lightseagreen;// * 0.90; /* Turquoise; */ 4 | $bar-color: $appbar-color; /* Turquoise; */ 5 | $form-color: white; 6 | $header-color: snow; //$appbar-color * 1.1; 7 | $footer-color: lightgray; 8 | $border-color: lightgray; 9 | $tab-color: white; 10 | $hoover-color: #D3D3D3 * 1.1; 11 | -------------------------------------------------------------------------------- /src/styles/_21-rp-body.scss: -------------------------------------------------------------------------------- 1 | /* reset for header - footer */ 2 | html { 3 | height: 100%; 4 | box-sizing: border-box; 5 | } 6 | 7 | *, 8 | *::before, 9 | *::after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | position: relative; 15 | margin: 0; 16 | padding-bottom: 2rem; 17 | min-height: 100%; 18 | } 19 | 20 | .rp-appbar { 21 | background-color: $appbar-color; 22 | } 23 | 24 | .rp-appbar a { 25 | color: black; 26 | text-decoration: none; 27 | height: 27px; 28 | font-size: 1.6rem 29 | } 30 | .rp-appbar select { 31 | height: 25px; 32 | font-size: 1.6rem; 33 | background-color: $appbar-color * 1.2; 34 | } 35 | .rp-appbar div { 36 | font-size: 1.6rem; 37 | } 38 | 39 | .rp-appbar a:hover { 40 | color: midnightblue; 41 | } 42 | 43 | .rp-bar { 44 | background-color: $bar-color; 45 | color: white 46 | } 47 | 48 | .rp-rbar-item { 49 | float: right; 50 | padding: 1rem; 51 | } 52 | 53 | .rp-lbar-item { 54 | padding: 1rem; 55 | } 56 | /* */ 57 | 58 | .rp-footer { 59 | position: absolute; 60 | right: 0; 61 | bottom: 0; 62 | left: 0; 63 | padding: .2rem; 64 | background-color: $footer-color; 65 | text-align: center; 66 | } 67 | /* */ 68 | -------------------------------------------------------------------------------- /src/styles/_22-rp-popup.scss: -------------------------------------------------------------------------------- 1 | /* 2 | rp-popup-* 3 | */ 4 | .rp-popup { 5 | position: absolute; 6 | left: 50%; 7 | top: 100px; 8 | margin: 0 0 0 -25%; 9 | background: white; 10 | z-index: 1000; 11 | } 12 | 13 | .rp-popup-behind { 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | z-index: 999; 18 | width: 100%; 19 | background : rgba(255,255,255,0.6); 20 | } 21 | 22 | .rp-popup-header { 23 | background-color: $header-color; 24 | border-style: solid; 25 | border-width: 1px; 26 | height: 27px; 27 | width: 100%; 28 | border-color: $border-color; 29 | padding-top: 2px; 30 | padding-left: 2px; 31 | padding-right: 2px; 32 | font-size: 1.6rem; 33 | } 34 | 35 | .rp-popup-msg { 36 | background-color: white; 37 | border-bottom-style: solid; 38 | border-bottom-width: 1px; 39 | border-bottom-color: $border-color; 40 | height: 80px; 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/_23-rp-table.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------- */ 2 | /* ==rp-table */ 3 | /* ----------------------------- */ 4 | 5 | table tbody tr:hover { 6 | background-color: $hoover-color 7 | } -------------------------------------------------------------------------------- /src/styles/_24-rp-widget.scss: -------------------------------------------------------------------------------- 1 | /* 2 | rp-widget-* 3 | */ 4 | .rp-widget { 5 | border-style: solid; 6 | border-width: 1px; 7 | border-color: $border-color; 8 | } 9 | 10 | .rp-widget-icon { 11 | float: right; 12 | margin: 4px; 13 | } 14 | 15 | .rp-widget-header { 16 | background-color: $header-color; 17 | border-style: solid; 18 | border-width: 1px; 19 | height: 27px; 20 | width: 100%; 21 | border-color: $border-color; 22 | padding-top: 2px; 23 | padding-left: 2px; 24 | padding-right: 2px; 25 | font-size: 1.6rem; 26 | } 27 | 28 | .rp-widget-button { 29 | background-color: white; 30 | border-color: gray; 31 | border-style: solid; 32 | border-width: 1px; 33 | } 34 | 35 | .rp-widget-button:hover { 36 | background-color: $hoover-color; 37 | border-color: gray; 38 | border-style: solid; 39 | border-width: 1px; 40 | } 41 | 42 | .rp-widget-tool { 43 | width: 140px; 44 | background-color: $hoover-color; 45 | } 46 | 47 | .react-grid-item.react-grid-placeholder { 48 | background: white !important; 49 | } 50 | 51 | /* */ 52 | -------------------------------------------------------------------------------- /src/styles/_25-rp-dashboard.scss: -------------------------------------------------------------------------------- 1 | /* 2 | rp-dashobard-* 3 | */ 4 | .rp-dashboard-button { 5 | background-color: white; 6 | border-color: gray; 7 | border-style: solid; 8 | border-width: 1px; 9 | } 10 | 11 | .rp-dashboard-button:hover { 12 | background-color: $hoover-color; 13 | border-color: gray; 14 | border-style: solid; 15 | border-width: 1px; 16 | } 17 | /* */ 18 | -------------------------------------------------------------------------------- /src/styles/_26-rp-search.scss: -------------------------------------------------------------------------------- 1 | /* 2 | rp-search-* 3 | */ 4 | .rp-search-tool { 5 | float: left; 6 | margin-top: 9px; 7 | } 8 | 9 | .rp-search-after { 10 | margin-top: 4px; 11 | float: left; 12 | display: none; 13 | } 14 | 15 | #asearchtool:hover #bsearchtool { 16 | display: block; 17 | margin-right: -350px; 18 | } 19 | /* */ -------------------------------------------------------------------------------- /src/styles/_27-rp-form.scss: -------------------------------------------------------------------------------- 1 | /**/ 2 | /* rp-form-small */ 3 | /**/ 4 | .rp-form-small { 5 | width : 45rem; 6 | background-color: $form-color; 7 | border-color: gray; 8 | border-style: solid; 9 | border-width: 1px; 10 | margin: 4px; 11 | } 12 | .rp-field-error { 13 | color: red; 14 | margin-left : 4px; 15 | } 16 | .rp-form-error { 17 | color: red; 18 | } 19 | 20 | form.rp-form-small label { 21 | @extend .kna-txtright; 22 | display: inline-block; 23 | margin-bottom: 1.2rem; 24 | margin-right: .4rem; 25 | width: 15rem; 26 | } 27 | form.rp-form-small label[required]::after { 28 | content: " *"; 29 | color: red; 30 | } 31 | form.rp-form-small input { 32 | width: 28rem; 33 | } 34 | form.rp-form-small select { 35 | height: 27px; 36 | margin-left: 4px; 37 | width: 28rem; 38 | } 39 | form.rp-form-small textarea { 40 | width: 28rem; 41 | } 42 | div.rp-form-button { 43 | margin-top: 4px; 44 | } 45 | 46 | /* end rp-from-small */ 47 | -------------------------------------------------------------------------------- /src/styles/_28-rp-tabbar.scss: -------------------------------------------------------------------------------- 1 | /* 2 | rp-tabbar 3 | */ 4 | .rp-tabbar { 5 | display: inline-block; 6 | margin-top: 6px; 7 | margin-left: 20px; 8 | } 9 | 10 | ul.rp-tabbar { 11 | display: table; 12 | list-style-type: none; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | ul.rp-tabbar>li { 18 | float: left; 19 | padding: 3px; 20 | padding-left: 5px; 21 | padding-right: 5px; 22 | background-color: white; 23 | } 24 | 25 | ul.rp-tabbar>li:hover { 26 | background-color: $hoover-color; 27 | } 28 | 29 | ul.rp-tabbar>li.selected { 30 | background-color: blue; 31 | } 32 | 33 | div.rp-tabbar-content { 34 | border-bottom: 1px; 35 | border-bottom-style: solid; 36 | border-bottom-color: $border-color; 37 | } 38 | /* */ 39 | -------------------------------------------------------------------------------- /src/styles/knacss.scss: -------------------------------------------------------------------------------- 1 | // Sass config file 2 | // ----------------- 3 | 4 | // (WARNING : you should comment this line and move config file from vendor/knacss folder to your own folder) 5 | @import "_00-config"; 6 | 7 | // normalize include 8 | @import "_01a-normalize"; // normalize 9 | 10 | // Sass base styles 11 | @import "_01b-base"; // reset and basic styles 12 | 13 | // Sass files : chose the ones you need 14 | @import "_02-layout"; // alignment, modules, positionning 15 | @import "_03-grids"; // grids 16 | @import "_04-tables"; // data tables consistency 17 | @import "_05-forms"; // forms consistency 18 | @import "_06-helpers"; // width and spacers visually classes 19 | @import "_07-responsive"; // Responsive Web Design helpers 20 | @import "_08-print"; // print quick reset 21 | @import "_09-misc"; // skip links, google maps and hyphens 22 | @import "_10-styling"; // minor stylings 23 | 24 | // WordPress base styles 25 | // @import "_11-wordpress"; // WordPress reset and basic styles 26 | 27 | /* ----------------------------- */ 28 | /* ==own stylesheet */ 29 | /* ----------------------------- */ 30 | 31 | /* Here should go your own CSS styles */ 32 | /* You can also link them with a Sass @import */ 33 | /* @import "my-styles"; */ 34 | /* 35 | Header body footer 36 | */ 37 | 38 | /* 39 | React-portal base color 40 | */ 41 | 42 | @import "_20-rp-config"; // color setting 43 | @import "_21-rp-body"; 44 | @import "_22-rp-popup"; 45 | @import "_23-rp-table"; 46 | @import "_24-rp-widget"; 47 | @import "_25-rp-dashboard"; 48 | // @import "_26_rp_search"; 49 | @import "_27-rp-form"; 50 | @import "_28-rp-tabbar"; 51 | 52 | -------------------------------------------------------------------------------- /src/types/auth_types.js: -------------------------------------------------------------------------------- 1 | export const AUTH_CHECK_TOKEN = 'auth_check_token' 2 | export const AUTH_SET_AUTHORIZATIONS = 'auth_set_authorizations' 3 | export const AUTH_SIGN_IN_UP = 'auth_sign_in_up' 4 | export const AUTH_SIGN_IN = 'auth_sign_in' 5 | export const AUTH_SIGN_UP = 'auth_sign_up' 6 | export const AUTH_SIGN_OUT = 'auth_sign_out' 7 | export const AUTH_ERROR = 'auth_error' 8 | export const AUTH_VALIDATE_SIGN_UP = 'auth_validate_sign_up' 9 | export const AUTH_VALIDATE_SIGN_IN = 'auth_validate_sign_in' -------------------------------------------------------------------------------- /src/types/dashboard_types.js: -------------------------------------------------------------------------------- 1 | export const DASHBOARD_ADD = 'dashboard_add' 2 | export const DASHBOARD_DELETE = 'dashboard_delete' 3 | export const DASHBOARD_UPDATE = 'dashboard_update' 4 | export const DASHBOARD_GET_ALL = 'dashboard_get_all' 5 | export const DASHBOARD_REMOVE_WIDGET = 'dashboard_remove_widget' 6 | export const DASHBOARD_ADD_WIDGET = 'dashboard_add_widget' 7 | export const DASHBOARD_ADD_DASHBOARD = 'dashboard_add_dashboard' 8 | export const DASHBOARD_DELETE_DASHBOARD = 'dashboard_delete_dashboard' 9 | export const DASHBOARD_RENAME_DASHBOARD = 'dashboard_rename_dashboard' 10 | export const DASHBOARD_SHOW = 'dashboard_show' 11 | export const DASHBOARD_HIDE = 'dashboard_hide' -------------------------------------------------------------------------------- /src/types/message_types.js: -------------------------------------------------------------------------------- 1 | export const MESSAGE_FETCH = 'message_fetch'; 2 | export const MESSAGE_ERROR = 'message_error'; -------------------------------------------------------------------------------- /src/types/popup_types.js: -------------------------------------------------------------------------------- 1 | export const POPUP_SHOW = 'popup_show' 2 | export const POPUP_CLOSE = 'popup_close' -------------------------------------------------------------------------------- /src/types/tabbar_types.js: -------------------------------------------------------------------------------- 1 | export const TABBAR_SHOW = 'tabbar_show' 2 | export const TABBAR_CLOSE = 'tabbar_close' 3 | export const TABBAR_CLOSE_ALL = 'tabbar_close_all' 4 | export const TABBAR_SELECT = 'tabbar_select' -------------------------------------------------------------------------------- /src/types/todo_types.js: -------------------------------------------------------------------------------- 1 | export const TODO_GET_ALL = 'todo_get_all' 2 | export const TODO_DELETE = 'todo_delete' 3 | export const TODO_ADD = 'todo_add' 4 | export const TODO_UPDATE = 'todo_update' 5 | export const TODO_NEXT_PAGE = "todo_next_page" 6 | export const TODO_PREVIOUS_PAGE = "todo_previous_page" 7 | export const TODO_ERROR = "todo_error" 8 | export const TODO_EDIT_FORM = "todo_edit_form" 9 | export const TODO_CANCEL_FORM = "todo_cancel_form" 10 | export const TODO_DONE = "todo_done" -------------------------------------------------------------------------------- /src/types/user_types.js: -------------------------------------------------------------------------------- 1 | export const USER_GET_ALL = 'user_get_all' 2 | export const USER_DELETE = 'user_delete' 3 | export const USER_NEXT_PAGE = "user_next_page" 4 | export const USER_PREVIOUS_PAGE = "user_previous_page" 5 | export const USER_ERROR = "user_error" -------------------------------------------------------------------------------- /src/types/widget_types.js: -------------------------------------------------------------------------------- 1 | export const WIDGET_ADD = 'widget_add' 2 | export const WIDGET_DELETE = 'widget_delete' 3 | export const WIDGET_UPDATE = 'widget_update' 4 | export const WIDGET_GET_ALL = 'widget_get_all' -------------------------------------------------------------------------------- /test/auth_test.js: -------------------------------------------------------------------------------- 1 | import Signin from '../src/components/auth/signin' 2 | import { authStore } from '../src/stores/auth_store' 3 | import { signinForm, signupForm } from '../src/forms/signin_form' 4 | import { renderComponent , expect } from './test_helper'; 5 | import AuthService from '../src/services/auth_service'; 6 | import MockAuthService from './services/auth_service'; 7 | import { setTestFunction } from '../src/resolvers/test_resolver'; 8 | 9 | describe('Authentification' , () => { 10 | let component; 11 | let instance; 12 | 13 | beforeEach(() => { 14 | const result = renderComponent(Signin, { store: authStore, form: signinForm}); 15 | component = result.component 16 | instance = result.instance 17 | }); 18 | 19 | afterEach(() => { 20 | setTestFunction(null) 21 | }); 22 | 23 | it('renders something', () => { 24 | expect(component).to.exist; 25 | }); 26 | 27 | it('email exist', () => { 28 | expect(component.find('input[name="email"]')).to.exist; 29 | }); 30 | 31 | it('password exist', () => { 32 | expect(component.find('input[name="password"]')).to.exist; 33 | }); 34 | 35 | it('bad signin with store', () => { 36 | var testPromise = new Promise(function(resolve, reject) { 37 | AuthService.setInstance(new MockAuthService()) 38 | component.find('input[name="email"]').simulate('change', 'jyv'); 39 | component.find('input[name="password"]').simulate('change', 'sds'); 40 | setTestFunction(function() { 41 | resolve(authStore.getError()) 42 | }) 43 | component.find('button').simulate('click'); 44 | }); 45 | 46 | return testPromise.then(function(result){ 47 | expect(result).to.equal("Unauthorized"); 48 | }); 49 | }); 50 | 51 | it('bad signin with component', () => { 52 | var testPromise = new Promise(function(resolve, reject) { 53 | AuthService.setInstance(new MockAuthService()) 54 | component.find('input[name="email"]').simulate('change', 'jyv'); 55 | component.find('input[name="password"]').simulate('change', 'sds'); 56 | setTestFunction(function() { 57 | resolve(component.find('div[class="rp-field-error"]')) 58 | }) 59 | component.find('button').simulate('click'); 60 | }); 61 | 62 | return testPromise.then(function(result){ 63 | expect(result).to.exist 64 | }); 65 | }); 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /test/base_test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow, mount, render } from 'enzyme'; 4 | import Test from '../src/helpers/test'; 5 | 6 | describe("Base test with enzyme", function() { 7 | it("contains spec with an expectation", function() { 8 | expect(shallow().contains(
    )).to.equal(true); 9 | }); 10 | 11 | it("contains spec with an expectation", function() { 12 | expect(shallow().is('.test')).to.equal(true); 13 | }); 14 | 15 | it("contains spec with an expectation", function() { 16 | expect(mount().find('.test').length).to.equal(1); 17 | }); 18 | }); -------------------------------------------------------------------------------- /test/services/auth_service.js: -------------------------------------------------------------------------------- 1 | import { dispatch } from '../../src/helpers/dispatcher' 2 | 3 | export default class MockAuthService { 4 | signIn({ email, password }, next, err) { 5 | if (email === 'jyvinet@hotmail.ca' && password === 'test') { 6 | dispatch(next('token', 'Jean-Yves')) 7 | } else { 8 | dispatch(err('Unauthorized')); 9 | } 10 | } 11 | 12 | signUp({ email, password, name }, next, err) { 13 | if (email === 'jyvinet@hotmail.ca' && password === 'test') { 14 | dispatch(next('token', 'Jean-Yves')) 15 | } else { 16 | dispatch(err('Unauthorized')); 17 | } 18 | } 19 | 20 | setAuthorizations(render, next, err) { 21 | dispatch(next(render, 'token')) 22 | } 23 | } -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | import _$ from 'jquery'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | import jsdom from 'jsdom'; 6 | import chai, { expect } from 'chai'; 7 | import chaiJquery from 'chai-jquery'; 8 | import { addLocaleData, IntlProvider } from 'react-intl' 9 | import { app } from '../src/helpers/translation' 10 | 11 | require('babel-register')(); 12 | 13 | var exposedProperties = ['window', 'navigator', 'document']; 14 | 15 | global.document = jsdom.jsdom(''); 16 | global.window = global.document.defaultView; 17 | const $ = _$(window); 18 | 19 | var locale = window.navigator.userLanguage || window.navigator.language 20 | var localePrefix = locale.slice(0, locale.indexOf('-')); 21 | if (localePrefix !== 'en' && localePrefix !== 'fr') { 22 | locale = 'en-US' 23 | localePrefix = 'en' 24 | } 25 | const defaultApp = app['en']; 26 | import en from 'react-intl/locale-data/en'; 27 | import fr from 'react-intl/locale-data/fr' 28 | addLocaleData([...en, ...fr]); 29 | 30 | 31 | Object.keys(document.defaultView).forEach((property) => { 32 | if (typeof global[property] === 'undefined') { 33 | exposedProperties.push(property); 34 | global[property] = document.defaultView[property]; 35 | } 36 | }); 37 | 38 | global.navigator = { 39 | userAgent: 'node.js' 40 | }; 41 | 42 | chaiJquery(chai, chai.util, $); 43 | 44 | function renderComponent(ComponentClass, props = {}, state = {}) { 45 | const componentInstance = TestUtils.renderIntoDocument( 46 | 47 | 48 | 49 | ); 50 | 51 | return { instance: componentInstance, component: $(ReactDOM.findDOMNode(componentInstance)) } 52 | } 53 | 54 | $.fn.simulate = function(eventName, value) { 55 | if (value) { 56 | this.val(value); 57 | } 58 | TestUtils.Simulate[eventName](this[0]); 59 | }; 60 | 61 | export {renderComponent, expect}; 62 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | var isProd = (process.env.NODE_ENV === 'production'); 5 | console.log('prod', isProd) 6 | // Conditionally return a list of plugins to use based on the current environment. 7 | // Repeat this pattern for any other config key (ie: loaders, etc). 8 | function getPlugins() { 9 | var plugins = []; 10 | // Always expose NODE_ENV to webpack, you can now use `process.env.NODE_ENV` 11 | // inside your code for any environment checks; UglifyJS will automatically 12 | // drop any unreachable code. 13 | 14 | // Conditionally add plugins for Production builds. 15 | if (isProd === true) { 16 | plugins.push(new webpack.optimize.UglifyJsPlugin( 17 | { 18 | compressor: { 19 | screw_ie8: true, 20 | warnings: false 21 | }, 22 | minimize: true 23 | } 24 | )); // plugins.push(new webpack.DefinePlugin({ "process.env": { NODE_ENV: JSON.stringify('production') } }) ); 25 | } 26 | 27 | // Conditionally add plugins for Development 28 | //else { 29 | // // ... 30 | //} 31 | ////externals: {'react': 'React', 'react-dom': 'ReactDOM'}, 32 | 33 | return plugins; 34 | } 35 | 36 | module.exports = { 37 | devtool: 'cheap-module-source-map', 38 | entry: './src/index.js', 39 | output: { 40 | filename: 'bundle.js', 41 | }, 42 | 43 | module: { 44 | loaders: [ 45 | { 46 | // JSX REACT transpiler 47 | test: /\.js$/, 48 | include: [ 49 | path.join(__dirname, 'src'), 50 | path.join(__dirname, 'test') 51 | ], 52 | loader: 'babel-loader' 53 | }, 54 | { 55 | // SASS transpiler 56 | test: /(\.css|\.scss)$/, 57 | loaders: ['style-loader', 'css-loader?sourceMap', 'sass-loader?sourceMap'] 58 | }, 59 | // file loaders 60 | {test: /.eot(\?v=\d+.\d+.\d+)?$/, loader: "file"}, 61 | {test: /.(woff|woff2)$/, loader: "file-loader?prefix=font/&limit=5000"}, 62 | {test: /.ttf(\?v=\d+.\d+.\d+)?$/, loader: "file-loader?limit=10000&mimetype=application/octet-stream"}, 63 | {test: /.svg(\?v=\d+.\d+.\d+)?$/, loader: "file-loader?limit=10000&mimetype=image/svg+xml"}, 64 | {test: /\.(jpe?g|png|gif)$/i, loaders: ['file']}, 65 | {test: /\.ico$/, loader: 'file-loader?name=[name].[ext]'}, 66 | ] 67 | }, 68 | plugins: getPlugins() 69 | } 70 | 71 | --------------------------------------------------------------------------------