diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fadd49a..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..3e4e7d1 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,82 @@ +module.exports = { + extends : ['eslint:recommended'], + plugins : ['react', 'jest', 'tslint'], + env : { + es6 : true, + jquery : true, + browser: true, + node : true, + jest : true + }, + globals : { + DS : true, + _ : true, + jest : true + }, + 'parser' : 'babel-eslint', + 'parserOptions': { + 'ecmaVersion' : 6, + 'sourceType' : 'module', + 'ecmaFeatures': { + 'jsx' : true, + 'experimentalObjectRestSpread': true + } + }, + rules : { + 'linebreak-style' : ['error', 'unix'], + 'quotes' : ['error', 'single', { 'allowTemplateLiterals': true }], // Forciert Single Quotes, aber erlaubt Backticks für Template Literals + 'no-mixed-spaces-and-tabs' : ['error'], // Erlaubt sind Tabs (siehe unter 'indent'), aber nicht Spaces (Auf keinen Fall 'Smart-Tabs' einschalten) + 'no-unused-vars' : 1, // Warnt bei ungenutzten Variabeln + 'no-undef' : 'warn', // Warnt bei undefinierten Variabeln + 'no-tabs' : 'off', // Forciert die Nutzung von Tabs vs. Spaces + 'indent' : ['error', 'tab', { 'SwitchCase': 1, 'MemberExpression': 1, 'ArrayExpression': 1, 'VariableDeclarator': 1 }], // Forciert die Nutzung von Tabs zur Einrückung + 'no-trailing-spaces' : 'error', // Löscht Leerzeichen hinter Codezeilen + 'object-curly-spacing' : ['error', 'always', { 'objectsInObjects': true }], // Definiert das Spacing innerhalb von geschwungenen Klammern + 'semi' : ['error', 'always'], // Forciert die Nutzung von Semikola + 'react/jsx-uses-vars' : 1, // Definiert, dass JSX-Klassen die importiert wurden nicht als unbenutzte Variabeln betrachtet werden + 'react/no-unused-prop-types' : 2, // Forciert, dass alle PropTypes in Verwendung sein müssen + 'react/jsx-no-duplicate-props': 2, // Erlaubt keine doppelten Props + 'react/require-render-return' : 1, // Erfordert einen return value in Render-Methoden + 'jest/no-disabled-tests' : 'warn', + 'jest/no-focused-tests' : 'error', + 'jest/no-identical-title' : 'error', + 'jest/valid-expect' : 'error', + 'default-case' : 'error', // Forciert, dass switch-case immer ein default case haben muss + 'key-spacing' : [1, { // Definiert die Darstellung von Leerraum zwischen Key-Value-Paaren + 'singleLine': { + 'beforeColon': false, + 'afterColon' : true, + 'mode' : 'minimum' + }, + 'multiLine' : { + 'beforeColon': false, + 'afterColon' : true, + 'align' : 'colon', + 'mode' : 'minimum' + } + }], + 'max-len' : [ // Definiert die maximale Länge von Codezeilen + 1, + { + code : 160, + tabWidth : 4, + ignoreComments: true, + ignoreUrls : true + } + ], + // Wir wollen JSDoc für alles außer Arrow functions und Funktionsausdrücken + 'require-jsdoc' : ['warn', { + 'require': { + 'FunctionDeclaration' : false, + 'MethodDefinition' : false, + 'ClassDeclaration' : true, + 'ArrowFunctionExpression': false, + 'FunctionExpression' : false + } + }], +/* 'tslint/config' : ['warn', { + lintFile : './tslint.json', + configFile: './tsconfig.json' + }] */ + } +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index b512c09..9a38d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,12 @@ -node_modules \ No newline at end of file +# Ignore folders +node_modules + +# logs +access.log + +# Ignore hidden files +.DS_Store + +# Ignore Lockfiles +yarn.lock +package-lock.json \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..3e3fb67 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,59 @@ +--- +image: node:8.16.0-jessie + +variables: + NODE_VERSION: "8.16.0" + +stages: + - test + - build + + +# ---------------------------------- +# TEST +# ---------------------------------- + +test app: + stage: test + image: node:$NODE_VERSION + before_script: + - cd app + script: + - npm install + - npm run test-app + +test server: + stage: test + image: node:$NODE_VERSION + before_script: + - cd server + script: + - npm install + - npm run test + + +# ---------------------------------- +# BUILD +# ---------------------------------- + +build app: + only: + - master + stage: build + image: node:$NODE_VERSION + before_script: + - cd app + script: + - npm install + - npm run start-server + +build server: + only: + - master + stage: build + image: node:$NODE_VERSION + before_script: + - cd server + script: + - npm install + - npm run build \ No newline at end of file diff --git a/app/.babelrc b/app/.babelrc index 5e12cb1..9fe7b1d 100644 --- a/app/.babelrc +++ b/app/.babelrc @@ -1,7 +1,7 @@ { "presets": [ "@babel/preset-react", - "@babel/preset-env" + "@babel/preset-env", ], "plugins": [ "@babel/plugin-proposal-class-properties" diff --git a/app/.gitignore b/app/.gitignore index b512c09..1e5f75b 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,12 @@ -node_modules \ No newline at end of file +# Ignore folders +node_modules + +# Ignore hidden files +.DS_Store + +# Ignore Lockfiles +yarn.lock +package-lock.json + +# Ignore Environment Variable Files +.env \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..d6e9afc Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/index.html b/app/index.html index 03f0cd6..c0711a1 100644 --- a/app/index.html +++ b/app/index.html @@ -6,7 +6,6 @@ - diff --git a/app/js/App.js b/app/js/App.js new file mode 100644 index 0000000..f706d29 --- /dev/null +++ b/app/js/App.js @@ -0,0 +1,453 @@ +import React from 'react'; +import { BrowserRouter as Router, Route } from 'react-router-dom'; +import intl from 'react-intl-universal'; +import axios from 'axios'; + +// Constants +const DEFAULT_LOCALE = 'en-US'; +const locales = { + 'en-US': require('./data/locales/en-US.json'), + 'es-ES': require('./data/locales/es-ES.json'), + 'de-DE': require('./data/locales/de-DE.json') +}; + +const API_URI = 'http://localhost:3030'; + +// Import Constants +import API_ACTIONS from './constants/apiActions'; + +// Import Services +import Auth from './services/Auth'; + +// Import Layouts +import Navbar from './basics/Navbar'; + +// --- BASE +import Dashboard from './scenes/Dashboard'; +import Login from './scenes/Base/Login'; +import AdminPanel from './scenes/Admin/AdminPanel'; + +// --- BOOKS +import ProjectsList from './scenes/Projects/ProjectsList'; +import Project from './scenes/Projects/Project'; +import NewProject from './scenes/Projects/NewProject'; +import EditProject from './scenes/Projects/EditProject'; + +// --- CHARACTERS +import CharactersList from './scenes/Characters/CharactersList'; +import Character from './scenes/Characters/Character'; +import NewCharacter from './scenes/Characters/NewCharacter'; +import EditCharacter from './scenes/Characters/EditCharacter'; + +// --- EVENTS +import EventsList from './scenes/Events/EventsList'; + +// --- TAGS +import TagsList from './scenes/Tags/TagsList'; + +import MessengerContainer from './containers/MessengerContainer'; +import { SET_LOCALE } from './actions/setLocale'; + +/** + * Class App + */ +export default class App extends React.Component { + /** + * Constructor + * @param {*} props + */ + constructor(props) { + super(props); + // Initialize Auth + this.auth = new Auth(this.props.history); + + // Initialize component state + this.state = { + currentLocale: DEFAULT_LOCALE, + loading : false, + genres : [], + admin : {}, + events : [], + projects : [], + project : {}, + characters : [], + character : {}, + dashboard : [], + tags : [] + }; + } + + /** + * Method SetCurrentLocale + * @param {string} locale + */ + setCurrentLocale = locale => { + this.setState({ + loading : true, + currentLocale: locale || DEFAULT_LOCALE + }); + this.props.setLocale({ type: SET_LOCALE, locale }); + this.loadLocales(locale); + this.setState({ + loading: false + }); + } + + /** + * Method loadLocales + * @param {string} lang + */ + loadLocales(lang) { + intl + .init({ + currentLocale: lang, + locales + }) + .then(() => { + this.setState({ loading: false }); + }); + } + + retrieveGenres = lang => { + axios.get(`http://localhost:3030/genres/${lang}`) + .catch(err => { + this.props.addMessage({ + type : 'error', + content: err.message + }); + }) + .then(res => { + this.setState({ + genres: res.data.data + }); + }); + } + + sendApiRequest = (uri, action, resource, param, payload) => { + this.setState({ + loading: true + }); + let targetUri = uri; + resource !== void 0 ? (targetUri += '/' + resource) : ''; + param !== void 0 ? (targetUri += '/' + param) : ''; + + try { + switch (action) { + case API_ACTIONS.GET: + axios({ + method : API_ACTIONS.GET, + url : targetUri, + responseType: 'json', + headers : { + 'content-type': 'application/json' + } + }) + .catch(err => { + this.props.addMessage({ + type : 'error', + content: err.message + }); + + this.setState({ + loading: false + }); + }) + .then(res => { + this.setState({ + [resource]: res.data, + loading : false + }); + }); + break; + case API_ACTIONS.POST: + axios({ + method : API_ACTIONS.POST, + url : targetUri, + responseType: 'json', + headers : { + 'content-type': 'application/json' + }, + params: param, + data : payload + }) + .catch(err => { + this.props.addMessage({ + type : 'error', + content: err.message + }); + + this.setState({ + loading: false + }); + }) + .then(res => { + this.setState({ + [resource]: res.data, + loading : false + }); + }); + break; + case API_ACTIONS.PUT: + axios({ + method : API_ACTIONS.PUT, + url : targetUri, + responseType: 'json', + headers : { + 'content-type': 'application/json' + }, + params: param, + data : payload + }) + .catch(err => { + this.props.addMessage({ + type : 'error', + content: err.message + }); + + this.setState({ + loading: false + }); + }) + .then(res => { + this.setState({ + [resource]: res.data, + loading : false + }); + }); + break; + case API_ACTIONS.DELETE: + axios({ + method : API_ACTIONS.DELETE, + url : targetUri, + responseType: 'json', + headers : { + 'content-type': 'application/json' + }, + params: param + }).catch(err => { + this.props.addMessage({ + type : 'error', + content: err.message + }); + + this.setState({ + loading: false + }); + }); + break; + default: + break; + } + } catch (err) { + this.props.addMessage({ + type : 'error', + content: err.message + }); + + this.setState({ + loading: false + }); + } + this.setState({ + loading: false + }); + } + /** + * Method componentWillMount + */ + componentWillMount() { + this.loadLocales(this.state.currentLocale); + } + + /** + * Method render + */ + render() { + return ( + + + this.setCurrentLocale(locale)} {...this.props} /> +
+ + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'dashboard')} + eventsData={this.state.events} + getEvents={() => this.sendApiRequest(API_URI, API_ACTIONS.GET, 'events', 'latest')} + /> + )} + /> + } /> + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'admin')} + /> + )} + /> + + {/* PROJECTS */} + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'projects')} + deleteSpecificProject={id => { + this.sendApiRequest(API_URI, API_ACTIONS.DELETE, 'project', id); + }} + projects={this.state.projects} + {...props} + /> + )} + /> + ( + { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'project', '', data); + }} + addTag={data => { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'tag', '', data); + }} + addEvent={data => { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'event', '', data); + }} + {...props} + /> + )} + /> + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'project', id)} + project={this.state.project !== void 0 ? this.state.project : {}} + /> + )} + /> + ( + { + this.sendApiRequest(API_URI, API_ACTIONS.PUT, 'project', id, data); + }} + getProjectById={id => this.sendApiRequest(API_URI, API_ACTIONS.GET, 'project', id)} + project={this.state.project !== void 0 ? this.state.project : {}} + addEvent={data => { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'event', '', data); + }} + addTag={data => { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'tag', '', data); + }} + match={props.match} + {...props} + /> + )} + /> + + {/* CHARACTERS */} + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'characters')} + characters={this.state.characters !== void 0 ? this.state.characters : []} + getProjectById={id => this.sendApiRequest(API_URI, API_ACTIONS.GET, 'project', id)} + project={this.state.project} + /> + )} + /> + ( + { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'character', '', data); + }} + addEvent={data => { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'event', '', data); + }} + getProjects={() => this.sendApiRequest(API_URI, API_ACTIONS.GET, 'projects')} + projects={this.state.projects} + {...props} + /> + )} + /> + ( + { + this.sendApiRequest(API_URI, API_ACTIONS.PUT, 'character', id, data); + }} + getCharacterById={id => this.sendApiRequest(API_URI, API_ACTIONS.GET, 'character', id)} + character={this.state.character !== void 0 ? this.state.character : {}} + getProjects={() => this.sendApiRequest(API_URI, API_ACTIONS.GET, 'projects')} + projects={this.state.projects} + addEvent={data => { + this.sendApiRequest(API_URI, API_ACTIONS.POST, 'event', '', data); + }} + match={props.match} + {...props} + /> + )} + /> + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'character', id)} + character={this.state.character !== void 0 ? this.state.character : {}} + match={props.match} + /> + )} + /> + {/** Events */} + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'events')} /> + )} + /> + {/** Tags */} + ( + this.sendApiRequest(API_URI, API_ACTIONS.GET, 'tags')} /> + )} + /> +
+
+
+ ); + } +} diff --git a/app/js/actions/addMessage.js b/app/js/actions/addMessage.js new file mode 100644 index 0000000..8d9617f --- /dev/null +++ b/app/js/actions/addMessage.js @@ -0,0 +1,10 @@ +/* ACTION TYPES */ +export const ADD_MESSAGE = 'ADD_MESSAGE'; + +/* ACTION CREATORS*/ +export const addMessage = message => { + return { + type: ADD_MESSAGE, + message + }; +}; \ No newline at end of file diff --git a/app/js/actions/setLocale.js b/app/js/actions/setLocale.js new file mode 100644 index 0000000..163e02d --- /dev/null +++ b/app/js/actions/setLocale.js @@ -0,0 +1,10 @@ +/* ACTION TYPES */ +export const SET_LOCALE = 'SET_LOCALE'; + +/* ACTION CREATORS*/ +export const setLocale = locale => { + return { + type: SET_LOCALE, + locale + }; +}; diff --git a/app/js/basics/CharacterCard.js b/app/js/basics/CharacterCard.js new file mode 100644 index 0000000..632e0c8 --- /dev/null +++ b/app/js/basics/CharacterCard.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Card, Icon, Button } from 'semantic-ui-react'; +import PropTypes from 'prop-types'; + +/** + * Class CharacterCard + * Displays Characters in a Card View + */ +export default class CharacterCard extends React.Component { + componentDidMount() { + this.props.getProjectTitle(this.props.character.project); + } + + render() { + return ( + + + + {this.props.character.full_name} + + + {this.props.character.birthday} + + + {this.props.character.gender} + + {this.props.character.desc} + + {this.props.character.project !== void 0 ? ( + + + + ) : null} + + + + - - - - - ); - } -} \ No newline at end of file diff --git a/app/js/pages/Books/BooksList.js b/app/js/pages/Books/BooksList.js deleted file mode 100644 index 376dd54..0000000 --- a/app/js/pages/Books/BooksList.js +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import axios from 'axios'; -import { Link } from 'react-router-dom'; -import intl from 'react-intl-universal'; -import { Container, Breadcrumb, Segment, Header, Card, Divider, Button, Icon } from 'semantic-ui-react'; - -export default class Books extends React.Component { - constructor(){ - super(); - this.state = { - books: [] - }; - } - componentDidMount(){ - axios.get('http://localhost:3030/books') - .then(res => { - this.setState({ books: res.data }); - } - ); - } - - render(){ - return( - - - - {intl.get('component.dashboard')} - - - - {intl.get('entity.books')} - - - - -
- {intl.get('entity.books')} - {intl.get('desc.books')} -
- - - -
- - - - - {this.state.books.map(book => { - return ( - - - - {book.title} - - - {book.series !== void 0 - ?

- {intl.get('entity.books.series')}{' '}{book.series} -

- : null - } -
- {book.desc} -
- - - - - -
- ); - } -} diff --git a/app/js/pages/Characters/Character.js b/app/js/pages/Characters/Character.js deleted file mode 100644 index 394c726..0000000 --- a/app/js/pages/Characters/Character.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import axios from 'axios'; -import intl from 'react-intl-universal'; -import { Link } from 'react-router-dom'; -import { Container, Breadcrumb, Segment, Divider, Header, Button, Icon } from 'semantic-ui-react'; - -export default class Character extends React.Component { - constructor(){ - super(); - this.state = { - character: [] - }; - } - componentDidMount(){ - axios.get(`http://localhost:3030/character/${this.props.match.params.id}`) - .then(res => { - this.setState({ character: res.data.character }); - } - ); - } - - render(){ - return( - - - - {intl.get('component.dashboard')} - - - - {intl.get('entity.characters')} - - - - {this.state.character.first_name} {this.state.character.last_name} - - - -
- {this.state.character.first_name} {this.state.character.last_name} - {this.state.character.gender}, {this.state.character.age} -
- - -

- {this.state.character.desc} -

- - - - -
- -
- ); - } -} \ No newline at end of file diff --git a/app/js/pages/Characters/CharactersList.js b/app/js/pages/Characters/CharactersList.js deleted file mode 100644 index a8a650f..0000000 --- a/app/js/pages/Characters/CharactersList.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import axios from 'axios'; -import intl from 'react-intl-universal'; -import { Link } from 'react-router-dom'; -import { Container, Breadcrumb, Segment, Header, Card, Divider, Button, Icon } from 'semantic-ui-react'; - -export default class CharactersList extends React.Component { - constructor(){ - super(); - this.state = { - characters: [] - }; - } - componentDidMount(){ - axios.get('http://localhost:3030/characters') - .then(res => { - this.setState({ characters: res.data }); - } - ); - } - - render(){ - return( - - - - {intl.get('component.dashboard')} - - - - {intl.get('entity.characters')} - - - - -
- {intl.get('entity.characters')} - {intl.get('desc.characters')} -
- - - -
- - - - - {this.state.characters.map(character => { - return ( - - - - {character.first_name} {character.last_name} - - - {character.gender},{character.age} - - {character.desc} - - -
    - {character.series !== void 0 - ?
  • - {intl.get('entity.books.series')} {character.series} -
  • - : null - } - {character.book !== void 0 - ?
  • - {intl.get('entity.character')} {character.book} -
  • - : null - } - {character.family !== void 0 - ?
  • - {'I18N Family'}: {character.family} -
  • - : null - } -
-
- - - - - -
- ); - } -} diff --git a/app/js/pages/Dashboard.js b/app/js/pages/Dashboard.js deleted file mode 100644 index 66c5f61..0000000 --- a/app/js/pages/Dashboard.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import intl from 'react-intl-universal'; -import { Container, Segment, Header, Divider, Card, Feed } from 'semantic-ui-react'; - -export default class Dashboard extends React.Component { - render(){ - return( - - -
- {intl.get('component.dashboard')} -
- - - - - -
- {'I18N: Recent Activity'} - - - - - - {'I18N: 3 days ago'} - - I18N: You added The Passersby to your books. - - - - -
-
-
- ); - } -} \ No newline at end of file diff --git a/app/js/reducers/Messenger.js b/app/js/reducers/Messenger.js new file mode 100644 index 0000000..7affeb4 --- /dev/null +++ b/app/js/reducers/Messenger.js @@ -0,0 +1,16 @@ +import { ADD_MESSAGE } from '../actions/addMessage'; + +const Messenger = (state = [], action) => { + switch (action.type) { + case ADD_MESSAGE: + return [ + ...state, + action.message + ]; + default: + return state; + } +}; + +export default Messenger; + diff --git a/app/js/reducers/index.js b/app/js/reducers/index.js new file mode 100644 index 0000000..4de2565 --- /dev/null +++ b/app/js/reducers/index.js @@ -0,0 +1,10 @@ +import { combineReducers } from 'redux'; +import Messenger from './Messenger'; +import switchLocale from './switchLocale'; + +export const rootReducer = combineReducers({ + Messenger, + switchLocale +}); + +export default rootReducer; \ No newline at end of file diff --git a/app/js/reducers/switchLocale.js b/app/js/reducers/switchLocale.js new file mode 100644 index 0000000..23ee606 --- /dev/null +++ b/app/js/reducers/switchLocale.js @@ -0,0 +1,12 @@ +import { SET_LOCALE } from '../actions/setLocale'; + +const switchLocale = (state = [], action) => { + switch (action.type) { + case SET_LOCALE: + return action.locale; + default: + return state; + } +}; + +export default switchLocale; \ No newline at end of file diff --git a/app/js/scenes/Admin/AdminPanel.js b/app/js/scenes/Admin/AdminPanel.js new file mode 100644 index 0000000..8a03421 --- /dev/null +++ b/app/js/scenes/Admin/AdminPanel.js @@ -0,0 +1,193 @@ +import React from 'react'; +import { Container, Breadcrumb, Segment, Header, Divider, Table, List, Button, Label, Icon } from 'semantic-ui-react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +/** + * Class AdminPanel + * Administrative Panel to Overview the App's modules, entities and components + */ +export default class AdminPanel extends React.Component { + + constructor(){ + super(); + this.state = { + api: {}, + db : {}, + app: {} + }; + } + componentDidMount(){ + this.props.getAdminData(); + this.setState({ + ...this.state, + app: { + online: navigator.onLine + } + }); + + } + + componentWillReceiveProps(nextProps) { + if (nextProps !== this.props) { + this.setState({ + api: nextProps.adminData.api, + db : nextProps.adminData.db + }); + } + } + + render() { + return ( + + + + {'I18N: Admin Panel'} + + + + +
+ {'I18N: Admin Panel'} +
+ +
+ + + {this.state.app !== void 0 + ? +
+ {'APP'} +
+ + + + {'Type'} + {'Status'} + + + + + + {'Service Worker'} + + {this.state.app.online ? 'Online': 'Offline'} + + +
+
+ : null + } + + {this.props.adminData.api + ? this.props.adminData.api.map((entry, index) => { + return +
+ {'API'} +
+ + + + {'Type'} + {'Status'} + + + + + + {'API Version'} + + {entry.version} + + + + {'API Uptime'} + + {entry.uptime} + + + + {'API Logs'} + + + + {entry.logs.map(log => { + return {log}; + })} + + + + +
+
; + }) + : null + } + + {this.props.adminData.db + ? this.props.adminData.db.map((entry, index) => { + return +
+ {'Database'} +
+ + + + {'Type'} + {'Status'} + + + + + + {'DB Status'} + + {entry.status} + + + + {'DB Backups'} + + + + {entry.backups.map((backup, index) => { + return + + {'Restore'} + + + {backup} + + + ; + })} + + + + +
+
; + }) + : null + } + +
+ ); + } +} + +AdminPanel.propTypes = { + getAdminData: PropTypes.func, + adminData : PropTypes.shape({ + api: PropTypes.arrayOf( + PropTypes.shape({ + version: PropTypes.string, + uptime : PropTypes.number + }) + ), + db: PropTypes.arrayOf( + PropTypes.shape({ + status: PropTypes.string + }) + ) + }) +}; diff --git a/app/js/pages/Base/Login.js b/app/js/scenes/Base/Login.js similarity index 93% rename from app/js/pages/Base/Login.js rename to app/js/scenes/Base/Login.js index 00632d4..3ec12b4 100644 --- a/app/js/pages/Base/Login.js +++ b/app/js/scenes/Base/Login.js @@ -2,6 +2,10 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Container, Segment, Header, Divider, Form, Button } from 'semantic-ui-react'; +/** + * Class Login + * Login Component for the App + */ export default class Login extends React.Component { constructor() { super(); @@ -31,11 +35,13 @@ export default class Login extends React.Component { } }); break; + default: + break; } }; handleLogin = () => { - console.log(this.state.login); + //console.log(this.state.login); }; render() { diff --git a/app/js/scenes/Base/NotFound.js b/app/js/scenes/Base/NotFound.js new file mode 100644 index 0000000..6dcfd2c --- /dev/null +++ b/app/js/scenes/Base/NotFound.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { Container, Segment, Header, Icon, Button } from 'semantic-ui-react'; + +/** + * Class NotFound + * To be displayed if a route cannot be associated with a component + */ +export default class NotFound extends React.Component { + render(){ + return ( + + +
+ + {'We cannot find what you\'re looking for'} +
+ + + +
+
+ ); + } +} \ No newline at end of file diff --git a/app/js/scenes/Characters/Character.js b/app/js/scenes/Characters/Character.js new file mode 100644 index 0000000..51d5814 --- /dev/null +++ b/app/js/scenes/Characters/Character.js @@ -0,0 +1,74 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Link } from 'react-router-dom'; +import { Container, Breadcrumb, Segment, Divider, Header, Button, Icon } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class Character + * Single Character View + */ +export default class Character extends React.Component { + constructor(){ + super(); + this.state = { + character: [] + }; + } + componentDidMount(){ + this.props.getCharacterById(this.props.match.params.id); + } + + render(){ + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.characters')} + + + + {this.props.character.first_name} {this.props.character.last_name} + + + +
+ {this.props.character.first_name} {this.props.character.last_name} + {this.props.character.gender}, {this.props.character.age} +
+ + +

+ {this.props.character.desc} +

+ + + + +
+
; + } +} + +Character.propTypes = { + loading : PropTypes.bool, + match : PropTypes.object, + getCharacterById: PropTypes.func, + character : PropTypes.shape({ + first_name: PropTypes.string, + last_name : PropTypes.string, + gender : PropTypes.string, + age : PropTypes.string, + desc : PropTypes.string + }) +}; \ No newline at end of file diff --git a/app/js/scenes/Characters/CharactersList.js b/app/js/scenes/Characters/CharactersList.js new file mode 100644 index 0000000..a3e78c6 --- /dev/null +++ b/app/js/scenes/Characters/CharactersList.js @@ -0,0 +1,77 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Link } from 'react-router-dom'; +import { Container, Breadcrumb, Segment, Header, Card, Divider, Button, Icon } from 'semantic-ui-react'; +import CharacterCard from '../../basics/CharacterCard'; +import Loader from '../../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class CharactersList + * List of characters + */ +export default class CharactersList extends React.Component { + componentDidMount() { + this.props.getCharacters(); + } + + getProjectById = id => { + this.props.getProjectById(id); + }; + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.characters')} + + + + +
+ {intl.get('entity.characters')} + {intl.get('desc.characters')} +
+ + +
+ + + + + {this.props.characters.map((character, index) => { + return ( + this.getProjectById(character.project)} + /> + ); + })} + +
; + } +} + +CharactersList.propTypes = { + loading : PropTypes.bool, + getCharacters : PropTypes.func, + getProjectById: PropTypes.func, + characters : PropTypes.arrayOf( + PropTypes.shape({ + project: PropTypes.string, + }) + ), + project: PropTypes.shape({ + title: PropTypes.string + }) +}; \ No newline at end of file diff --git a/app/js/scenes/Characters/EditCharacter.js b/app/js/scenes/Characters/EditCharacter.js new file mode 100644 index 0000000..cf61160 --- /dev/null +++ b/app/js/scenes/Characters/EditCharacter.js @@ -0,0 +1,272 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Link } from 'react-router-dom'; +import { Container, Segment, Form, Button, Divider, Header, Breadcrumb } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class EditCharacter + * Edit Single Character + */ +export default class EditCharacter extends React.Component { + constructor() { + super(); + this.state = { + firstname : '', + middlename : '', + lastname : '', + birthday : '', + desc : '', + gender : '', + origin : '', + nationality: '', + family : '', + project : '', + series : '', + imgurl : '' + }; + } + + handleInput = (source, evt) => { + switch (source) { + case 'firstname': + this.setState({ firstname: evt.currentTarget.value }); + break; + case 'middlename': + this.setState({ middlename: evt.currentTarget.value }); + break; + case 'lastname': + this.setState({ lastname: evt.currentTarget.value }); + break; + case 'gender': + this.setState({ gender: evt.currentTarget.value }); + break; + case 'birthday': + this.setState({ birthday: evt.currentTarget.value }); + break; + case 'origin': + this.setState({ origin: evt.currentTarget.value }); + break; + case 'project': + this.setState({ project: evt.currentTarget.value }); + break; + case 'desc': + this.setState({ desc: evt.currentTarget.value }); + break; + default: + break; + } + }; + + assembleFullName = (first, middle, last) => { + try { + let fullName = first !== void 0 && first.length > 0 ? first + ' ' : ''; + fullName += middle !== void 0 && middle.length > 0 ? middle + ' ' : ''; + fullName += last !== void 0 && last.length > 0 ? last : ''; + return fullName.trim(); + } catch (err) { + throw err; + } + }; + + postnewCharacter(evt) { + evt.preventDefault(); + + this.props.editCharacter(this.props.match.params.id, { + desc : this.state.desc, + first_name : this.state.firstname, + middle_name: this.state.middlename, + last_name : this.state.lastname, + full_name : this.assembleFullName(this.state.firstname, this.state.middlename, this.state.lastname), + gender : this.state.gender, + birthday : this.state.birthday, + origin : this.state.origin, + project : this.state.project + }); + + this.props.addEvent({ + user : 'testAdmin', + action: 'edit_character', + ref : this.assembleFullName(this.state.firstname, this.state.middlename, this.state.lastname) + }); + + this.props.history.push('/characters'); + } + + componentDidMount() { + this.assembleFullName(); + this.props.getCharacterById(this.props.match.params.id); + this.props.getProjects(); + } + + componentWillReceiveProps(nextProps) { + if (nextProps !== this.props) { + this.setState({ + first_name : nextProps.firstname, + middle_name: nextProps.middlename, + last_name : nextProps.lastname, + full_name : nextProps.full_name, + gender : nextProps.gender, + birthday : nextProps.birthday, + origin : nextProps.origin, + project : nextProps.project + }); + } + } + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.characters')} + + + {intl.get('character.action-edit')} + + + +
{intl.get('character.action-edit')}
+ + + +
+ + + this.handleInput('desc', evt)} + defaultValue={this.props.character.desc} + type="text" + id="desc" + placeholder="A most loveable person" + /> + + + + this.handleInput('firstname', evt)} + defaultValue={this.props.character.first_name} + type="text" + id="firstname" + placeholder="Jane" + required + /> + + + + this.handleInput('middlename', evt)} + defaultValue={this.props.character.middle_name} + type="text" + id="middlename" + placeholder="Agatha" + /> + + + + this.handleInput('lastname', evt)} + defaultValue={this.props.character.last_name} + type="text" + id="lastname" + placeholder="Doe" + /> + + + + this.handleInput('gender', evt)} + defaultValue={this.props.character.gender} + type="text" + className="form-control" + id="gender" + placeholder="Non-binary" + /> + + + + this.handleInput('origin', evt)} + defaultValue={this.props.character.origin} + type="text" + className="form-control" + id="origin" + placeholder="Wakanda" + /> + + + + this.handleInput('birthday', evt)} + defaultValue={this.props.character.birthday} + type="text" + className="form-control" + id="birthday" + placeholder="01-07-2019" + /> + + + + + + + + + + + +
+
; + } +} + +EditCharacter.propTypes = { + loading : PropTypes.bool, + editCharacter: PropTypes.func, + addEvent : PropTypes.func, + history : PropTypes.object, + match : PropTypes.object, + character : PropTypes.shape({ + firstname : PropTypes.string, + middlename: PropTypes.string, + lastname : PropTypes.string, + fullname : PropTypes.string, + gender : PropTypes.string, + birthday : PropTypes.string, + origin : PropTypes.string, + project : PropTypes.string + }) +}; \ No newline at end of file diff --git a/app/js/scenes/Characters/NewCharacter.js b/app/js/scenes/Characters/NewCharacter.js new file mode 100644 index 0000000..18b403e --- /dev/null +++ b/app/js/scenes/Characters/NewCharacter.js @@ -0,0 +1,216 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Link } from 'react-router-dom'; +import { Container, Segment, Form, Button, Divider, Header, Breadcrumb } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class NewCharacter + * Create new character + */ +export default class NewCharacter extends React.Component { + constructor() { + super(); + this.state = { + firstname : '', + middlename : '', + lastname : '', + birthday : '', + desc : '', + gender : '', + origin : '', + nationality: '', + family : '', + project : '', + series : '', + imgurl : '' + }; + } + + handleInput = (source, evt) => { + switch (source) { + case 'firstname': + this.setState({ firstname: evt.currentTarget.value }); + break; + case 'middlename': + this.setState({ middlename: evt.currentTarget.value }); + break; + case 'lastname': + this.setState({ lastname: evt.currentTarget.value }); + break; + case 'gender': + this.setState({ gender: evt.currentTarget.value }); + break; + case 'birthday': + this.setState({ birthday: evt.currentTarget.value }); + break; + case 'origin': + this.setState({ origin: evt.currentTarget.value }); + break; + case 'project': + this.setState({ project: evt.currentTarget.value }); + break; + case 'desc': + this.setState({ desc: evt.currentTarget.value }); + break; + default: + break; + } + }; + + assembleFullName = (first, middle, last) => { + try { + let fullName = first !== void 0 && first.length > 0 ? first + ' ' : ''; + fullName += middle !== void 0 && middle.length > 0 ? middle + ' ' : ''; + fullName += last !== void 0 && last.length > 0 ? last : ''; + return fullName.trim(); + } catch (err) { + throw err; + } + }; + + postnewCharacter(evt) { + evt.preventDefault(); + + this.props.addCharacter({ + desc : this.state.desc, + first_name : this.state.firstname, + middle_name: this.state.middlename, + last_name : this.state.lastname, + full_name : this.assembleFullName(this.state.firstname, this.state.middlename, this.state.lastname), + gender : this.state.gender, + birthday : this.state.birthday, + origin : this.state.origin, + project : this.state.project + }); + + this.props.addEvent({ + user : 'testAdmin', + action: 'add_character', + ref : this.assembleFullName(this.state.firstname, this.state.middlename, this.state.lastname) + }); + + this.props.history.push('/characters'); + } + + componentDidMount() { + this.props.getProjects(); + } + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.characters')} + + + + {intl.get('character.action-add')} + + + + +
+ {intl.get('character.action-add')} +
+ + + +
+ + + this.handleInput('desc', evt)} + type="text" id="desc" placeholder="A most loveable person" /> + + + + this.handleInput('firstname', evt)} + type="text" id="firstname" placeholder="Jane" required /> + + + + this.handleInput('middlename', evt)} + type="text" id="middlename" placeholder="Agatha" /> + + + + this.handleInput('lastname', evt)} + type="text" id="lastname" placeholder="Doe" /> + + + + this.handleInput('gender', evt)} + type="text" className="form-control" id="gender" + placeholder="Non-binary" /> + + + + this.handleInput('origin', evt)} + type="text" className="form-control" id="origin" placeholder="Wakanda" /> + + + + this.handleInput('birthday', evt)} + type="date" className="form-control" id="birthday" placeholder="01-07-2019" /> + + + + + + + + + + + + +
+
; + } +} + +NewCharacter.propTypes = { + loading : PropTypes.bool, + addCharacter: PropTypes.func, + addEvent : PropTypes.func, + getProjects : PropTypes.func, + projects : PropTypes.arrayOf( + PropTypes.shape({ + _id : PropTypes.string, + title: PropTypes.string + }) + ), + history: PropTypes.object, +}; \ No newline at end of file diff --git a/app/js/scenes/Dashboard.js b/app/js/scenes/Dashboard.js new file mode 100644 index 0000000..0fd6a4e --- /dev/null +++ b/app/js/scenes/Dashboard.js @@ -0,0 +1,111 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import intl from 'react-intl-universal'; +import moment from 'moment'; +import { Container, Segment, Header, Divider, Card, Feed } from 'semantic-ui-react'; +import Loader from '../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class Dashboard + * Dashboard of the Ensemble App + */ +export default class Dashboard extends React.Component { + constructor() { + super(); + this.state = { + eventsData : [], + dashboardData: [] + }; + } + + parseEvent = event => { + switch (event.action) { + case 'add_project': + return intl.get('event.action-addproject', { ref: event.ref }); + case 'add_character': + return intl.get('event.action-addcharacter', { ref: event.ref }); + case 'edit_project': + return intl.get('event.action-editproject', { ref: event.ref }); + case 'edit_character': + return intl.get('event.action-editcharacter', { ref: event.ref }); + case 'delete_project': + return intl.get('event.action-deletecharacter', { ref: event.ref }); + case 'delete_character': + return intl.get('event.action-deletecharacter', { ref: event.ref }); + default: break; + } + }; + + componentDidMount() { + this.props.getEvents(); + this.props.getDashboardData(); + } + + render() { + return this.props.loading + ? + : + +
+ {intl.get('component.dashboard')} +
+ + + this.props.history.push('/projects')} + image={'../../public/img/photo-1443188631128-a1b6b1c5c207.jpeg'} + header={intl.get('entity.projects')} + meta={intl.get('project.count-available', { + count: this.props.dashboardData ? this.props.dashboardData.projects : 0 + })} + /> + this.props.history.push('/characters')} + image={'../../public/img/photo-1427805371062-cacdd21273f1.jpeg'} + header={intl.get('entity.characters')} + meta={intl.get('character.count-available', { + count: this.props.dashboardData ? this.props.dashboardData.characters : 0 + })} + /> + +
+ {intl.get('event.recent-activity')} + + + {this.props.eventsData + ? this.props.eventsData.map((event, index) => { + return ( + + + + + {moment(event.timestamp, 'X').format('lll')} + + {this.parseEvent(event)} + + + + + ); + }) + : null} + +
+
+
; + } +} + +Dashboard.propTypes = { + loading : PropTypes.bool, + history : PropTypes.object, + getEvents : PropTypes.func, + getDashboardData: PropTypes.func, + dashboardData : PropTypes.any, + eventsData : PropTypes.arrayOf(PropTypes.shape({ + timestamp: PropTypes.string, + action : PropTypes.string, + ref : PropTypes.string + })) +}; \ No newline at end of file diff --git a/app/js/scenes/Events/EventsList.js b/app/js/scenes/Events/EventsList.js new file mode 100644 index 0000000..b0c0961 --- /dev/null +++ b/app/js/scenes/Events/EventsList.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import intl from 'react-intl-universal'; +import { Container, Breadcrumb, Segment, Header, Feed, Divider } from 'semantic-ui-react'; +import moment from 'moment'; +import Loader from '../../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class EventsList + * List of events + */ +export default class EventsList extends React.Component { + parseEvent = event => { + switch (event.action) { + case 'add_project': + return intl.get('event.action-addproject', { ref: event.ref }); + case 'add_character': + return intl.get('event.action-addcharacter', { ref: event.ref }); + case 'edit_project': + return intl.get('event.action-editproject', { ref: event.ref }); + case 'edit_character': + return intl.get('event.action-editcharacter', { ref: event.ref }); + case 'delete_project': + return intl.get('event.action-deletecharacter', { ref: event.ref }); + case 'delete_character': + return intl.get('event.action-deletecharacter', { ref: event.ref }); + default: break; + } + }; + + componentDidMount() { + this.props.getEvents(); + } + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.events')} + + + + +
+ {intl.get('entity.events')} + {intl.get('desc.events')} +
+
+ + + + {this.props.events + ? this.props.events.map((event, index) => { + return ( + + + + + {moment(event.timestamp, 'X').format('lll')} + {this.parseEvent(event)} + + + + ); + }) + : null} +
; + } +} + +EventsList.propTypes = { + loading : PropTypes.bool, + getEvents: PropTypes.func, + events : PropTypes.arrayOf( + PropTypes.shape({ + timestamp: PropTypes.timestamp, + action : PropTypes.string, + ref : PropTypes.string + }) + ) +}; \ No newline at end of file diff --git a/app/js/scenes/Projects/EditProject.js b/app/js/scenes/Projects/EditProject.js new file mode 100644 index 0000000..0df7349 --- /dev/null +++ b/app/js/scenes/Projects/EditProject.js @@ -0,0 +1,272 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Link } from 'react-router-dom'; +import { Container, Segment, Form, Button, Divider, Header, Breadcrumb } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import { tagTypes } from '../../constants/tagTypes'; +import { projectTypes } from '../../constants/projectTypes'; +import { projectStatus } from '../../constants/projectStatus'; +import PropTypes from 'prop-types'; + +/** + * Class EditProject + * View for Editing a single project + */ +export default class EditProject extends React.Component { + constructor() { + super(); + this.state = { + title : '', + status : '', + genre : '', + series : '', + cast : '', + desc : '', + tags : [], + projectTypes : [], + projectStatus: [], + project : {} + }; + } + + handleInput = (source, evt) => { + switch (source) { + case 'type': + this.setState({ type: evt.currentTarget.value }); + break; + case 'status': + this.setState({ status: evt.currentTarget.value }); + break; + case 'title': + this.setState({ title: evt.currentTarget.value }); + break; + case 'series': + this.setState({ series: evt.currentTarget.value }); + break; + case 'genre': + this.setState({ genre: evt.currentTarget.value }); + break; + case 'desc': + this.setState({ desc: evt.currentTarget.value }); + break; + case 'cast': + this.setState({ cast: evt.currentTarget.value }); + break; + case 'tags': + this.setState({ + tags: evt.currentTarget.value.split(',').map(item => item.trim()) + }); + break; + default: + break; + } + } + + postEditedProject(evt) { + evt.preventDefault(); + + this.props.editProject(this.props.match.params.id, { + type : this.state.type || this.props.project.type, + status: this.state.status || this.props.project.status || projectStatus.DRAFT, + title : this.state.title || this.props.project.title, + genre : this.state.genre || this.props.project.genre, + series: this.state.series || this.props.project.series, + desc : this.state.desc || this.props.project.desc, + cast : this.state.cast || this.props.project.cast, + tags : this.state.tags || this.props.project.tags + }); + + if (this.state.tags !== void 0) { + this.state.tags.forEach(tag => { + this.props.addTag({ + type: tagTypes.PROJECT, + name: tag, + ref : this.state.title + }); + }); + } + + this.props.addTag({ + type: tagTypes.PROJECT_TYPE, + name: this.state.type, + ref : this.state.title + }); + + this.props.addEvent({ + user : 'testAdmin', + action: 'edit_project', + ref : this.state.title + }); + + this.props.history.push('/projects'); + } + + componentWillMount = () => { + this.props.getProjectById(this.props.match.params.id); + + let types = []; + let statuses = []; + for (let type in projectTypes) { + types.push(projectTypes[type]); + } + for (let status in projectStatus) { + statuses.push(projectStatus[status]); + } + this.setState({ + projectTypes : types, + projectStatus: statuses + }); + } + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.projects')} + + + + {intl.get('project.action-edit')} + + + + +
+ {intl.get('project.action-edit')} +
+ + + +
+ + + + + + + + + + + this.handleInput('title', evt)} + type="text" + className="form-control" + id="title" + placeholder="The Final Problem" + defaultValue={this.props.project.title} + required + /> + + + + this.handleInput('genre', evt)} + type="text" + className="form-control" + id="genre" + placeholder="Crime, Suspense" + defaultValue={this.props.project.genre} + /> + + + + this.handleInput('series', evt)} + type="text" + className="form-control" + id="series" + placeholder="The Memoirs of Sherlock Holmes" + defaultValue={this.props.project.series} + /> + + + + this.handleInput('cast', evt)} + type="text" + className="form-control" + id="cast" + placeholder="Sherlock Holmes, James Watson" + defaultValue={this.props.project.cast} + /> + + + + this.handleInput('desc', evt)} + type="text" + className="form-control" + id="desc" + placeholder="The Adventures of Sherlock Holmes" + defaultValue={this.props.project.desc} + required + /> + + + + this.handleInput('tags', evt)} + type="text" + className="form-control" + id="tags" + placeholder="crime, noir" + defaultValue={this.props.project.tags !== void 0 ? this.props.project.tags.join(', ') : ''} + /> + + + +
+ +
+
; + } +} + +EditProject.propTypes = { + loading : PropTypes.bool, + history : PropTypes.object, + match : PropTypes.object, + editProject: PropTypes.func, + addTag : PropTypes.func, + project : PropTypes.shape({ + type : PropTypes.string, + status: PropTypes.string, + title : PropTypes.string, + genre : PropTypes.genre, + series: PropTypes.series, + desc : PropTypes.desc, + cast : PropTypes.cast, + tags : PropTypes.array + }) + +}; \ No newline at end of file diff --git a/app/js/scenes/Projects/NewProject.js b/app/js/scenes/Projects/NewProject.js new file mode 100644 index 0000000..5f9b32b --- /dev/null +++ b/app/js/scenes/Projects/NewProject.js @@ -0,0 +1,220 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Link } from 'react-router-dom'; +import { Container, Segment, Form, Button, Divider, Header, Breadcrumb } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import { tagTypes } from '../../constants/tagTypes'; +import { projectTypes } from '../../constants/projectTypes'; +import { projectStatus } from '../../constants/projectStatus'; +import PropTypes from 'prop-types'; + +/** + * Class NewProject + * Form page for new projects + */ +export default class NewProject extends React.Component { + constructor() { + super(); + this.state = { + title : '', + genre : '', + series : '', + cast : '', + desc : '', + tags : [], + projectTypes: [] + }; + } + + handleInput = (source, evt) => { + switch (source) { + case 'type': + this.setState({ type: evt.currentTarget.value }); + break; + case 'title': + this.setState({ title: evt.currentTarget.value }); + break; + case 'series': + this.setState({ series: evt.currentTarget.value }); + break; + case 'genre': + this.setState({ genre: evt.currentTarget.value }); + break; + case 'desc': + this.setState({ desc: evt.currentTarget.value }); + break; + case 'cast': + this.setState({ cast: evt.currentTarget.value }); + break; + case 'tags': + this.setState({ + tags: evt.currentTarget.value.split(',').map(item => item.trim()) + }); + break; + default: + break; + } + } + + postNewProject(evt) { + evt.preventDefault(); + + this.props.addProject({ + type : this.state.type, + status: projectStatus.DRAFT, + title : this.state.title, + genre : this.state.genre, + series: this.state.series, + desc : this.state.desc, + cast : this.state.cast, + tags : this.state.tags, + }); + + if(this.state.tags !== void 0) { + this.state.tags.forEach(tag => { + this.props.addTag({ + type: tagTypes.PROJECT, + name: tag, + ref : this.state.title + }); + + }); + } + + this.props.addTag({ + type: tagTypes.PROJECT_TYPE, + name: this.state.type, + ref : this.state.title + }); + + this.props.addEvent({ + user : 'testAdmin', + action: 'add_project', + ref : this.state.title, + }); + + this.props.history.push('/projects'); + } + + componentWillMount = () => { + let types = []; + for (let type in projectTypes) { + types.push(projectTypes[type]); + } + this.setState({ + projectTypes: types + }); + + } + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.projects')} + + + + {intl.get('project.action-add')} + + + + +
+ {intl.get('project.action-add')} +
+ + + +
+ + + + + + + this.handleInput('title', evt)} + type="text" + className="form-control" + id="title" + placeholder="The Final Problem" + required + /> + + + + this.handleInput('genre', evt)} + type="text" className="form-control" id="genre" placeholder="Crime, Suspense" /> + + + + this.handleInput('series', evt)} + type="text" + className="form-control" + id="series" + placeholder="The Memoirs of Sherlock Holmes" + /> + + + + this.handleInput('cast', evt)} + type="text" + className="form-control" + id="cast" + placeholder="Sherlock Holmes, James Watson" + /> + + + + this.handleInput('desc', evt)} + type="text" + className="form-control" + id="desc" + placeholder="The Adventures of Sherlock Holmes" + required + /> + + + + this.handleInput('tags', evt)} type="text" className="form-control" id="tags" placeholder="crime, noir" /> + + + +
+ +
+
; + } +} + +NewProject.propTypes = { + loading : PropTypes.bool, + addProject: PropTypes.func, + addTag : PropTypes.func, + addEvent : PropTypes.func, + history : PropTypes.object +}; \ No newline at end of file diff --git a/app/js/scenes/Projects/Project.js b/app/js/scenes/Projects/Project.js new file mode 100644 index 0000000..c9eab52 --- /dev/null +++ b/app/js/scenes/Projects/Project.js @@ -0,0 +1,87 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import intl from 'react-intl-universal'; +import { Container, Breadcrumb, Segment, Header, Table, Divider, Button, Icon, Label } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class Project + * Single Project view + */ +export default class Project extends React.Component { + componentDidMount() { + this.props.getProjectById(this.props.match.params.id); + } + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.projects')} + + + + {this.props.project.title} + + + + +
+ {this.props.project.title} + {this.props.project.series} +
+ + +

+ {this.props.project.desc} +

+ + {this.props.project.tags !== void 0 + ? + {this.props.project.tags.map((tag, index) => { + return ; + })} + + : null} + + + + + + {intl.get('entity.characters')} + + + +
+ + + +
+ +
; + } +} + +Project.propTypes = { + loading : PropTypes.bool, + getProjectById: PropTypes.func, + match : PropTypes.object, + project : PropTypes.shape({ + _id : PropTypes.string, + tags : PropTypes.array, + title : PropTypes.string, + series: PropTypes.string, + desc : PropTypes.string + }) +}; \ No newline at end of file diff --git a/app/js/scenes/Projects/ProjectsList.js b/app/js/scenes/Projects/ProjectsList.js new file mode 100644 index 0000000..83d2090 --- /dev/null +++ b/app/js/scenes/Projects/ProjectsList.js @@ -0,0 +1,329 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import intl from 'react-intl-universal'; +import PropTypes from 'prop-types'; +import { Container, Breadcrumb, Segment, Header, Card, Divider, Button, Label, Tab, Grid, Icon } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import DeleteModal from '../../basics/DeleteModal'; +import { projectStatus } from '../../constants/projectStatus'; + +/** + * Class ProjectsList + * List of Projects + */ +export default class ProjectsList extends React.Component { + constructor() { + super(); + this.state = { + showDeleteModal: false, + projectId : null + }; + } + + componentDidMount() { + this.props.getProjects(); + } + + /** + * deleteSpecificProject method + * triggers deleteSpecificProject with the given id + * @param {*} id The id of the specific project + */ + deleteSpecificProject(id) { + this.props.deleteSpecificProject(id); + } + + /** + * toggleDeleteModal method + * toggles showing/hiding the modal + */ + toggleDeleteModal = id => { + this.setState({ + showDeleteModal: !this.state.showDeleteModal, + projectId : id + }); + } + + /** + * Method getProjectPanes + * @param {object} props + */ + getProjectPanes = props => { + return [ + { + menuItem: { key: 'overview', icon: 'th', content: intl.get('project.tab-overview') }, + render : () => {this.displayProjectOverview(props)} + }, + { + menuItem: { key: 'board', icon: 'columns', content: intl.get('project.tab-board') }, + render : () => {this.displayProjectBoard(props)} + } + ]; + } + + displayProjectOverview = props => { + return ( + + {props.projects !== void 0 + ? props.projects.map((project, index) => { + return ( + + + + {project.title} + + {project.type !== void 0 ? intl.get(`project.select-type-${project.type}`) : null} + + + + + {project.series !== void 0 && project.series.length > 0 + ? +

+ {intl.get('entity.projects.series')}{' '}{project.series} +

+
+ : null} + {project.desc !== void 0 && project.desc.length > 0 + ? + {project.desc} + + : null} + + {project.tags !== void 0 && project.tags.length > 0 + ? + + {project.tags.map((tag, index) => { + return ; + })} + + + : null} + + + + + + + + + + ; + } +} + +ProjectsList.propTypes = { + loading : PropTypes.bool, + getProjects : PropTypes.func, + deleteSpecificProject: PropTypes.func +}; + + +// Project PropTypes +/* + projects : PropTypes.shape({ + _id : PropTypes.number, + title : PropTypes.string, + type : PropTypes.string, + status: PropTypes.string, + series: PropTypes.string, + desc : PropTypes.string, + tags : PropTypes.array + }) +*/ \ No newline at end of file diff --git a/app/js/scenes/Tags/TagsList.js b/app/js/scenes/Tags/TagsList.js new file mode 100644 index 0000000..d538d04 --- /dev/null +++ b/app/js/scenes/Tags/TagsList.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import intl from 'react-intl-universal'; +import { Container, Breadcrumb, Segment, Header, Divider, Label } from 'semantic-ui-react'; +import Loader from '../../basics/Loader'; +import PropTypes from 'prop-types'; + +/** + * Class TagsList + * A list of tags + */ +export default class TagsList extends React.Component { + + componentDidMount() { + this.props.getTags(); + } + + render() { + return this.props.loading + ? + : + + + {intl.get('component.dashboard')} + + + + {intl.get('entity.tags')} + + + + +
+ {intl.get('entity.tags')} + {intl.get('desc.tags')} +
+
+ + + + + + {this.props.tags !== void 0 + ? this.props.tags.map((tag, index) => { + return ; + }) + : null} + + +
; + } +} + +TagsList.propTypes = { + loading: PropTypes.bool, + tags : PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string + }) + ) +}; diff --git a/app/js/services/Auth.js b/app/js/services/Auth.js new file mode 100644 index 0000000..6c9afe5 --- /dev/null +++ b/app/js/services/Auth.js @@ -0,0 +1,23 @@ +import auth0 from 'auth0-js'; + +/** + * Class Auth + * Client Authentication + */ +export default class Auth { + + constructor(history) { + this.history = history; + this.auth0 = new auth0.WebAuth({ + domain : process.env.CLIENT_AUTH0_DOMAIN || '', + clientID : process.env.CLIENT_AUTH0_CLIENTID || '', + redirectUri : process.env.CLIENT_AUTH0_REDIRECTURI, + responseType: 'token id_token', + scope : 'openid profile email' + }); + } + + login = () => { + this.auth0.authorize(); + }; +} diff --git a/app/offline/index.html b/app/offline/index.html new file mode 100644 index 0000000..32a9eec --- /dev/null +++ b/app/offline/index.html @@ -0,0 +1,22 @@ + + + + + + + + + Offline + + + + + + You are offline, buddy! + + + + + \ No newline at end of file diff --git a/app/package.json b/app/package.json index 44bab92..b4970ec 100644 --- a/app/package.json +++ b/app/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "start": "webpack-dev-server --host 127.0.0.1 --port 3001 --watch", - "test": "echo \"Error: no test specified\" && exit 1" + "test-app": "jest run ./app", + "build": "webpack --mode production" }, "author": "", "license": "ISC", @@ -13,31 +14,56 @@ "@babel/cli": "^7.2.3", "@babel/core": "^7.2.2", "@babel/plugin-proposal-class-properties": "^7.4.4", + "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.2.3", "@babel/preset-react": "^7.0.0", + "auth0-js": "^9.10.4", "axios": "^0.18.1", "babel": "^6.23.0", "babel-loader": "8", "babel-plugin-lodash": "^3.3.2", "babel-plugin-react-transform": "^3.0.0", "babel-preset-react": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", "browser-sync": "^2.26.3", "browser-sync-webpack-plugin": "^2.2.2", "css-loader": "^2.1.1", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^3.0.1", "html-webpack-plugin": "^3.2.0", + "moment": "^2.24.0", "node-sass": "^4.12.0", "react": "^16.7.0", "react-dom": "^16.7.0", "react-intl-universal": "^1.16.2", + "react-redux": "^7.1.0", "react-router-dom": "^4.3.1", + "redux": "^4.0.1", "sass-loader": "^7.1.0", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.87.1", + "serviceworker-webpack-plugin": "^1.0.1", "style-loader": "^0.23.1", "webpack": "^4.28.3", "webpack-cli": "^3.2.0", "webpack-dev-server": "^3.1.14" + }, + "devDependencies": { + "tslint": "^5.18.0", + "eslint-plugin-tslint": "^3.1.0", + "babel-jest": "^24.5.0", + "dotenv-webpack": "^1.7.0", + "enzyme": "^3.9.0", + "enzyme-adapter-react-16": "^1.11.2", + "enzyme-to-json": "^3.3.5", + "jest": "^24.5.0" + }, + "jest": { + "setupFiles": [ + "./tests/jestsetup.js" + ], + "snapshotSerializers": [ + "enzyme-to-json/serializer" + ] } -} +} \ No newline at end of file diff --git a/app/public/favicon/android-icon-192x192.png b/app/public/favicon/android-icon-192x192.png new file mode 100644 index 0000000..c44f86c Binary files /dev/null and b/app/public/favicon/android-icon-192x192.png differ diff --git a/app/public/favicon/apple-icon-114x114.png b/app/public/favicon/apple-icon-114x114.png new file mode 100644 index 0000000..45ea754 Binary files /dev/null and b/app/public/favicon/apple-icon-114x114.png differ diff --git a/app/public/favicon/apple-icon-120x120.png b/app/public/favicon/apple-icon-120x120.png new file mode 100644 index 0000000..1370ebf Binary files /dev/null and b/app/public/favicon/apple-icon-120x120.png differ diff --git a/app/public/favicon/apple-icon-144x144.png b/app/public/favicon/apple-icon-144x144.png new file mode 100644 index 0000000..f0c735c Binary files /dev/null and b/app/public/favicon/apple-icon-144x144.png differ diff --git a/app/public/favicon/apple-icon-152x152.png b/app/public/favicon/apple-icon-152x152.png new file mode 100644 index 0000000..14eebc5 Binary files /dev/null and b/app/public/favicon/apple-icon-152x152.png differ diff --git a/app/public/favicon/apple-icon-180x180.png b/app/public/favicon/apple-icon-180x180.png new file mode 100644 index 0000000..11c0a5e Binary files /dev/null and b/app/public/favicon/apple-icon-180x180.png differ diff --git a/app/public/favicon/apple-icon-57x57.png b/app/public/favicon/apple-icon-57x57.png new file mode 100644 index 0000000..cfea797 Binary files /dev/null and b/app/public/favicon/apple-icon-57x57.png differ diff --git a/app/public/favicon/apple-icon-60x60.png b/app/public/favicon/apple-icon-60x60.png new file mode 100644 index 0000000..6227583 Binary files /dev/null and b/app/public/favicon/apple-icon-60x60.png differ diff --git a/app/public/favicon/apple-icon-72x72.png b/app/public/favicon/apple-icon-72x72.png new file mode 100644 index 0000000..33eacb4 Binary files /dev/null and b/app/public/favicon/apple-icon-72x72.png differ diff --git a/app/public/favicon/apple-icon-76x76.png b/app/public/favicon/apple-icon-76x76.png new file mode 100644 index 0000000..7792c9e Binary files /dev/null and b/app/public/favicon/apple-icon-76x76.png differ diff --git a/app/public/favicon/browserconfig.xml b/app/public/favicon/browserconfig.xml new file mode 100644 index 0000000..c554148 --- /dev/null +++ b/app/public/favicon/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/app/public/favicon/favicon-16x16.png b/app/public/favicon/favicon-16x16.png new file mode 100644 index 0000000..f0ab510 Binary files /dev/null and b/app/public/favicon/favicon-16x16.png differ diff --git a/app/public/favicon/favicon-256x256.png b/app/public/favicon/favicon-256x256.png new file mode 100644 index 0000000..f0a3fd6 Binary files /dev/null and b/app/public/favicon/favicon-256x256.png differ diff --git a/app/public/favicon/favicon-32x32.png b/app/public/favicon/favicon-32x32.png new file mode 100644 index 0000000..2b45964 Binary files /dev/null and b/app/public/favicon/favicon-32x32.png differ diff --git a/app/public/favicon/favicon-96x96.png b/app/public/favicon/favicon-96x96.png new file mode 100644 index 0000000..f259773 Binary files /dev/null and b/app/public/favicon/favicon-96x96.png differ diff --git a/app/public/favicon/favicon.ico b/app/public/favicon/favicon.ico new file mode 100644 index 0000000..d6e9afc Binary files /dev/null and b/app/public/favicon/favicon.ico differ diff --git a/app/public/favicon/instructions.txt b/app/public/favicon/instructions.txt new file mode 100644 index 0000000..5c7c8c5 --- /dev/null +++ b/app/public/favicon/instructions.txt @@ -0,0 +1,22 @@ +1) Upload images +Upload your images into your domain's root directory + +2) Add HTML code +Insert the following HTML code between the tags: + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/public/favicon/manifest.json b/app/public/favicon/manifest.json new file mode 100644 index 0000000..013d4a6 --- /dev/null +++ b/app/public/favicon/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/app/public/favicon/ms-icon-144x144.png b/app/public/favicon/ms-icon-144x144.png new file mode 100644 index 0000000..f0c735c Binary files /dev/null and b/app/public/favicon/ms-icon-144x144.png differ diff --git a/app/public/favicon/ms-icon-150x150.png b/app/public/favicon/ms-icon-150x150.png new file mode 100644 index 0000000..5125256 Binary files /dev/null and b/app/public/favicon/ms-icon-150x150.png differ diff --git a/app/public/favicon/ms-icon-310x310.png b/app/public/favicon/ms-icon-310x310.png new file mode 100644 index 0000000..c081dcd Binary files /dev/null and b/app/public/favicon/ms-icon-310x310.png differ diff --git a/app/public/favicon/ms-icon-70x70.png b/app/public/favicon/ms-icon-70x70.png new file mode 100644 index 0000000..ed700d3 Binary files /dev/null and b/app/public/favicon/ms-icon-70x70.png differ diff --git a/app/public/styles/main.scss b/app/public/styles/main.scss index 14576fd..bb6e704 100644 --- a/app/public/styles/main.scss +++ b/app/public/styles/main.scss @@ -2,4 +2,8 @@ main { margin-top: 6em; +} + +.app_notification-container { + margin-bottom: 1em; } \ No newline at end of file diff --git a/app/sw.js b/app/sw.js new file mode 100644 index 0000000..d17a08e --- /dev/null +++ b/app/sw.js @@ -0,0 +1,56 @@ +const version = 'v1'; + +self.addEventListener('install', (event) => { + + // Install and Activate right away + self.skipWaiting(); + + event.waitUntil( + caches.open(version) + .then(cache => cache.addAll([ + // add all files to cache + './offline/index.html' + ])) + ); +}); + +self.addEventListener('activate', function (event){ + console.log('SW activated'); + event.waitUntil( + caches.keys() + .then(function (keys) { + return Promise.all(keys.filter(function (key) { + return key !== version; + }).map(key => { + return caches.delete(key); + })); + }) + ); +}); + +self.addEventListener('fetch', function (event) { + event.respondWith( + caches.match(event.request) + .then(function (res) { + if (res) { + return res; + } + if (!navigator.onLine) { + return caches.match(new Request('/app/offline/index.html')); + } + return fetchAndUpdate(event.request); + }) + ); +}); + +function fetchAndUpdate(request) { + return fetch(request).then(res => { + if (res) { + return caches.open(version) + .then(cache => { + return cache.put(request, res.clone()) + .then(()=> {return res;}); + }); + } + }); +} diff --git a/app/tests/jestsetup.js b/app/tests/jestsetup.js new file mode 100644 index 0000000..dbc82c7 --- /dev/null +++ b/app/tests/jestsetup.js @@ -0,0 +1,7 @@ +import Enzyme, { shallow, render, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +Enzyme.configure({ adapter: new Adapter() }); + +global.shallow = shallow; +global.render = render; +global.mount = mount; diff --git a/app/tests/scenes/Dashboard.test.js b/app/tests/scenes/Dashboard.test.js new file mode 100644 index 0000000..baceb39 --- /dev/null +++ b/app/tests/scenes/Dashboard.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import intl from 'react-intl-universal'; + +// Import Components +import Dashboard from './../../js/scenes/Dashboard'; + +// Initialize React-Intl-Universal with TestData +intl.init({ + currentLocale: 'en-US', + locales : { + 'en-US': require('./../../js/data/locales/en-US.json') + }, +}); + +test('Dashboard -- Snapshot Test', () => { + const wrapper = shallow( true} getDashboardData={() => true} />); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/Events/EventsList.test.js b/app/tests/scenes/Events/EventsList.test.js new file mode 100644 index 0000000..7864b11 --- /dev/null +++ b/app/tests/scenes/Events/EventsList.test.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +// Import Components +import EventsList from '../../../js/scenes/Events/EventsList'; + +test('EventsList -- Snapshot Test', () => { + const wrapper = shallow( true} />); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/Events/__snapshots__/EventsList.test.js.snap b/app/tests/scenes/Events/__snapshots__/EventsList.test.js.snap new file mode 100644 index 0000000..c0acbd0 --- /dev/null +++ b/app/tests/scenes/Events/__snapshots__/EventsList.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EventsList -- Snapshot Test 1`] = ` + + + + + + + + + + + +
+ +
+
+ +
+`; diff --git a/app/tests/scenes/Tags/TagsList.test.js b/app/tests/scenes/Tags/TagsList.test.js new file mode 100644 index 0000000..b6709b9 --- /dev/null +++ b/app/tests/scenes/Tags/TagsList.test.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +// Import Components +import TagsList from '../../../js/scenes/Tags/TagsList'; + +test('TagsList -- Snapshot Test', () => { + const wrapper = shallow( true} />); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/Tags/__snapshots__/TagsList.test.js.snap b/app/tests/scenes/Tags/__snapshots__/TagsList.test.js.snap new file mode 100644 index 0000000..362ce63 --- /dev/null +++ b/app/tests/scenes/Tags/__snapshots__/TagsList.test.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TagsList -- Snapshot Test 1`] = ` + + + + + + + + + + + +
+ +
+
+ + + + +
+`; diff --git a/app/tests/scenes/__snapshots__/Dashboard.test.js.snap b/app/tests/scenes/__snapshots__/Dashboard.test.js.snap new file mode 100644 index 0000000..8bb0df8 --- /dev/null +++ b/app/tests/scenes/__snapshots__/Dashboard.test.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dashboard -- Snapshot Test 1`] = ` + + +
+ Dashboard +
+ + + + + +
+ + Recent Activity + + +
+
+
+`; diff --git a/app/tests/scenes/basics/CharacterCard.test.js b/app/tests/scenes/basics/CharacterCard.test.js new file mode 100644 index 0000000..c9d82be --- /dev/null +++ b/app/tests/scenes/basics/CharacterCard.test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +const testData = { + projectTitle : 'TestTitle', + getProjectTitle: jest.fn(), + character : { + _id : 1, + project : 2, + desc : 'TestDescription', + gender : 'TestGender', + birthday : '01/01/1990', + full_name: 'TestName' + } +}; + +// Import Components +import CharacterCard from '../../../js/basics/CharacterCard'; + +test('CharacterCard -- Snapshot Test', () => { + const wrapper = shallow( testData.getProjectTitle()} + character={testData.character} + projectTitle={testData.projectTitle} + /> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/basics/DeleteModal.test.js b/app/tests/scenes/basics/DeleteModal.test.js new file mode 100644 index 0000000..dca5e62 --- /dev/null +++ b/app/tests/scenes/basics/DeleteModal.test.js @@ -0,0 +1,29 @@ + + +import React from 'react'; +import { shallow } from 'enzyme'; + +const testData = { + close : jest.fn(), + open : jest.fn(), + confirmDelete: jest.fn(), + entity : 'Project', + item : 'TestProject', + target : '/projects' +}; + +// Import Components +import DeleteModal from '../../../js/basics/DeleteModal'; + +test('DeleteModal -- Snapshot Test', () => { + const wrapper = shallow( testData.getProjectTitle()} + open={() => testData.getProjectTitle()} + confirmDelete={() => testData.getProjectTitle()} + entity={testData.entity} + item={testData.item} + target={testData.target} + /> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/basics/LanguageSwitcher.test.js b/app/tests/scenes/basics/LanguageSwitcher.test.js new file mode 100644 index 0000000..b3a983e --- /dev/null +++ b/app/tests/scenes/basics/LanguageSwitcher.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +const testData = { + setCurrentLocale: jest.fn() +}; + +// Import Components +import LanguageSwitcher from '../../../js/basics/LanguageSwitcher'; + +test('LanguageSwitcher -- Snapshot Test', () => { + const wrapper = shallow( testData.setCurrentLocale()} + /> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/basics/Loader.test.js b/app/tests/scenes/basics/Loader.test.js new file mode 100644 index 0000000..1cfbc06 --- /dev/null +++ b/app/tests/scenes/basics/Loader.test.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +// Import Components +import Loader from '../../../js/basics/Loader'; + +test('Loader -- Snapshot Test', () => { + const wrapper = shallow( + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/basics/Navbar.test.js b/app/tests/scenes/basics/Navbar.test.js new file mode 100644 index 0000000..4b765d2 --- /dev/null +++ b/app/tests/scenes/basics/Navbar.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +const testData = { + setCurrentLocale: jest.fn(), +}; + +// Import Components +import Navbar from '../../../js/basics/Navbar'; + +test('Navbar -- Snapshot Test', () => { + const wrapper = shallow( testData.setCurrentLocale()} + /> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/app/tests/scenes/basics/Notification.test.js b/app/tests/scenes/basics/Notification.test.js new file mode 100644 index 0000000..1e23366 --- /dev/null +++ b/app/tests/scenes/basics/Notification.test.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { notificationTypes } from '../../../js/constants/notificationTypes'; + +const testData = { + type : notificationTypes.ERROR, + content : 'A test error occurred', + description: 'Something went wrong' +}; + +// Import Components +import Notification from '../../../js/basics/Notification'; + +test('Notification -- Snapshot Test', () => { + const wrapper = shallow( + ); + expect(wrapper).toMatchSnapshot(); +}); + +test('Notification -- Types Tests', () => { + const NotificationWithError = shallow(); + expect(NotificationWithError.find('.error')).toHaveLength(1); + const NotificationWithWarning = shallow(); + expect(NotificationWithWarning.find('.warning')).toHaveLength(1); + const NotificationWithSuccess = shallow(); + expect(NotificationWithSuccess.find('.success')).toHaveLength(1); + const NotificationWithInfo = shallow(); + expect(NotificationWithInfo.find('.info')).toHaveLength(1); +}); diff --git a/app/tests/scenes/basics/NotificationCenter.test.js b/app/tests/scenes/basics/NotificationCenter.test.js new file mode 100644 index 0000000..e4897cf --- /dev/null +++ b/app/tests/scenes/basics/NotificationCenter.test.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { shallow, mount, render } from 'enzyme'; +import { notificationTypes } from '../../../js/constants/notificationTypes'; +import { Provider } from 'react-redux'; + +const testData = { + messages: [ + { + type : notificationTypes.ERROR, + content : 'A test error occurred', + description: 'Something went wrong' + }, + { + type : notificationTypes.WARN, + content : 'A test warning occurred', + description: 'Something went wrong' + }, + { + type : notificationTypes.INFO, + content: 'Here be dragons', + }, + { + type : notificationTypes.SUCCESS, + content : 'Yay you did it!', + description: 'A job well done' + }, + ], + store: { + dispatch : jest.fn(), + getState : jest.fn(), + subscribe: jest.fn() + } +}; + +// Import Components +import NotificationCenter from '../../../js/containers/MessengerContainer'; + +test('NotificationCenter -- Snapshot Test', () => { + const wrapper = shallow( + + + ); + expect(wrapper).toMatchSnapshot(); +}); + +/* test('NotificationCenter -- Type Test', () => { + const wrapper = render( + + + ); + expect(wrapper.find(NotificationCenter).children()).toHaveLength(4); +}); */ diff --git a/app/tests/scenes/basics/__snapshots__/CharacterCard.test.js.snap b/app/tests/scenes/basics/__snapshots__/CharacterCard.test.js.snap new file mode 100644 index 0000000..d96edaf --- /dev/null +++ b/app/tests/scenes/basics/__snapshots__/CharacterCard.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CharacterCard -- Snapshot Test 1`] = ` + + + + + TestName + + + + + + 01/01/1990 + + + + + TestGender + + + TestDescription + + + + + + + + + +