├── app ├── utils │ ├── .gitkeep │ ├── rendererUtils.js │ ├── data.js │ └── bootstrap.js ├── variables.scss ├── app.icns ├── assets │ └── logo.png ├── components │ ├── Home.css │ ├── Home.js │ ├── Counter.css │ ├── Counter.js │ └── ItemList.jsx ├── managers │ └── screenManager.js ├── pages │ ├── settings │ │ ├── CountdownPage.jsx │ │ ├── LanguagePage.jsx │ │ ├── MigrationsPage.jsx │ │ ├── BibleTranslationsPage.jsx │ │ └── ProjectorsPage.jsx │ ├── elements │ │ ├── AnnouncementsPage.jsx │ │ ├── PresentationsPage.jsx │ │ ├── ScripturePage.jsx │ │ ├── media │ │ │ ├── MediaPage.jsx │ │ │ ├── MediaPickerDialog.jsx │ │ │ └── MediaComponent.jsx │ │ ├── background │ │ │ ├── BackgroundsPage.jsx │ │ │ ├── BackgroundsPickerDialog.jsx │ │ │ └── BackgroundsComponent.jsx │ │ └── songs │ │ │ ├── SongsPage.jsx │ │ │ └── SongsPageComponent.jsx │ ├── NewsPage.jsx │ ├── live │ │ └── LivePage.jsx │ └── productions │ │ ├── ProductionPage.jsx │ │ └── ProductionPageComponent.jsx ├── layout │ ├── Root.jsx │ ├── SettingsLayout.jsx │ ├── DashboardLayout.jsx │ └── ElementsLayout.jsx ├── index.jsx ├── data │ └── stores │ │ ├── AppStore.js │ │ ├── localdb.js │ │ ├── ScreenStore.js │ │ ├── ElementStore.js │ │ └── ProductionStore.js ├── app.global.scss ├── app.html ├── main.dev.js ├── menu.js └── projector │ └── projector.html ├── .npmrc ├── internals ├── mocks │ └── fileMock.js ├── flow │ ├── WebpackAsset.js.flow │ └── CSSModule.js.flow ├── img │ ├── js.png │ ├── npm.png │ ├── flow.png │ ├── jest.png │ ├── mobx.png │ ├── react.png │ ├── redux.png │ ├── yarn.png │ ├── eslint.png │ ├── webpack.png │ ├── js-padded.png │ ├── eslint-padded.png │ ├── flow-padded.png │ ├── jest-padded.png │ ├── react-padded.png │ ├── react-router.png │ ├── redux-padded.png │ ├── yarn-padded.png │ ├── flow-padded-90.png │ ├── jest-padded-90.png │ ├── react-padded-90.png │ ├── redux-padded-90.png │ ├── webpack-padded.png │ ├── yarn-padded-90.png │ ├── eslint-padded-90.png │ ├── webpack-padded-90.png │ ├── react-router-padded.png │ └── react-router-padded-90.png └── scripts │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ └── CheckBuiltsExist.js ├── resources ├── icon.ico ├── icon.png ├── icon.icns └── icons │ ├── 16x16.png │ ├── 24x24.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── 96x96.png │ ├── 128x128.png │ ├── 256x256.png │ ├── 512x512.png │ └── 1024x1024.png ├── flow-typed └── module_vx.x.x.js ├── test ├── example.js ├── e2e │ ├── helpers.js │ └── HomePage.e2e.js ├── .eslintrc ├── actions │ ├── __snapshots__ │ │ └── counter.spec.js.snap │ └── counter.spec.js ├── reducers │ ├── __snapshots__ │ │ └── counter.spec.js.snap │ └── counter.spec.js ├── components │ ├── __snapshots__ │ │ └── Counter.spec.js.snap │ └── Counter.spec.js └── containers │ └── CounterPage.spec.js ├── renovate.json ├── configs ├── webpack.config.eslint.js ├── webpack.config.base.js ├── webpack.config.renderer.dev.dll.babel.js ├── webpack.config.main.prod.babel.js ├── webpack.config.renderer.prod.babel.js └── webpack.config.renderer.dev.babel.js ├── appveyor.yml ├── .vscode └── launch.json ├── .gitignore ├── LICENSE ├── babel.config.js ├── i18n ├── languages │ └── en.js └── i18n.js ├── README.md ├── package.json └── CHANGELOG.md /app/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true -------------------------------------------------------------------------------- /app/variables.scss: -------------------------------------------------------------------------------- 1 | $bgColor: #f8f5f4; 2 | $borderColor: #e4d7cc; -------------------------------------------------------------------------------- /internals/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/app/app.icns -------------------------------------------------------------------------------- /internals/flow/WebpackAsset.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string 3 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icon.png -------------------------------------------------------------------------------- /app/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/app/assets/logo.png -------------------------------------------------------------------------------- /flow-typed/module_vx.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'module' { 2 | declare module.exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /internals/flow/CSSModule.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare export default { [key: string]: string } -------------------------------------------------------------------------------- /internals/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/js.png -------------------------------------------------------------------------------- /internals/img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/npm.png -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /internals/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/flow.png -------------------------------------------------------------------------------- /internals/img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/jest.png -------------------------------------------------------------------------------- /internals/img/mobx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/mobx.png -------------------------------------------------------------------------------- /internals/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/react.png -------------------------------------------------------------------------------- /internals/img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/redux.png -------------------------------------------------------------------------------- /internals/img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/yarn.png -------------------------------------------------------------------------------- /internals/img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/eslint.png -------------------------------------------------------------------------------- /internals/img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/webpack.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/96x96.png -------------------------------------------------------------------------------- /internals/img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/js-padded.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/512x512.png -------------------------------------------------------------------------------- /internals/img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/eslint-padded.png -------------------------------------------------------------------------------- /internals/img/flow-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/flow-padded.png -------------------------------------------------------------------------------- /internals/img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/jest-padded.png -------------------------------------------------------------------------------- /internals/img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/react-padded.png -------------------------------------------------------------------------------- /internals/img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/react-router.png -------------------------------------------------------------------------------- /internals/img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/redux-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/yarn-padded.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /internals/img/flow-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/flow-padded-90.png -------------------------------------------------------------------------------- /internals/img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/jest-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/react-padded-90.png -------------------------------------------------------------------------------- /internals/img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/redux-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/webpack-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/yarn-padded-90.png -------------------------------------------------------------------------------- /internals/img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/eslint-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/webpack-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/react-router-padded.png -------------------------------------------------------------------------------- /internals/img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyobo/epicworshipneo/HEAD/internals/img/react-router-padded-90.png -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | describe('description', () => { 2 | it('should have description', () => { 3 | expect(1 + 2).toBe(3); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/e2e/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { ClientFunction } from 'testcafe'; 3 | 4 | export const getPageUrl = ClientFunction(() => window.location.href); 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "rangeStrategy": "bump", 4 | "baseBranches": ["next"], 5 | "automerge": true, 6 | "major": { 7 | "automerge": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /configs/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | require('@babel/register'); 3 | 4 | module.exports = require('./webpack.config.renderer.dev.babel').default; 5 | -------------------------------------------------------------------------------- /app/components/Home.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | top: 30%; 4 | left: 10px; 5 | text-align: center; 6 | } 7 | 8 | .container h2 { 9 | font-size: 5rem; 10 | } 11 | 12 | .container a { 13 | font-size: 1.4rem; 14 | } 15 | -------------------------------------------------------------------------------- /app/managers/screenManager.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | 3 | let screens = [] 4 | 5 | export const initializeScreens = (app) =>{ 6 | console.log('Initializing Message Pipes...'); 7 | 8 | screens = electron.screen.getAllDisplays() 9 | 10 | console.log('screens defined.') 11 | } -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:testcafe/recommended", 3 | "env": { 4 | "jest/globals": true 5 | }, 6 | "plugins": ["jest", "testcafe"], 7 | "rules": { 8 | "jest/no-disabled-tests": "warn", 9 | "jest/no-focused-tests": "error", 10 | "jest/no-identical-title": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/pages/settings/CountdownPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | @inject("store") 5 | @observer 6 | export default class CountdownPage extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/pages/settings/LanguagePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | @inject("store") 5 | @observer 6 | export default class LanguagePage extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/pages/settings/MigrationsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | @inject("store") 5 | @observer 6 | export default class MigrationsPage extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/pages/elements/AnnouncementsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | @inject("store") 5 | @observer 6 | export default class AnnouncementsPage extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/pages/elements/PresentationsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | @inject("store") 5 | @observer 6 | export default class PresentationsPage extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/utils/rendererUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a proxy to a normal object. 3 | * @param proxyParam 4 | */ 5 | export const proxyToObject = (proxyParam)=>{ 6 | const obj = {}; 7 | const keys = Object.getOwnPropertyNames(proxyParam); 8 | 9 | console.log('proxyToObject', keys) 10 | 11 | keys.forEach(key=>{ 12 | obj[key] = proxyParam[key] 13 | }); 14 | return obj; 15 | } -------------------------------------------------------------------------------- /test/actions/__snapshots__/counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`actions should decrement should create decrement action 1`] = ` 4 | Object { 5 | "type": "DECREMENT_COUNTER", 6 | } 7 | `; 8 | 9 | exports[`actions should increment should create increment action 1`] = ` 10 | Object { 11 | "type": "INCREMENT_COUNTER", 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /app/pages/settings/BibleTranslationsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | @inject("store") 5 | @observer 6 | export default class BibleTranslationsPage extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/pages/elements/ScripturePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | 4 | @inject("store") 5 | @observer 6 | export default class ScripturePage extends Component { 7 | render() { 8 | return ( 9 |
10 |

Scripture

11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/reducers/__snapshots__/counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reducers counter should handle DECREMENT_COUNTER 1`] = `0`; 4 | 5 | exports[`reducers counter should handle INCREMENT_COUNTER 1`] = `2`; 6 | 7 | exports[`reducers counter should handle initial state 1`] = `0`; 8 | 9 | exports[`reducers counter should handle unknown action type 1`] = `1`; 10 | -------------------------------------------------------------------------------- /app/pages/settings/ProjectorsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import { T } from "../../../i18n/i18n"; 4 | 5 | @inject("store") 6 | @observer 7 | export default class ProjectorsPage extends Component { 8 | render() { 9 | return ( 10 |
11 |

12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internals/scripts/CheckNodeEnv.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | 4 | export default function CheckNodeEnv(expectedEnv: string) { 5 | if (!expectedEnv) { 6 | throw new Error('"expectedEnv" not set'); 7 | } 8 | 9 | if (process.env.NODE_ENV !== expectedEnv) { 10 | console.log( 11 | chalk.whiteBright.bgRed.bold( 12 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 13 | ) 14 | ); 15 | process.exit(2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/Home.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import routes from '../constants/routes'; 5 | import styles from './Home.css'; 6 | 7 | type Props = {}; 8 | 9 | export default class Home extends Component { 10 | props: Props; 11 | 12 | render() { 13 | return ( 14 |
15 |

Home

16 | to Counter 17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/utils/data.js: -------------------------------------------------------------------------------- 1 | export const elementTypes = { 2 | SONG: "song", 3 | SCRIPTURE: "scripture", //production bound 4 | MEDIA: "media", 5 | BACKGROUND: "background", 6 | ANNOUNCEMENT: "announcement", 7 | PRESENTATION: "presentation", 8 | NOTE: "note" //production bound 9 | }; 10 | 11 | export const entityTypes = { 12 | PRODUCTION: "production", 13 | ELEMENT: "element", 14 | SETTING: "setting" 15 | }; 16 | 17 | export const settings = { 18 | liveProductionId: "liveProductionId", 19 | projectorScreenId: 'projectorScreenId' 20 | }; -------------------------------------------------------------------------------- /app/pages/elements/media/MediaPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import MediaComponent from "./MediaComponent"; 4 | 5 | 6 | @inject("store") 7 | @observer 8 | export default class MediaPage extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | getSelectedId() { 15 | return this.props.match.params.id; 16 | } 17 | 18 | render() { 19 | 20 | return ( 21 | 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/pages/elements/background/BackgroundsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import BackgroundsComponent from "./BackgroundsComponent"; 4 | 5 | 6 | 7 | @inject("store") 8 | @observer 9 | export default class BackgroundsPage extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | getSelectedId() { 16 | return this.props.match.params.id; 17 | } 18 | 19 | render() { 20 | 21 | return ( 22 | 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internals/scripts/CheckPortInUse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | import detectPort from 'detect-port'; 4 | 5 | (function CheckPortInUse() { 6 | const port: string = process.env.PORT || '1212'; 7 | 8 | detectPort(port, (err: ?Error, availablePort: number) => { 9 | if (port !== String(availablePort)) { 10 | throw new Error( 11 | chalk.whiteBright.bgRed.bold( 12 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev` 13 | ) 14 | ); 15 | } else { 16 | process.exit(0); 17 | } 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | 3 | platform: 4 | - x64 5 | 6 | environment: 7 | matrix: 8 | - nodejs_version: 10 9 | 10 | cache: 11 | - '%LOCALAPPDATA%/Yarn' 12 | - node_modules 13 | - flow-typed 14 | - '%USERPROFILE%\.electron' 15 | 16 | matrix: 17 | fast_finish: true 18 | 19 | build: off 20 | 21 | version: '{build}' 22 | 23 | shallow_clone: true 24 | 25 | clone_depth: 1 26 | 27 | install: 28 | - ps: Install-Product node $env:nodejs_version x64 29 | - set CI=true 30 | - yarn 31 | 32 | test_script: 33 | - yarn package-ci 34 | - yarn lint 35 | # - yarn flow 36 | - yarn test 37 | - yarn build-e2e 38 | - yarn test-e2e 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Launch via NPM", 10 | "type": "node", 11 | "request": "launch", 12 | "cwd": "${workspaceRoot}", 13 | "runtimeExecutable": "npm", 14 | "runtimeArgs": [ 15 | "run-script", "dev", "--", "--inspect-brk=9229" 16 | ], 17 | "port": 9229, 18 | "protocol": "inspector" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /app/components/Counter.css: -------------------------------------------------------------------------------- 1 | .backButton { 2 | position: absolute; 3 | } 4 | 5 | .counter { 6 | position: absolute; 7 | top: 30%; 8 | left: 45%; 9 | font-size: 10rem; 10 | font-weight: bold; 11 | letter-spacing: -0.025em; 12 | } 13 | 14 | .btnGroup { 15 | position: relative; 16 | top: 500px; 17 | width: 480px; 18 | margin: 0 auto; 19 | } 20 | 21 | .btn { 22 | font-size: 1.6rem; 23 | font-weight: bold; 24 | background-color: #fff; 25 | border-radius: 50%; 26 | margin: 10px; 27 | width: 100px; 28 | height: 100px; 29 | opacity: 0.7; 30 | cursor: pointer; 31 | font-family: Arial, Helvetica, Helvetica Neue, sans-serif; 32 | } 33 | 34 | .btn:hover { 35 | color: white; 36 | background-color: rgba(0, 0, 0, 0.5); 37 | } 38 | -------------------------------------------------------------------------------- /app/pages/NewsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import { Link } from "react-router-dom"; 4 | import { T } from "../../i18n/i18n"; 5 | 6 | @inject("store") 7 | @observer 8 | export default class NewsPage extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 |
14 | Productions 15 |
16 |
17 |
18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import counter from '../../app/reducers/counter'; 2 | import { 3 | INCREMENT_COUNTER, 4 | DECREMENT_COUNTER 5 | } from '../../app/actions/counter'; 6 | 7 | describe('reducers', () => { 8 | describe('counter', () => { 9 | it('should handle initial state', () => { 10 | expect(counter(undefined, {})).toMatchSnapshot(); 11 | }); 12 | 13 | it('should handle INCREMENT_COUNTER', () => { 14 | expect(counter(1, { type: INCREMENT_COUNTER })).toMatchSnapshot(); 15 | }); 16 | 17 | it('should handle DECREMENT_COUNTER', () => { 18 | expect(counter(1, { type: DECREMENT_COUNTER })).toMatchSnapshot(); 19 | }); 20 | 21 | it('should handle unknown action type', () => { 22 | expect(counter(1, { type: 'unknown' })).toMatchSnapshot(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /app/pages/elements/media/MediaPickerDialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import MediaComponent from "./MediaComponent"; 4 | import Modal from "react-responsive-modal"; 5 | 6 | 7 | type MediaDialogType= { 8 | selectedId: string, 9 | open: boolean, 10 | onClose?: func 11 | } 12 | 13 | @inject("store") 14 | @observer 15 | export default class MediaPickerDialog extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | } 20 | 21 | getSelectedId() { 22 | return this.props.selectedId; 23 | } 24 | 25 | render() { 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/pages/elements/background/BackgroundsPickerDialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import Modal from "react-responsive-modal"; 4 | import BackgroundsComponent from "./BackgroundsComponent"; 5 | 6 | 7 | type MediaDialogType= { 8 | selectedId: string, 9 | open: boolean, 10 | onClose?: func 11 | } 12 | 13 | @inject("store") 14 | @observer 15 | export default class BackgroundsPickerDialog extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | } 20 | 21 | getSelectedId() { 22 | return this.props.selectedId; 23 | } 24 | 25 | render() { 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internals/scripts/CheckBuiltsExist.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Check if the renderer and main bundles are built 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | import fs from 'fs'; 6 | 7 | function CheckBuildsExist() { 8 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); 9 | const rendererPath = path.join( 10 | __dirname, 11 | '..', 12 | '..', 13 | 'app', 14 | 'dist', 15 | 'renderer.prod.js' 16 | ); 17 | 18 | if (!fs.existsSync(mainPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The main process is not built yet. Build it by running "yarn build-main"' 22 | ) 23 | ); 24 | } 25 | 26 | if (!fs.existsSync(rendererPath)) { 27 | throw new Error( 28 | chalk.whiteBright.bgRed.bold( 29 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"' 30 | ) 31 | ); 32 | } 33 | } 34 | 35 | CheckBuildsExist(); 36 | -------------------------------------------------------------------------------- /.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 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # flow-typed 34 | flow-typed/npm/* 35 | !flow-typed/npm/module_vx.x.x.js 36 | 37 | # App packaged 38 | release 39 | app/main.prod.js 40 | app/main.prod.js.map 41 | app/renderer.prod.js 42 | app/renderer.prod.js.map 43 | app/style.css 44 | app/style.css.map 45 | dist 46 | dll 47 | main.js 48 | main.js.map 49 | 50 | .idea 51 | npm-debug.log.* 52 | 53 | database/* 54 | epicworshipdb* 55 | 56 | db* 57 | epicdb* 58 | mediafiles/* -------------------------------------------------------------------------------- /app/layout/Root.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component, Fragment } from "react"; 3 | 4 | import { Provider } from "mobx-react"; 5 | import appStore from "../data/stores/AppStore"; 6 | import SettingsLayout from "./SettingsLayout"; 7 | import DashboardLayout from "./DashboardLayout"; 8 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 9 | import Loading from "react-loading-bar"; 10 | import "react-loading-bar/dist/index.css"; 11 | 12 | 13 | export default class Root extends Component { 14 | 15 | componentDidCatch(error) { 16 | toast.error({ title: "Oops", message: error.message }); 17 | } 18 | 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/pages/live/LivePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import ItemList from "../../components/ItemList"; 4 | 5 | @inject("store") 6 | @observer 7 | export default class LivePage extends Component { 8 | 9 | onItemClick = (item) => { 10 | this.props.store.screenStore.sendToProjector('',item.payload); 11 | }; 12 | 13 | render() { 14 | const elementStore = this.props.store.elementStore; 15 | const prodStore = this.props.store.productionStore; 16 | return ( 17 |
18 |

Live

19 | 29 | 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present C. T. Lin 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 | 23 | -------------------------------------------------------------------------------- /configs/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { dependencies } from '../package.json'; 8 | 9 | export default { 10 | externals: [...Object.keys(dependencies || {})], 11 | 12 | 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | cacheDirectory: true 22 | } 23 | } 24 | } 25 | ] 26 | }, 27 | 28 | 29 | 30 | output: { 31 | path: path.join(__dirname, '..', 'app'), 32 | // https://github.com/webpack/webpack/issues/1114 33 | libraryTarget: 'commonjs2' 34 | }, 35 | 36 | /** 37 | * Determine the array of extensions that should be used to resolve modules. 38 | */ 39 | resolve: { 40 | extensions: ['.js', '.jsx', '.json'], 41 | // alias: { 42 | // common: path.resolve(__dirname, 'app/common/') 43 | // } 44 | }, 45 | 46 | plugins: [ 47 | new webpack.EnvironmentPlugin({ 48 | NODE_ENV: 'production' 49 | }), 50 | 51 | new webpack.NamedModulesPlugin() 52 | ] 53 | }; 54 | -------------------------------------------------------------------------------- /app/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import { render } from "react-dom"; 4 | import { AppContainer } from "react-hot-loader"; 5 | // import Root from "./layout/Root"; 6 | import "./app.global.scss"; 7 | 8 | const el = require('electron'); 9 | const electron = require("electron").remote; 10 | 11 | const path = electron.require('path'); 12 | const os = electron.require('os'); 13 | const storageFolder = path.join(os.homedir(), 'epicworshipData'); 14 | const { initializeData, setConfig } = require('./data/stores/localdb'); 15 | 16 | (async function(){ 17 | 18 | console.log({ 19 | elTek: el.remote.app.getAppPath(), 20 | processCWD: process.cwd(), 21 | 22 | }) 23 | 24 | await initializeData({storageFolder, verbose: true}); 25 | 26 | const Root = require("./layout/Root").default; 27 | render( 28 | 29 | 30 | , 31 | document.getElementById("root") 32 | ); 33 | 34 | if (module.hot) { 35 | module.hot.accept("./layout/Root", () => { 36 | // eslint-disable-next-line global-require 37 | const NextRoot = require("./layout/Root").default; 38 | render( 39 | 40 | 41 | , 42 | document.getElementById("root") 43 | ); 44 | }); 45 | } 46 | 47 | 48 | })(); -------------------------------------------------------------------------------- /app/data/stores/AppStore.js: -------------------------------------------------------------------------------- 1 | import ScreenStore from "./ScreenStore"; 2 | import ProductionStore from "./ProductionStore"; 3 | import ElementStore from "./ElementStore"; 4 | import { observable, action } from "mobx"; 5 | import { elementTypes } from "../../utils/data"; 6 | 7 | class AppStore { 8 | 9 | @observable isBusy = false; 10 | history = null; 11 | 12 | constructor() { 13 | this.screenStore = new ScreenStore(this); 14 | this.productionStore = new ProductionStore(this); 15 | this.elementStore = new ElementStore(this); 16 | } 17 | 18 | 19 | setHistory = (history) => { 20 | this.history = history; 21 | }; 22 | 23 | navigateToProduction = (productionId) => { 24 | this.navigateToElement(elementTypes.PRODUCTION, productionId); 25 | }; 26 | 27 | navigateToElement = (elementType, id) => { 28 | if (!this.history) throw new Error("Cannot navigate: History not set"); 29 | 30 | if (elementType === elementTypes.PRODUCTION) { 31 | this.productionStore.setLastSelectedProduction(id); 32 | this.history.push(`/productions/${id}`); 33 | } else { 34 | console.log('navigating to ',elementType,id) 35 | this.history.push(`/elements/${elementType}/${id}`); 36 | } 37 | 38 | }; 39 | 40 | @action 41 | showBusy() { 42 | this.isBusy = true; 43 | } 44 | 45 | @action 46 | hideBusy() { 47 | this.isBusy = false; 48 | } 49 | 50 | } 51 | 52 | 53 | export default new AppStore(); -------------------------------------------------------------------------------- /app/utils/bootstrap.js: -------------------------------------------------------------------------------- 1 | export const installExtensions = async () => { 2 | const installer = require("electron-devtools-installer"); 3 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 4 | const extensions = ["REACT_DEVELOPER_TOOLS", "REDUX_DEVTOOLS"]; 5 | 6 | return Promise.all( 7 | extensions.map(name => installer.default(installer[name], forceDownload)) 8 | ).catch(console.log); 9 | }; 10 | 11 | 12 | export const initLogger = () => { 13 | 14 | ["log", "warn", "error"].forEach((methodName) => { 15 | const originalMethod = console[methodName]; 16 | console[methodName] = (...args) => { 17 | let initiator = "unknown place"; 18 | const timestamp = new Date().toISOString(); 19 | try { 20 | throw new Error(); 21 | } catch (e) { 22 | if (typeof e.stack === "string") { 23 | let isFirst = true; 24 | for (const line of e.stack.split("\n")) { 25 | const matches = line.match(/^\s+at\s+(.*)/); 26 | if (matches) { 27 | if (!isFirst) { // first line - current function 28 | // second line - caller (what we are looking for) 29 | initiator = matches[1]; 30 | break; 31 | } 32 | isFirst = false; 33 | } 34 | } 35 | } 36 | } 37 | originalMethod.apply(console, [...args, "\n ", `[${timestamp}] @ ${initiator}`]); 38 | }; 39 | }); 40 | 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Counter component should match exact snapshot 1`] = ` 4 |
5 |
6 |
10 | 14 | 17 | 18 |
19 |
23 | 1 24 |
25 |
28 | 38 | 48 | 56 | 64 |
65 |
66 |
67 | `; 68 | -------------------------------------------------------------------------------- /test/actions/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { spy } from 'sinon'; 2 | import * as actions from '../../app/actions/counter'; 3 | 4 | describe('actions', () => { 5 | it('should increment should create increment action', () => { 6 | expect(actions.increment()).toMatchSnapshot(); 7 | }); 8 | 9 | it('should decrement should create decrement action', () => { 10 | expect(actions.decrement()).toMatchSnapshot(); 11 | }); 12 | 13 | it('should incrementIfOdd should create increment action', () => { 14 | const fn = actions.incrementIfOdd(); 15 | expect(fn).toBeInstanceOf(Function); 16 | const dispatch = spy(); 17 | const getState = () => ({ counter: 1 }); 18 | fn(dispatch, getState); 19 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe(true); 20 | }); 21 | 22 | it('should incrementIfOdd shouldnt create increment action if counter is even', () => { 23 | const fn = actions.incrementIfOdd(); 24 | const dispatch = spy(); 25 | const getState = () => ({ counter: 2 }); 26 | fn(dispatch, getState); 27 | expect(dispatch.called).toBe(false); 28 | }); 29 | 30 | // There's no nice way to test this at the moment... 31 | it('should incrementAsync', done => { 32 | const fn = actions.incrementAsync(1); 33 | expect(fn).toBeInstanceOf(Function); 34 | const dispatch = spy(); 35 | fn(dispatch); 36 | setTimeout(() => { 37 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe( 38 | true 39 | ); 40 | done(); 41 | }, 5); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /app/layout/SettingsLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link, NavLink, Route, Switch } from "react-router-dom"; 3 | import { inject, observer } from "mobx-react/index"; 4 | import ProjectorsPage from "../pages/settings/ProjectorsPage"; 5 | import CountdownPage from "../pages/settings/CountdownPage"; 6 | import BibleVersionsPage from "../pages/settings/BibleTranslationsPage"; 7 | import MigrationsPage from "../pages/settings/MigrationsPage"; 8 | import LanguagePage from "../pages/settings/LanguagePage"; 9 | 10 | @inject('store') 11 | @observer 12 | export default class SettingsLayout extends Component { 13 | render() { 14 | return ( 15 |
16 |
    17 |
  • Projectors
  • 18 |
  • Countdown
  • 19 |
  • Bible Versions
  • 20 |
  • Migrations
  • 21 |
  • Language
  • 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-dynamic-require: off */ 2 | 3 | /** 4 | * Builds the DLL for development electron renderer process 5 | */ 6 | 7 | import webpack from 'webpack'; 8 | import path from 'path'; 9 | import merge from 'webpack-merge'; 10 | import baseConfig from './webpack.config.base'; 11 | import { dependencies } from '../package.json'; 12 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 13 | 14 | CheckNodeEnv('development'); 15 | 16 | const dist = path.join(__dirname, '..', 'dll'); 17 | 18 | export default merge.smart(baseConfig, { 19 | context: path.join(__dirname, '..'), 20 | 21 | devtool: 'eval', 22 | 23 | mode: 'development', 24 | 25 | target: 'electron-renderer', 26 | 27 | externals: ['fsevents', 'crypto-browserify'], 28 | 29 | /** 30 | * Use `module` from `webpack.config.renderer.dev.js` 31 | */ 32 | module: require('./webpack.config.renderer.dev.babel').default.module, 33 | 34 | entry: { 35 | renderer: Object.keys(dependencies || {}) 36 | }, 37 | 38 | output: { 39 | library: 'renderer', 40 | path: dist, 41 | filename: '[name].dev.dll.js', 42 | libraryTarget: 'var' 43 | }, 44 | 45 | plugins: [ 46 | new webpack.DllPlugin({ 47 | path: path.join(dist, '[name].json'), 48 | name: '[name]' 49 | }), 50 | 51 | /** 52 | * Create global constants which can be configured at compile time. 53 | * 54 | * Useful for allowing different behaviour between development builds and 55 | * release builds 56 | * 57 | * NODE_ENV should be production so that modules do not perform certain 58 | * development checks 59 | */ 60 | new webpack.EnvironmentPlugin({ 61 | NODE_ENV: 'development' 62 | }), 63 | 64 | new webpack.LoaderOptionsPlugin({ 65 | debug: true, 66 | options: { 67 | context: path.join(__dirname, '..', 'app'), 68 | output: { 69 | path: path.join(__dirname, '..', 'dll') 70 | } 71 | } 72 | }) 73 | ] 74 | }); 75 | -------------------------------------------------------------------------------- /app/components/Counter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import styles from './Counter.css'; 5 | import routes from '../constants/routes'; 6 | 7 | type Props = { 8 | increment: () => void, 9 | incrementIfOdd: () => void, 10 | incrementAsync: () => void, 11 | decrement: () => void, 12 | counter: number 13 | }; 14 | 15 | export default class Counter extends Component { 16 | props: Props; 17 | 18 | render() { 19 | const { 20 | increment, 21 | incrementIfOdd, 22 | incrementAsync, 23 | decrement, 24 | counter 25 | } = this.props; 26 | return ( 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | {counter} 35 |
36 |
37 | 45 | 53 | 61 | 69 |
70 |
71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/containers/CounterPage.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { Provider } from 'react-redux'; 5 | import { createBrowserHistory } from 'history'; 6 | import { ConnectedRouter } from 'connected-react-router'; 7 | import CounterPage from '../../app/containers/CounterPage'; 8 | import { configureStore } from '../../app/store/configureStore'; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | function setup(initialState) { 13 | const store = configureStore(initialState); 14 | const history = createBrowserHistory(); 15 | const provider = ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | const app = mount(provider); 23 | return { 24 | app, 25 | buttons: app.find('button'), 26 | p: app.find('.counter') 27 | }; 28 | } 29 | 30 | describe('containers', () => { 31 | describe('App', () => { 32 | it('should display initial count', () => { 33 | const { p } = setup(); 34 | expect(p.text()).toMatch(/^0$/); 35 | }); 36 | 37 | it('should display updated count after increment button click', () => { 38 | const { buttons, p } = setup(); 39 | buttons.at(0).simulate('click'); 40 | expect(p.text()).toMatch(/^1$/); 41 | }); 42 | 43 | it('should display updated count after decrement button click', () => { 44 | const { buttons, p } = setup(); 45 | buttons.at(1).simulate('click'); 46 | expect(p.text()).toMatch(/^-1$/); 47 | }); 48 | 49 | it('shouldnt change if even and if odd button clicked', () => { 50 | const { buttons, p } = setup(); 51 | buttons.at(2).simulate('click'); 52 | expect(p.text()).toMatch(/^0$/); 53 | }); 54 | 55 | it('should change if odd and if odd button clicked', () => { 56 | const { buttons, p } = setup({ counter: 1 }); 57 | buttons.at(2).simulate('click'); 58 | expect(p.text()).toMatch(/^2$/); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /configs/webpack.config.main.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import merge from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 12 | 13 | CheckNodeEnv('production'); 14 | 15 | export default merge.smart(baseConfig, { 16 | devtool: 'source-map', 17 | 18 | mode: 'production', 19 | 20 | target: 'electron-main', 21 | 22 | entry: './app/main.dev', 23 | 24 | output: { 25 | path: path.join(__dirname, '..'), 26 | filename: './app/main.prod.js' 27 | }, 28 | 29 | optimization: { 30 | minimizer: process.env.E2E_BUILD 31 | ? [] 32 | : [ 33 | new TerserPlugin({ 34 | parallel: true, 35 | sourceMap: true, 36 | cache: true 37 | }) 38 | ] 39 | }, 40 | 41 | plugins: [ 42 | new BundleAnalyzerPlugin({ 43 | analyzerMode: 44 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 45 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 46 | }), 47 | 48 | /** 49 | * Create global constants which can be configured at compile time. 50 | * 51 | * Useful for allowing different behaviour between development builds and 52 | * release builds 53 | * 54 | * NODE_ENV should be production so that modules do not perform certain 55 | * development checks 56 | */ 57 | new webpack.EnvironmentPlugin({ 58 | NODE_ENV: 'production', 59 | DEBUG_PROD: false, 60 | START_MINIMIZED: false 61 | }) 62 | ], 63 | 64 | /** 65 | * Disables webpack processing of __dirname and __filename. 66 | * If you run the bundle in node.js it falls back to these values of node.js. 67 | * https://github.com/webpack/webpack/issues/2010 68 | */ 69 | node: { 70 | __dirname: false, 71 | __filename: false 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import { spy } from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | import renderer from 'react-test-renderer'; 7 | import Counter from '../../app/components/Counter'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | function setup() { 12 | const actions = { 13 | increment: spy(), 14 | incrementIfOdd: spy(), 15 | incrementAsync: spy(), 16 | decrement: spy() 17 | }; 18 | const component = shallow(); 19 | return { 20 | component, 21 | actions, 22 | buttons: component.find('button'), 23 | p: component.find('.counter') 24 | }; 25 | } 26 | 27 | describe('Counter component', () => { 28 | it('should should display count', () => { 29 | const { p } = setup(); 30 | expect(p.text()).toMatch(/^1$/); 31 | }); 32 | 33 | it('should first button should call increment', () => { 34 | const { buttons, actions } = setup(); 35 | buttons.at(0).simulate('click'); 36 | expect(actions.increment.called).toBe(true); 37 | }); 38 | 39 | it('should match exact snapshot', () => { 40 | const { actions } = setup(); 41 | const counter = ( 42 |
43 | 44 | 45 | 46 |
47 | ); 48 | const tree = renderer.create(counter).toJSON(); 49 | 50 | expect(tree).toMatchSnapshot(); 51 | }); 52 | 53 | it('should second button should call decrement', () => { 54 | const { buttons, actions } = setup(); 55 | buttons.at(1).simulate('click'); 56 | expect(actions.decrement.called).toBe(true); 57 | }); 58 | 59 | it('should third button should call incrementIfOdd', () => { 60 | const { buttons, actions } = setup(); 61 | buttons.at(2).simulate('click'); 62 | expect(actions.incrementIfOdd.called).toBe(true); 63 | }); 64 | 65 | it('should fourth button should call incrementAsync', () => { 66 | const { buttons, actions } = setup(); 67 | buttons.at(3).simulate('click'); 68 | expect(actions.incrementAsync.called).toBe(true); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [require('react-hot-loader/babel')]; 6 | 7 | const productionPlugins = [ 8 | require('babel-plugin-dev-expression'), 9 | 10 | // babel-preset-react-optimize 11 | require('@babel/plugin-transform-react-constant-elements'), 12 | require('@babel/plugin-transform-react-inline-elements'), 13 | require('babel-plugin-transform-react-remove-prop-types') 14 | ]; 15 | 16 | module.exports = api => { 17 | // see docs about api at https://babeljs.io/docs/en/config-files#apicache 18 | 19 | const development = api.env(developmentEnvironments); 20 | 21 | return { 22 | presets: [ 23 | [ 24 | require('@babel/preset-env'), 25 | { 26 | targets: { electron: require('electron/package.json').version }, 27 | useBuiltIns: 'usage' 28 | } 29 | ], 30 | require('@babel/preset-flow'), 31 | [require('@babel/preset-react'), { development }] 32 | ], 33 | plugins: [ 34 | // Stage 0 35 | require('@babel/plugin-proposal-function-bind'), 36 | 37 | // Stage 1 38 | require('@babel/plugin-proposal-export-default-from'), 39 | require('@babel/plugin-proposal-logical-assignment-operators'), 40 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }], 41 | [ 42 | require('@babel/plugin-proposal-pipeline-operator'), 43 | { proposal: 'minimal' } 44 | ], 45 | [ 46 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 47 | { loose: false } 48 | ], 49 | require('@babel/plugin-proposal-do-expressions'), 50 | 51 | // Stage 2 52 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 53 | require('@babel/plugin-proposal-function-sent'), 54 | require('@babel/plugin-proposal-export-namespace-from'), 55 | require('@babel/plugin-proposal-numeric-separator'), 56 | require('@babel/plugin-proposal-throw-expressions'), 57 | 58 | // Stage 3 59 | require('@babel/plugin-syntax-dynamic-import'), 60 | require('@babel/plugin-syntax-import-meta'), 61 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 62 | require('@babel/plugin-proposal-json-strings'), 63 | 64 | ...(development ? developmentPlugins : productionPlugins) 65 | ] 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /app/app.global.scss: -------------------------------------------------------------------------------- 1 | @import "~@fortawesome/fontawesome-free/css/all.css"; 2 | @import "./variables.scss"; 3 | 4 | html, body { 5 | //background-color: rgb(248, 248, 248); 6 | background-color: $bgColor; 7 | 8 | //position: relative; 9 | color: #4b4b4b; 10 | height: 100%; 11 | } 12 | 13 | .dashboardBody { 14 | padding-left: 20px; 15 | padding-right: 5px; 16 | padding-bottom: 10px; 17 | } 18 | 19 | .uk-navbar-nav { 20 | a.active { 21 | color: #555; 22 | font-weight: bolder; 23 | } 24 | } 25 | 26 | .uk-subnav { 27 | a.active { 28 | color: #555; 29 | font-weight: bolder; 30 | } 31 | } 32 | 33 | .mainnav { 34 | padding-left: 20px; 35 | border-bottom: 1px solid $borderColor; 36 | } 37 | 38 | .pad10 { 39 | padding: 10px; 40 | } 41 | 42 | 43 | section { 44 | background-color: white; 45 | border-radius: 5px; 46 | //border: 1px solid #cecece; 47 | padding: 15px; 48 | } 49 | 50 | .flexContainer { 51 | display: flex; 52 | flex-direction: row; 53 | align-items: stretch; 54 | flex-wrap: nowrap; 55 | height: 100%; 56 | 57 | > div { 58 | padding-right: 5px; 59 | margin-right: 5px; 60 | align-self: flex-start; 61 | flex-grow: 1; 62 | } 63 | } 64 | 65 | .searchBox { 66 | margin-top: 10px; 67 | background-color: oldlace; 68 | } 69 | 70 | .sidePanel { 71 | max-width: 400px; 72 | } 73 | 74 | .itemListWrapper { 75 | margin-top: 10px; 76 | 77 | //height: calc(100vh - 270px); 78 | overflow-y: auto; 79 | } 80 | 81 | .itemList { 82 | 83 | //max-width: 300px; 84 | background-color: white; 85 | //border: 1px solid $borderColor; 86 | display: block; 87 | //position: relative; 88 | //bottom: 0px; 89 | overflow: scroll; 90 | 91 | li { 92 | padding: 10px; 93 | cursor: pointer; 94 | margin: 0 !important; 95 | 96 | //background-color: #e5ffff; 97 | border-top: 1px solid $borderColor; 98 | //border-bottom: 1px solid $borderColor; 99 | 100 | &.selected { 101 | background-color: #3fc3ee; 102 | } 103 | 104 | &.active { 105 | background-color: #a5dc86; 106 | } 107 | } 108 | } 109 | textarea.epicTextArea{ 110 | min-height: 200px; 111 | height: 40vh; 112 | max-width: 100%; 113 | line-height: 25px; 114 | } 115 | 116 | 117 | .h100{ 118 | height: 100px; 119 | } 120 | 121 | 122 | .h200{ 123 | height: 200px; 124 | } 125 | 126 | .hardWrap{ 127 | word-wrap: break-word; 128 | } -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EpicWorship 7 | 8 | 9 | 10 | 19 | 20 | 21 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /i18n/languages/en.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | menu_productions: "Productions", 3 | menu_elements: "Elements", 4 | menu_live: "Live", 5 | menu_settings: "Settings", 6 | menu_songs: "Songs", 7 | menu_scripture: "Scripture", 8 | menu_media: "Media", 9 | menu_backgrounds: "Backgrounds", 10 | menu_announcements: "Announcements", 11 | menu_presentations: "Presentations", 12 | menu_projectors: "Projectors", 13 | menu_countdown: "Countdown", 14 | menu_bibleVersions: "Bible Versions", 15 | menu_migrations: "Migrations", 16 | menu_language: "Language", 17 | menu_nowLive: "Now Live", 18 | 19 | news_goTo: "Go to", 20 | news_toGetStarted: "to get started", 21 | news_comingSoon: "News and Tutorials coming soon", 22 | 23 | newProduction: "New Production", 24 | clearSearch: "Clear Search", 25 | search: "Search", 26 | 27 | production_tooltip_create: "Create a new Production", 28 | production_tooltip_delete: "Delete this Production", 29 | production_tooltip_clone: "Clone this Production", 30 | production_tooltip_makeLive: "Make this Production LIVE", 31 | 32 | production_page_instructions: "Create (+) a Production, or Select a production and Click on the Star icon to make it live", 33 | production_page_noElements: "No elements were added to this production", 34 | production_page_noElementsGoAdd: "click to go add some!", 35 | 36 | production_errorTitle_productionName: "Invalid production name", 37 | production_errorMessage_productionName: "Every great production needs a good name", 38 | 39 | song_tooltip_create: "Create a new Song", 40 | song_tooltip_delete: "Delete this Song", 41 | song_tooltip_clone: "Clone this Song", 42 | 43 | song_page_instructions: "Create (+) a Song, or Select a song.", 44 | song_page_noElements: "No elements were added to this Song", 45 | song_page_noElementsGoAdd: "click to go add some!", 46 | 47 | song_errorTitle_productionName: "Invalid Song name", 48 | song_errorMessage_productionName: "Every great Song needs a good name", 49 | 50 | field_name: "Name", 51 | field_body: "Body", 52 | field_content: "Content", 53 | field_text: "text", 54 | 55 | new: "new", 56 | song: "song", 57 | video: "video", 58 | audio: "audio", 59 | media: "media", 60 | background: "background", 61 | bible: "bible", 62 | staticBackground: "Static Background", 63 | motionBackground: "Motion Background", 64 | 65 | toProduction: 'Add to Production', 66 | element_tooltip_delete_prodItem: 'Delete selected production item', 67 | 68 | tooltip_create: "Create a new {{elementType}}", 69 | tooltip_delete: "Delete this {{elementType}}", 70 | tooltip_clone: "Clone this {{elementType}}", 71 | 72 | media_page_instructions: "Select Media to preview, or Click '+' to import a file", 73 | background_page_instructions: "Select Background to preview, or Click '+' to import a file", 74 | 75 | }; -------------------------------------------------------------------------------- /i18n/i18n.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Fragment } from "react"; 3 | 4 | const pluralize = require("pluralize"); 5 | const capitalize = require("capitalize"); 6 | 7 | 8 | export let activeLanguage = 'en'; 9 | 10 | 11 | String.prototype.replaceAll = function(search, replacement) { 12 | var target = this; 13 | return target.replace(new RegExp(search, "g"), replacement); 14 | }; 15 | 16 | export let dict = {}; 17 | const reservedParams = [ 18 | "pluralize", 19 | "uppercase", 20 | "capitalize", 21 | "lowercase" 22 | ]; 23 | 24 | export const loadLanguage = (newLanguage) => { 25 | let locale = newLanguage; 26 | 27 | if (!locale) { 28 | locale = navigator.language || 'en'; 29 | console.log("Switching to Language:", locale); 30 | } 31 | 32 | //We only use the first 2 characters 33 | locale = locale.slice(0,2); 34 | 35 | let newDict = {} 36 | try { 37 | newDict = require("./languages/" + locale); 38 | }catch(err){ 39 | console.error(`Language files for ${locale} not found. Using english instead`); 40 | loadLanguage('en'); 41 | return; 42 | } 43 | 44 | 45 | //check to make sure no param variables uses a reserved param 46 | for (let [k, v] of Object.entries(newDict)) { 47 | for (let reserved of reservedParams) { 48 | if (v.includes("{{" + reserved + "}}")) { 49 | const errorMsg = `Could not load language ${activeLanguage}: Content for '${k}' is using a reserved keyword '${reserved}'`; 50 | console.error(errorMsg); 51 | throw new Error(errorMsg); 52 | } 53 | } 54 | } 55 | 56 | dict = newDict; 57 | activeLanguage = locale; 58 | }; 59 | 60 | loadLanguage(); 61 | 62 | export type TranslationParams={ 63 | pluralize: boolean, 64 | uppercase: boolean, 65 | lowercase: boolean, 66 | capitalize: boolean, 67 | } 68 | 69 | /** 70 | * translate some text by key name 71 | * @param name 72 | * @param params - opts. undefine this to skip dynamic translation features (static is faster) 73 | * @returns {*} 74 | */ 75 | export const t = (name, params:TranslationParams) => { 76 | let text = dict[name] || ""; 77 | 78 | 79 | if (params) { 80 | 81 | //replace variables? 82 | for (let [k, v] of Object.entries(params)) { 83 | text = text.replaceAll(`{{${k}}}`, v); 84 | } 85 | 86 | //pluralize? 87 | if (params.pluralize) text = pluralize(text); 88 | 89 | //uppercase? 90 | if (params.uppercase) text = text.toUpperCase(); 91 | if (params.lowercase) text = text.toLowerCase(); 92 | 93 | //capitalize? 94 | if (params.capitalize) text = capitalize(text); 95 | } 96 | 97 | return text; 98 | }; 99 | export const translate = t; 100 | 101 | dict._ = t; 102 | 103 | 104 | export const T = (props) => 105 | {t(props.name, props.params)} 106 | ; -------------------------------------------------------------------------------- /test/e2e/HomePage.e2e.js: -------------------------------------------------------------------------------- 1 | import { ClientFunction, Selector } from 'testcafe'; 2 | import { ReactSelector, waitForReact } from 'testcafe-react-selectors'; 3 | import { getPageUrl } from './helpers'; 4 | 5 | const getPageTitle = ClientFunction(() => document.title); 6 | const counterSelector = Selector('[data-tid="counter"]'); 7 | const buttonsSelector = Selector('[data-tclass="btn"]'); 8 | const clickToCounterLink = t => 9 | t.click(Selector('a').withExactText('to Counter')); 10 | const incrementButton = buttonsSelector.nth(0); 11 | const decrementButton = buttonsSelector.nth(1); 12 | const oddButton = buttonsSelector.nth(2); 13 | const asyncButton = buttonsSelector.nth(3); 14 | const getCounterText = () => counterSelector().innerText; 15 | const assertNoConsoleErrors = async t => { 16 | const { error } = await t.getBrowserConsoleMessages(); 17 | await t.expect(error).eql([]); 18 | }; 19 | 20 | fixture`Home Page`.page('../../app/app.html').afterEach(assertNoConsoleErrors); 21 | 22 | test('e2e', async t => { 23 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 24 | }); 25 | 26 | test('should open window', async t => { 27 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 28 | }); 29 | 30 | test( 31 | "should haven't any logs in console of main window", 32 | assertNoConsoleErrors 33 | ); 34 | 35 | test('should to Counter with click "to Counter" link', async t => { 36 | await t 37 | .click('[data-tid=container] > a') 38 | .expect(getCounterText()) 39 | .eql('0'); 40 | }); 41 | 42 | test('should navgiate to /counter', async t => { 43 | await waitForReact(); 44 | await t 45 | .click( 46 | ReactSelector('Link').withProps({ 47 | to: '/counter' 48 | }) 49 | ) 50 | .expect(getPageUrl()) 51 | .contains('/counter'); 52 | }); 53 | 54 | fixture`Counter Tests` 55 | .page('../../app/app.html') 56 | .beforeEach(clickToCounterLink) 57 | .afterEach(assertNoConsoleErrors); 58 | 59 | test('should display updated count after increment button click', async t => { 60 | await t 61 | .click(incrementButton) 62 | .expect(getCounterText()) 63 | .eql('1'); 64 | }); 65 | 66 | test('should display updated count after descrement button click', async t => { 67 | await t 68 | .click(decrementButton) 69 | .expect(getCounterText()) 70 | .eql('-1'); 71 | }); 72 | 73 | test('should not change if even and if odd button clicked', async t => { 74 | await t 75 | .click(oddButton) 76 | .expect(getCounterText()) 77 | .eql('0'); 78 | }); 79 | 80 | test('should change if odd and if odd button clicked', async t => { 81 | await t 82 | .click(incrementButton) 83 | .click(oddButton) 84 | .expect(getCounterText()) 85 | .eql('2'); 86 | }); 87 | 88 | test('should change if async button clicked and a second later', async t => { 89 | await t 90 | .click(asyncButton) 91 | .expect(getCounterText()) 92 | .eql('0') 93 | .expect(getCounterText()) 94 | .eql('1'); 95 | }); 96 | 97 | test('should back to home if back button clicked', async t => { 98 | await t 99 | .click('[data-tid="backButton"] > a') 100 | .expect(Selector('[data-tid="container"]').visible) 101 | .ok(); 102 | }); 103 | -------------------------------------------------------------------------------- /app/pages/productions/ProductionPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import { dict } from "../../../i18n/i18n"; 4 | import ProductionPageComponent from "./ProductionPageComponent"; 5 | import type { TSideBarButton } from "../../components/ItemList"; 6 | import ItemList from "../../components/ItemList"; 7 | 8 | @inject("store") 9 | @observer 10 | export default class ProductionPage extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | } 15 | 16 | onCreateProduction = () => { 17 | this.props.store.navigateToProduction("new"); 18 | }; 19 | 20 | selectProduction = (prod) => { 21 | this.props.store.navigateToProduction(prod._id); 22 | }; 23 | 24 | componentWillReceiveProps(nextProps: Readonly

, nextContext: any): void { 25 | 26 | this.props.store.productionStore.setLastSelectedProduction(nextProps.match.params.id); 27 | } 28 | 29 | getSelectedId() { 30 | const prodStore = this.props.store.productionStore; 31 | return this.props.match.params.id || prodStore.lastSelectedProductionId; 32 | } 33 | 34 | componentDidCatch(error) { 35 | toast.error({ title: "Oops", message: error.message }); 36 | } 37 | 38 | onClone = async () => { 39 | const prodStore = this.props.store.productionStore; 40 | const selectedProdId = this.getSelectedId(); 41 | 42 | await prodStore.cloneProduction(selectedProdId); 43 | }; 44 | 45 | onDelete = async () => { 46 | const prodStore = this.props.store.productionStore; 47 | const selectedProdId = this.getSelectedId(); 48 | 49 | await prodStore.deleteProduction(selectedProdId); 50 | }; 51 | 52 | onMakeLive = async (itemId) => { 53 | const prodStore = this.props.store.productionStore; 54 | console.log('Making production live',itemId) 55 | await prodStore.makeProductionLive(itemId); 56 | }; 57 | 58 | buttons: array = [ 59 | { icon: "plus", tooltip: dict.production_tooltip_create, handler: this.onCreateProduction }, 60 | { icon: "copy", tooltip: dict.production_tooltip_clone, handler: this.onClone, showOnlyIfSelected: true }, 61 | { icon: "trash", tooltip: dict.production_tooltip_delete, handler: this.onDelete, showOnlyIfSelected: true }, 62 | { icon: "star", tooltip: dict.production_tooltip_makeLive, handler: this.onMakeLive, showOnlyIfSelected: true } 63 | ]; 64 | 65 | render() { 66 | const prodStore = this.props.store.productionStore; 67 | const liveProductionId = prodStore.liveProductionId; 68 | const selectedProdId = this.getSelectedId(); 69 | 70 | return ( 71 |

72 |

{dict.menu_productions}

73 | 74 |
75 | 76 | this.selectProduction(item)} 85 | onItemDoubleClick={(item)=> this.onMakeLive(item._id)} 86 | /> 87 | 88 |
89 | 90 |
91 |
92 | 93 |
94 | 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/layout/DashboardLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link, NavLink, Route, Switch } from "react-router-dom"; 3 | import { inject, observer } from "mobx-react"; 4 | import ProductionPage from "../pages/productions/ProductionPage"; 5 | import ElementsLayout from "./ElementsLayout"; 6 | import LivePage from "../pages/live/LivePage"; 7 | import SettingsLayout from "./SettingsLayout"; 8 | import NewsPage from "../pages/NewsPage"; 9 | import { T } from "../../i18n/i18n"; 10 | 11 | 12 | @inject("store") 13 | @observer 14 | export default class DashboardLayout extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = { error: null }; 19 | 20 | //IMPORTANT: allows for store-based navigation 21 | props.store.setHistory(this.props.history); 22 | } 23 | 24 | render() { 25 | 26 | const nav = ( 27 | 73 | ); 74 | 75 | const body = ( 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
87 | ); 88 | 89 | return ( 90 |
91 | {nav} 92 | {this.state.error ?
Oops, something went wrong: {this.state.error.message}
: body} 93 |
94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/pages/elements/songs/SongsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import { dict } from "../../../../i18n/i18n"; 4 | import SongsPageComponent from "./SongsPageComponent"; 5 | import type { TSideBarButton } from "../../../components/ItemList"; 6 | import ItemList from "../../../components/ItemList"; 7 | import { elementTypes } from "../../../utils/data"; 8 | 9 | 10 | const elementType = elementTypes.SONG; 11 | 12 | 13 | @inject("store") 14 | @observer 15 | export default class SongsPage extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | } 20 | 21 | onCreate = () => { 22 | this.props.store.navigateToElement(elementType, "new"); 23 | }; 24 | 25 | onItemClick = (item) => { 26 | this.props.store.navigateToElement(elementType, item._id); 27 | }; 28 | onItemDoubleClick = (item) => { 29 | this.props.store.navigateToElement(elementType, item._id); 30 | this.props.store.productionStore.addToLiveProduction(item); 31 | }; 32 | 33 | 34 | componentWillReceiveProps(nextProps: Readonly

, nextContext: any): void { 35 | 36 | // this.props.store.elementStore.setLastSelectedProduction(nextProps.match.params.id); 37 | } 38 | 39 | getSelectedId() { 40 | return this.props.match.params.id; 41 | } 42 | 43 | componentDidCatch(error) { 44 | toast.error({ title: "Oops", message: error.message }); 45 | } 46 | 47 | onClone = async () => { 48 | const store = this.props.store.elementStore; 49 | const selectedElementId = this.getSelectedId(); 50 | 51 | await store.cloneElement(elementType, selectedElementId); 52 | }; 53 | 54 | onDelete = async () => { 55 | const store = this.props.store.elementStore; 56 | const selectedElementId = this.getSelectedId(); 57 | 58 | await store.deleteElement(elementType, selectedElementId); 59 | }; 60 | 61 | onAddToProduction = async () => { 62 | const store = this.props.store.productionStore; 63 | const selectedElementId = this.getSelectedId(); 64 | 65 | const elemStore = this.props.store.elementStore; 66 | const element = await elemStore.findElement(selectedElementId); 67 | await store.addToLiveProduction(element); 68 | }; 69 | 70 | 71 | buttons: array = [ 72 | { icon: "plus", tooltip: dict.song_tooltip_create, handler: this.onCreate }, 73 | { icon: "copy", tooltip: dict.song_tooltip_clone, handler: this.onClone, showOnlyIfSelected: true }, 74 | { icon: "trash", tooltip: dict.song_tooltip_delete, handler: this.onDelete, showOnlyIfSelected: true }, 75 | { icon: "chevron-right", tooltip: dict.toProduction, handler: this.onAddToProduction, showOnlyIfSelected: true } 76 | ]; 77 | 78 | render() { 79 | const elementStore = this.props.store.elementStore; 80 | const selectedElementId = this.getSelectedId(); 81 | // debugger; 82 | return ( 83 |

84 |

{dict.menu_songs}

85 | 86 |
87 | 96 | 97 |
98 | 99 |
100 |
101 |
102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/pages/productions/ProductionPageComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import { dict } from "../../../i18n/i18n"; 4 | import { Link } from "react-router-dom"; 5 | import ItemList from "../../components/ItemList"; 6 | 7 | 8 | type Props = { 9 | selectedId?: string 10 | } 11 | 12 | @inject("store") 13 | @observer 14 | export default class ProductionPageComponent extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | name: "" 21 | }; 22 | } 23 | 24 | onSubmit = async (evt) => { 25 | evt.preventDefault(); 26 | 27 | if (!this.state.name) return toast.error({ 28 | title: "Invalid Production name", 29 | message: "Every production needs a good name" 30 | }); 31 | 32 | const prodStore = this.props.store.productionStore; 33 | const prodId = this.props.selectedId; 34 | let production = prodStore.findProductionById(prodId); 35 | 36 | if (!production) { 37 | production = await prodStore.createProduction(this.state.name); 38 | } else { 39 | production.name = this.state.name; 40 | await prodStore.updateProduction(production); 41 | } 42 | // debugger; 43 | this.setState({ name: "" }); 44 | this.props.store.navigateToProduction(production._id); 45 | 46 | }; 47 | 48 | render() { 49 | const prodStore = this.props.store.productionStore; 50 | const prodId = this.props.selectedId || prodStore.lastSelectedProductionId; //if id='new' go all the way down and work with null production 51 | 52 | // If still no id then no current or previous selection 53 | if (!prodId) return
{dict.production_page_instructions}
; 54 | 55 | const production = prodStore.findProductionById(prodId); 56 | 57 | const isLive = prodId && prodStore.liveProductionId === prodId; 58 | const BlankLook = isLive ? 59 | ()=>

{dict.production_page_noElements}. {dict.production_page_noElementsGoAdd}

60 | : 61 | ()=>

{dict.production_page_noElements}

; 62 | 63 | return ( 64 |
65 |
66 |
67 | 68 | {production ? production.name : dict.newProduction} 69 | 70 |
71 | {dict.field_name} 72 | { 75 | this.setState({ name: evt.target.value }); 76 | }} 77 | value={this.state.name}/> 78 |
79 | 80 | {production &&
81 | {dict.menu_elements} 82 | {production.items && production.items.length > 0 ? this.onItemSelect(item)} 88 | 89 | /> 90 | : 91 | 92 | } 93 |
94 | } 95 |
96 | 97 | 98 |
99 |
100 |
101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/layout/ElementsLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import { NavLink, Route, Switch } from "react-router-dom"; 4 | import ScripturePage from "../pages/elements/ScripturePage"; 5 | import MediaPage from "../pages/elements/media/MediaPage"; 6 | import AnnouncementsPage from "../pages/elements/AnnouncementsPage"; 7 | import PresentationsPage from "../pages/elements/PresentationsPage"; 8 | import { dict, T } from "../../i18n/i18n"; 9 | import { Redirect } from "react-router"; 10 | import SongsPage from "../pages/elements/songs/SongsPage"; 11 | import ItemList from "../components/ItemList"; 12 | import type { TSideBarButton } from "../components/ItemList"; 13 | import BackgroundsPage from "../pages/elements/background/BackgroundsPage"; 14 | 15 | @inject("store") 16 | @observer 17 | export default class ElementsLayout extends Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | } 22 | 23 | onRemoveProductionItem = async (itemId) => { 24 | 25 | const prodStore = this.props.store.productionStore; 26 | await prodStore.removeFromLiveProduction(itemId); 27 | }; 28 | 29 | productionItemButtons: array = [ 30 | { 31 | icon: "trash", 32 | tooltip: dict.element_tooltip_delete_prodItem, 33 | handler: this.onRemoveProductionItem, 34 | showOnlyIfSelected: true 35 | } 36 | ]; 37 | 38 | render() { 39 | 40 | const liveProduction = this.props.store.productionStore.liveProduction; 41 | const navigateToElement = this.props.store.navigateToElement; 42 | 43 | // console.log({liveProduction}) 44 | 45 | return ( 46 |
47 | 48 |
    49 |
  • 50 |
  • 51 |
  • 52 |
  • 53 |
  • 54 | {/*
  • */} 56 | {/*
  • */} 58 | 59 |
60 | {/*Order of routes is important*/} 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {liveProduction && 86 |
87 |

Production Set

88 | navigateToElement(item.elementType, item.elementId)} 94 | stretch 95 | /> 96 |
97 | } 98 |
99 | 100 |
101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/pages/elements/songs/SongsPageComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import { dict } from "../../../../i18n/i18n"; 4 | import { Link } from "react-router-dom"; 5 | import { elementTypes } from "../../../utils/data"; 6 | 7 | const elementType = elementTypes.SONG; 8 | 9 | type Props = { 10 | selectedId?: string 11 | } 12 | 13 | @inject("store") 14 | @observer 15 | export default class SongsPageComponent extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | name: "", 22 | text: "" 23 | }; 24 | } 25 | 26 | componentDidMount(): void { 27 | this._refresh(this.props) 28 | } 29 | 30 | componentWillReceiveProps(nextProps: Readonly

, nextContext: any): void { 31 | if (this.props.selectedId !== nextProps.selectedId) { 32 | this._refresh(nextProps) 33 | } 34 | } 35 | 36 | _refresh=(props)=>{ 37 | const elementStore = props.store.elementStore; 38 | const element = elementStore.getElement(elementType, props.selectedId); 39 | 40 | // debugger; 41 | 42 | if (element) { 43 | // console.log(element); 44 | this.setState({ name: element.name, text: element.text }); 45 | } else if (props.selectedId === "new") { 46 | this.setState({ name: "", text: "" }); 47 | } 48 | } 49 | 50 | 51 | onSubmit = async (evt) => { 52 | evt.preventDefault(); 53 | 54 | if (!this.state.name) return toast.error({ 55 | title: `Empty ${elementType} name`, 56 | message: `Every ${elementType} needs a good name` 57 | }); 58 | if (!this.state.text) return toast.error({ 59 | title: `Empty ${elementType} body`, 60 | message: `Every ${elementType} needs a good body` 61 | }); 62 | 63 | const elementStore = this.props.store.elementStore; 64 | const elementId = this.props.selectedId; 65 | let element = elementStore.getElement(elementType, elementId); 66 | 67 | if (!element) { 68 | element = await elementStore.createElement(elementType, this.state.name, this.state.text); 69 | } else { 70 | element.name = this.state.name; 71 | element.text = this.state.text; 72 | await elementStore.updateElement(element); 73 | } 74 | // debugger; 75 | // this.setState({ name: "" }); 76 | this.props.store.navigateToElement(elementType, element._id); 77 | 78 | }; 79 | 80 | render() { 81 | const elementStore = this.props.store.elementStore; 82 | const elementId = this.props.selectedId; //if id='new' go all the way down and work with null element 83 | // debugger; 84 | // console.log({ elementId }); 85 | 86 | // If still no id then no current or previous selection 87 | if (!elementId) return

{dict.song_page_instructions}
; 88 | 89 | const element = elementStore.getElement(elementType, elementId); 90 | // debugger; 91 | 92 | return ( 93 |
94 |
95 |
96 | 97 | {element ? element.name : dict.song_tooltip_create} 98 | 99 |
100 | {dict.field_name} 101 | { 106 | this.setState({ name: evt.target.value }); 107 | }} 108 | value={this.state.name}/> 109 |
110 | 111 |
112 |
{dict.field_text}
113 |