├── .nvmrc ├── src ├── utils │ ├── getOS.js │ ├── convertOldPrompts.js │ ├── helpers.js │ ├── theme.js │ ├── fullScreen.js │ ├── getFontFamily.js │ ├── findIndex.js │ ├── apiEndpoints.js │ ├── fakeApi │ │ └── segments.js │ └── consts.js ├── store │ ├── actions │ │ ├── Test.js │ │ ├── misc.js │ │ ├── segments.js │ │ ├── actionTypes.js │ │ ├── text.js │ │ ├── authUser.js │ │ ├── user.js │ │ └── prompter.js │ ├── migrations │ │ └── ver2.js │ ├── reducers │ │ ├── Test.js │ │ ├── authUser.js │ │ ├── user.js │ │ ├── misc.js │ │ ├── segments.js │ │ ├── prompter.js │ │ └── text.js │ └── configureStore.js ├── components │ ├── common │ │ ├── index.js │ │ ├── Icon │ │ │ ├── Icon.module.scss │ │ │ └── index.js │ │ ├── Logo │ │ │ ├── Logo.module.scss │ │ │ └── index.js │ │ ├── Selector │ │ │ ├── Selector.module.scss │ │ │ └── index.js │ │ ├── Instruction │ │ │ ├── Instruction.module.scss │ │ │ └── index.js │ │ ├── Input │ │ │ ├── Input.module.scss │ │ │ └── index.js │ │ ├── SliderAlt │ │ │ ├── Slider.scss │ │ │ └── index.js │ │ ├── Break │ │ │ ├── index.js │ │ │ └── Break.module.scss │ │ ├── TextPreview │ │ │ ├── TextPreview.module.scss │ │ │ └── index.js │ │ ├── Checkbox │ │ │ └── index.js │ │ ├── Button │ │ │ ├── Button.module.scss │ │ │ └── index.js │ │ └── Modal │ │ │ ├── Modal.module.scss │ │ │ └── index.js │ ├── Main │ │ ├── Main.module.scss │ │ ├── rootContext.js │ │ └── index.js │ ├── TextEditor │ │ ├── TextEditor.module.scss │ │ └── index.js │ ├── Loader │ │ ├── Loader.module.scss │ │ └── index.js │ ├── AboutModal │ │ ├── AboutModal.module.scss │ │ └── index.js │ ├── ColorPicker │ │ ├── ColorPicker.module.scss │ │ └── index.jsx │ ├── ForgottenPasswordModal │ │ ├── ForgottenPasswordModal.module.scss │ │ └── index.js │ ├── Policy │ │ ├── PolicyModal │ │ │ └── index.js │ │ └── Policy.module.scss │ ├── Mobile │ │ ├── Mobile.module.scss │ │ └── index.js │ ├── Footer │ │ ├── Footer.module.scss │ │ └── index.js │ ├── HowToUseModal │ │ └── HowToUseModal.module.scss │ ├── TextScroller │ │ └── TextScroller.module.scss │ ├── EditorSidebar │ │ ├── Toggle.scss │ │ ├── EditorSidebar.module.scss │ │ └── index.js │ ├── Player │ │ ├── Player.module.scss │ │ ├── Header │ │ │ └── index.js │ │ └── index.js │ ├── ActionHeader │ │ └── ActionHeader.module.scss │ ├── ActionSidebar │ │ └── ActionSidebar.module.scss │ ├── Password │ │ ├── Password.module.scss │ │ └── index.js │ ├── Preview │ │ ├── index.js │ │ └── Preview.module.scss │ ├── Segment │ │ ├── Segment.module.scss │ │ └── index.js │ ├── UserSettingsModal │ │ └── UserSettingsModal.module.scss │ ├── About │ │ └── index.js │ ├── MobileController │ │ ├── index.js │ │ └── MobileContainer.module.scss │ └── Login │ │ └── Login.module.scss ├── index.css ├── styles │ ├── mixins │ │ ├── _keyframes.scss │ │ ├── heading.scss │ │ ├── label.scss │ │ ├── paragraph.scss │ │ └── _hideScrollbar.scss │ ├── _fonts.scss │ └── variables.scss ├── assets │ ├── controls │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── bg.svg │ │ ├── angle-up-1.svg │ │ ├── angle-up.svg │ │ ├── forward.svg │ │ ├── backward.svg │ │ └── Layer 2.svg │ ├── Layer 2.svg │ └── prompterme-logo-dark.svg ├── index.js ├── App │ └── index.js └── serviceWorker.js ├── .husky ├── .gitignore ├── pre-commit └── prepare-commit-msg ├── public ├── _redirects ├── favicons │ ├── favicon.png │ ├── favicon-128.png │ ├── favicon-152.png │ ├── favicon-167.png │ ├── favicon-180.png │ ├── favicon-192.png │ ├── favicon-196.png │ ├── favicon-32.png │ └── favicon-512.png ├── misc │ ├── prompterme-sharing.jpeg │ └── prompterme-sharing.png ├── manifest.json └── index.html ├── .czrc ├── config └── .env.prod ├── .netlify └── state.json ├── images ├── preview1.png └── prompterme-logo-dark.svg ├── jsconfig.json ├── .issue_label_bot.yaml ├── .gitignore ├── .github └── workflows │ ├── schedule-stale.yml │ ├── .release.yml │ └── codeql-analysis.yml ├── .eslintrc ├── .releaserc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── CODE_OF_CONDUCT.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.8.1 -------------------------------------------------------------------------------- /src/utils/getOS.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /src/store/actions/Test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged -------------------------------------------------------------------------------- /src/components/common/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/convertOldPrompts.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | exec < /dev/tty && npx --no-install cz --hook || true -------------------------------------------------------------------------------- /config/.env.prod: -------------------------------------------------------------------------------- 1 | REACT_APP_BACKEND_V2=https://prompter-server.herokuapp.com -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "3a04958c-0749-481b-8452-32e7f13693bc" 3 | } -------------------------------------------------------------------------------- /images/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/images/preview1.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /public/favicons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon.png -------------------------------------------------------------------------------- /.issue_label_bot.yaml: -------------------------------------------------------------------------------- 1 | label-alias: 2 | bug: 'bug' 3 | feature_request: 'enhancement' 4 | question: 'question' -------------------------------------------------------------------------------- /public/favicons/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-128.png -------------------------------------------------------------------------------- /public/favicons/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-152.png -------------------------------------------------------------------------------- /public/favicons/favicon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-167.png -------------------------------------------------------------------------------- /public/favicons/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-180.png -------------------------------------------------------------------------------- /public/favicons/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-192.png -------------------------------------------------------------------------------- /public/favicons/favicon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-196.png -------------------------------------------------------------------------------- /public/favicons/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-32.png -------------------------------------------------------------------------------- /public/favicons/favicon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/favicons/favicon-512.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #root { 7 | position: relative; 8 | } -------------------------------------------------------------------------------- /src/components/common/Icon/Icon.module.scss: -------------------------------------------------------------------------------- 1 | .iconContainer { 2 | &:hover { 3 | cursor: pointer; 4 | } 5 | } -------------------------------------------------------------------------------- /src/styles/mixins/_keyframes.scss: -------------------------------------------------------------------------------- 1 | @mixin keyframes($name) { 2 | @keyframes #{$name} { 3 | @content; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/misc/prompterme-sharing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/misc/prompterme-sharing.jpeg -------------------------------------------------------------------------------- /public/misc/prompterme-sharing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zilahir/teleprompter/HEAD/public/misc/prompterme-sharing.png -------------------------------------------------------------------------------- /src/styles/mixins/heading.scss: -------------------------------------------------------------------------------- 1 | @mixin heading($color: #fff) { 2 | font: 800 28px/34px 'Barlow'; 3 | letter-spacing: 0; 4 | } -------------------------------------------------------------------------------- /src/components/common/Logo/Logo.module.scss: -------------------------------------------------------------------------------- 1 | .logoContainer { 2 | width: 133px; 3 | margin-left: 20px; 4 | position: relative; 5 | } -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | import { isEqual } from 'lodash' 2 | 3 | export const shallowComparePrompterObjects = (a, b) => ( 4 | isEqual(a, b) 5 | ) 6 | -------------------------------------------------------------------------------- /src/components/Main/Main.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | 3 | .mainContainer { 4 | .heightFixer { 5 | height: calc(100vh - 44px); 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/Main/rootContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export default createContext({ 4 | textPreview: '', 5 | setTextPreview: () => {}, 6 | }) 7 | -------------------------------------------------------------------------------- /src/styles/mixins/label.scss: -------------------------------------------------------------------------------- 1 | @mixin labelText { 2 | font: 600 14px/17px 'Barlow'; 3 | letter-spacing: 0.7px; 4 | color: #FFFFFF; 5 | margin-bottom: 10px; 6 | } -------------------------------------------------------------------------------- /src/utils/theme.js: -------------------------------------------------------------------------------- 1 | import hexToRgba from 'hex-to-rgba' 2 | 3 | export const theme = { 4 | misc: { 5 | borderRadius: 5, 6 | }, 7 | colos: { 8 | purple: hexToRgba('#8380FF', 1), 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /src/store/migrations/ver2.js: -------------------------------------------------------------------------------- 1 | import { textState } from '../reducers/text' 2 | 3 | export const migrateStore = { 4 | 3: state => ({ 5 | ...state, 6 | text: { 7 | ...textState, 8 | }, 9 | }), 10 | } 11 | -------------------------------------------------------------------------------- /src/components/TextEditor/TextEditor.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .textEditorContainer { 5 | display: flex; 6 | flex-direction: column; 7 | flex: 1; 8 | } -------------------------------------------------------------------------------- /src/utils/fullScreen.js: -------------------------------------------------------------------------------- 1 | export function toggleFullScreen() { 2 | if (!document.fullscreenElement) { 3 | document.documentElement.requestFullscreen() 4 | } else if (document.exitFullscreen) { 5 | document.exitFullscreen() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/mixins/paragraph.scss: -------------------------------------------------------------------------------- 1 | @mixin paragraph($size:12px, $color: #fff, $letter-spacing:1.2px) { 2 | margin: 0; 3 | padding: 0; 4 | color: $color; 5 | font-family: 'Barlow'; 6 | letter-spacing: $letter-spacing; 7 | font-size: $size; 8 | } -------------------------------------------------------------------------------- /src/assets/controls/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.module.scss: -------------------------------------------------------------------------------- 1 | .loadingOverlay { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100vh; 6 | } 7 | 8 | .inlineLoader { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | margin: 10px 0; 13 | } -------------------------------------------------------------------------------- /src/assets/controls/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/getFontFamily.js: -------------------------------------------------------------------------------- 1 | import { MONO, SERIF } from './consts' 2 | 3 | export const getFontFamily = chosenFont => { 4 | let font = 'Barlow' 5 | 6 | if (chosenFont === MONO.toLowerCase()) { 7 | font = '\'Courier Prime\', monospace' 8 | } else if (chosenFont === SERIF.toLowerCase()) { 9 | font = '\'Crimson Pro\', serif' 10 | } 11 | return font 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/controls/bg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/reducers/Test.js: -------------------------------------------------------------------------------- 1 | import { TEST } from '../actions/actionTypes' 2 | 3 | const initialState = { 4 | testItem: 'test', 5 | } 6 | 7 | const reducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case TEST: 10 | return { 11 | ...state, 12 | testItem: action.payload.testItem, 13 | } 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | export default reducer 20 | -------------------------------------------------------------------------------- /src/assets/controls/angle-up-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env* -------------------------------------------------------------------------------- /src/assets/controls/angle-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/controls/forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/controls/backward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AboutModal/AboutModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .aboutModalWrapper { 4 | width: 100%; 5 | overflow: hidden; 6 | overflow-y: auto; 7 | height: 100%; 8 | } 9 | 10 | .aboutModalOverlay { 11 | animation: opacity 0.3s; 12 | position: absolute; 13 | top: 0; 14 | width: 100%; 15 | height: 100%; 16 | background: $overlay-bg-color; 17 | opacity: 1; 18 | z-index: 9; 19 | backdrop-filter: blur($modal-backdrop-blur-value / 2); 20 | left: 0; 21 | } -------------------------------------------------------------------------------- /src/styles/mixins/_hideScrollbar.scss: -------------------------------------------------------------------------------- 1 | @mixin hideScrollBar($selector) { 2 | .#{$selector} { 3 | scrollbar-width: thin; 4 | scrollbar-color: transparent transparent; 5 | 6 | &::-webkit-scrollbar { 7 | width: 1px; 8 | } 9 | 10 | &::-webkit-scrollbar-track { 11 | background: transparent; 12 | } 13 | 14 | &::-webkit-scrollbar-thumb { 15 | background-color: transparent; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/store/reducers/authUser.js: -------------------------------------------------------------------------------- 1 | import { AUTH_USER, REMOVE_USER } from '../actions/actionTypes' 2 | 3 | const initialState = { 4 | user: null, 5 | business: null, 6 | } 7 | 8 | const reducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case AUTH_USER: 11 | return { 12 | ...state, 13 | user: action.payload.user, 14 | } 15 | case REMOVE_USER: 16 | return { 17 | ...state, 18 | user: null, 19 | business: null, 20 | } 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | export default reducer 27 | -------------------------------------------------------------------------------- /src/store/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { AUTH_USER, REMOVE_USER } from '../actions/actionTypes' 2 | 3 | const initialState = { 4 | user: null, 5 | loggedIn: null, 6 | } 7 | 8 | const reducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case AUTH_USER: 11 | return { 12 | ...state, 13 | user: action.payload.user, 14 | loggedIn: action.payload.user.isSuccess, 15 | } 16 | case REMOVE_USER: 17 | return { 18 | ...state, 19 | user: null, 20 | loggedIn: false, 21 | } 22 | default: 23 | return state 24 | } 25 | } 26 | 27 | export default reducer 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { PersistGate } from 'redux-persist/integration/react' 5 | 6 | import App from './App' 7 | import './index.css' 8 | import * as serviceWorker from './serviceWorker' 9 | import { store, persistor } from './store/configureStore' 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root'), 18 | ) 19 | 20 | serviceWorker.unregister() 21 | -------------------------------------------------------------------------------- /src/components/ColorPicker/ColorPicker.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | 3 | .overlay { 4 | position: absolute; 5 | width: 100%; 6 | height: 100%; 7 | left: 0; 8 | top: 0; 9 | } 10 | 11 | .colorPickerContainer { 12 | display: block; 13 | position: absolute; 14 | top: -50px; 15 | right: -50px; 16 | padding: 20px; 17 | background: rgba($color: $gray-2, $alpha: 1); 18 | border-radius: $border-radius; 19 | box-shadow: 3px 3px 20px rgba($color: #000000, $alpha: 0.5); 20 | z-index: 99; 21 | 22 | &.hidden { 23 | visibility: hidden; 24 | display: none; 25 | } 26 | 27 | &.visible { 28 | display: block; 29 | } 30 | } -------------------------------------------------------------------------------- /src/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | /* UI FONTS */ 2 | 3 | @import url('https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&family=Crimson+Pro:wght@200;300;400;500;600;700;800&display=swap'); 4 | @import url('https://fonts.googleapis.com/css?family=Barlow:400,500,600,700,800&display=swap'); 5 | 6 | /* PROMPTER FONTS */ 7 | 8 | @import url('https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap'); 9 | 10 | /* font-family: 'Courier Prime', monospace; */ 11 | 12 | @import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;700&display=swap'); 13 | 14 | /* font-family: 'Crimson Pro', serif; */ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "prompter.me", 3 | "name": "Prompter.me", 4 | "icons": [ 5 | { 6 | "src": "./favicons/favicon-512.png", 7 | "type": "image/png", 8 | "sizes": "512x512" 9 | }, 10 | { 11 | "src": "./favicons/favicon-192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "./favicons/favicon-196.png", 17 | "type": "image/png", 18 | "sizes": "196x196" 19 | }, 20 | { 21 | "src": "./favicons/favicon-32.png", 22 | "sizes": "64x64 32x32 24x24 16x16", 23 | "type": "image/x-icon" 24 | } 25 | ], 26 | "start_url": "./", 27 | "display": "standalone", 28 | "theme_color": "#3f51b5", 29 | "background_color": "#303030" 30 | } 31 | -------------------------------------------------------------------------------- /src/components/common/Selector/Selector.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables'; 2 | @import '../../../styles/mixins/paragraph'; 3 | 4 | .selectorContainer { 5 | display: flex; 6 | width: 185px; 7 | .label { 8 | @include paragraph(13px, #ffffff, 0px); 9 | text-align: center; 10 | text-transform: capitalize; 11 | } 12 | .selectorItem { 13 | padding: 3px; 14 | margin-right: 3px; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | width: 58px; 19 | height: 36px; 20 | &:hover { 21 | cursor: pointer; 22 | transition: all .4s ease; 23 | background-color: rgba($color: $purple, $alpha: 0.2); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/common/Instruction/Instruction.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables'; 2 | @import '../../../styles/mixins/paragraph.scss'; 3 | 4 | .instructionContainer { 5 | margin: 0 50px; 6 | padding: 20px; 7 | padding-right: 0; 8 | margin-right: 20px; 9 | 10 | p { 11 | @include paragraph(14px, $gray-4); 12 | letter-spacing: 0.42px; 13 | } 14 | 15 | span { 16 | margin-left: 4px; 17 | color: #ffffff; 18 | &:hover { 19 | color: $purple; 20 | text-decoration: underline; 21 | cursor: pointer; 22 | } 23 | 24 | &:focus { 25 | outline: none; 26 | } 27 | } 28 | 29 | &.noPadding { 30 | padding: 0; 31 | margin-left: 0; 32 | } 33 | } -------------------------------------------------------------------------------- /src/store/actions/misc.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE_UPDATE_BTN, HIDE_INSTRUCTION, SET_COLOR_SCHEME } from './actionTypes' 2 | 3 | export const toggleUpdateBtn = boolean => dispatch => new Promise(resolve => { 4 | dispatch({ 5 | type: TOGGLE_UPDATE_BTN, 6 | payload: { 7 | boolean, 8 | }, 9 | }) 10 | resolve(true) 11 | }) 12 | 13 | export const hideInstruction = (whichInstruction, boolean) => dispatch => new Promise(resolve => { 14 | dispatch({ 15 | type: HIDE_INSTRUCTION, 16 | payload: { 17 | whichInstruction, 18 | boolean, 19 | }, 20 | }) 21 | resolve({ 22 | success: true, 23 | }) 24 | }) 25 | 26 | export const setColorScheme = chosenColorScheme => dispatch => { 27 | dispatch({ 28 | type: SET_COLOR_SCHEME, 29 | payload: { 30 | chosenColorScheme, 31 | }, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/Layer 2.svg: -------------------------------------------------------------------------------- 1 | prompter.me -------------------------------------------------------------------------------- /src/assets/controls/Layer 2.svg: -------------------------------------------------------------------------------- 1 | prompter.me -------------------------------------------------------------------------------- /src/components/common/Icon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | 5 | import styles from './Icon.module.scss' 6 | 7 | /** 8 | * @author zilahir 9 | * @function icon 10 | * */ 11 | 12 | const IconWrapper = styled.div` 13 | color: ${props => props.color}; 14 | ` 15 | 16 | const Icon = props => { 17 | const { icon, onClick, color } = props 18 | return ( 19 | 24 | {icon} 25 | 26 | ) 27 | } 28 | 29 | Icon.defaultProps = { 30 | color: '#fff', 31 | onClick: null, 32 | } 33 | 34 | Icon.propTypes = { 35 | color: PropTypes.string, 36 | icon: PropTypes.node.isRequired, 37 | onClick: PropTypes.func, 38 | } 39 | 40 | export default Icon 41 | -------------------------------------------------------------------------------- /.github/workflows/schedule-stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PR' 2 | on: 3 | schedule: 4 | - cron: '0 */12 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue is stale because it has been open 10 days with no activity. Remove stale label or comment or this will be closed in 30 days.' 14 | stale-pr-message: 'This PR is stale because it has been open 50 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 15 | close-issue-message: 'This issue was closed because it has been stalled for 30 days with no activity.' 16 | days-before-stale: 10 17 | days-before-close: 30 18 | days-before-pr-close: -1 -------------------------------------------------------------------------------- /src/components/ForgottenPasswordModal/ForgottenPasswordModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | @import "../../styles/mixins/paragraph"; 3 | 4 | .forgottenPasswordModal { 5 | width: 500px; 6 | padding: 20px; 7 | p { 8 | @include paragraph(14px, #ffffff, 0.42); 9 | } 10 | 11 | .inputContainer { 12 | display: flex; 13 | justify-content: center; 14 | 15 | .input { 16 | height: 45px; 17 | margin-top: 20px; 18 | margin-bottom: 40px; 19 | } 20 | } 21 | 22 | .btnContainer { 23 | display: flex; 24 | justify-content: space-between; 25 | 26 | button { 27 | height: 45px; 28 | } 29 | } 30 | } 31 | 32 | .emailSentContainer { 33 | display: flex; 34 | justify-content: center; 35 | flex-direction: column; 36 | align-items: center; 37 | p { 38 | margin: 20px 0; 39 | } 40 | } -------------------------------------------------------------------------------- /src/components/Policy/PolicyModal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Modal from '../../common/Modal' 5 | import styles from '../../AboutModal/AboutModal.module.scss' 6 | import Policy from '..' 7 | 8 | const PolicyModal = ({ 9 | isVisible, 10 | handleClose, 11 | selector, 12 | }) => ( 13 | <> 14 | 22 | 25 | 26 | 27 | ) 28 | 29 | PolicyModal.propTypes = { 30 | handleClose: PropTypes.func.isRequired, 31 | isVisible: PropTypes.bool.isRequired, 32 | selector: PropTypes.string.isRequired, 33 | } 34 | 35 | export default PolicyModal 36 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | /* FONTS */ 2 | 3 | @import './fonts'; 4 | 5 | /* COLORS */ 6 | 7 | $purple: #8380FF; 8 | $orange: #F4A836; 9 | $green: #09b813; 10 | $red: #f26457; 11 | $gray-1: #1E1E1E; 12 | $gray-2: #2D2D2D; 13 | $gray-3: #3A3A3A; 14 | $gray-4: #C1C1C1; 15 | $gray-5: #2E2E2E; 16 | $gray-6: #444444; 17 | $gray-7: #4d4d4d; 18 | 19 | $warning: $orange; 20 | $success: $green; 21 | $error: $red; 22 | 23 | /* BREAKPOINTS */ 24 | 25 | $screen-sm-min: 576px; 26 | $screen-md-min: 768px; 27 | $screen-lg-min: 992px; 28 | $screen-xl-min: 1200px; 29 | 30 | /* VARIABLES */ 31 | 32 | $border-radius: 5px; 33 | $login-box-border-radius: $border-radius; 34 | $login-box-bg-color: $gray-2; 35 | $modal-drop-shadow-color: #000000; 36 | $modal-border-radius: $border-radius; 37 | $modal-backdrop-blur-value: 5px; 38 | $modal-background-color: $gray-2; 39 | $overlay-bg-color: $gray-1; 40 | -------------------------------------------------------------------------------- /src/components/Mobile/Mobile.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .mobileContainer { 4 | background: rgba($color: $gray-3, $alpha: 1); 5 | 6 | .mobileLogo { 7 | padding-bottom: 60px; 8 | } 9 | 10 | display: flex; 11 | justify-content: center; 12 | height: 100vh; 13 | flex-direction: column; 14 | align-items: center; 15 | overflow: hidden; 16 | .innerContainer { 17 | display: flex; 18 | justify-content: center; 19 | position: relative; 20 | 21 | span { 22 | text-align: center; 23 | } 24 | 25 | .goBtn { 26 | width: 85px; 27 | height: 45px; 28 | position: absolute; 29 | bottom: 0; 30 | right: 0; 31 | button { 32 | width: inherit; 33 | height: inherit; 34 | min-width: unset; 35 | border-top-right-radius: 5px; 36 | border-bottom-right-radius: 5px; 37 | } 38 | } 39 | 40 | .input { 41 | margin: 0; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/components/AboutModal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import About from '../About' 5 | import Modal from '../common/Modal' 6 | import styles from './AboutModal.module.scss' 7 | 8 | /** 9 | * @author zilahir 10 | * @function AboutModal 11 | * */ 12 | 13 | const AboutModal = ({ 14 | isVisible, 15 | handleClose, 16 | selector, 17 | }) => ( 18 | <> 19 | 27 | 28 | 29 | 30 | ) 31 | 32 | AboutModal.propTypes = { 33 | handleClose: PropTypes.func.isRequired, 34 | isVisible: PropTypes.bool.isRequired, 35 | selector: PropTypes.string.isRequired, 36 | } 37 | 38 | export default AboutModal 39 | -------------------------------------------------------------------------------- /src/store/reducers/misc.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE_UPDATE_BTN, HIDE_INSTRUCTION, SET_COLOR_SCHEME } from '../actions/actionTypes' 2 | import { DARK_THEME } from '../../utils/consts' 3 | 4 | const initialState = { 5 | showActiveBtn: false, 6 | instructions: { 7 | INFOBOX_TOP: true, 8 | }, 9 | chosenColorScheme: DARK_THEME, 10 | } 11 | 12 | const reducer = (state = initialState, action) => { 13 | switch (action.type) { 14 | case TOGGLE_UPDATE_BTN: 15 | return { 16 | ...state, 17 | showActiveBtn: action.payload.boolean, 18 | } 19 | case HIDE_INSTRUCTION: 20 | return { 21 | ...state, 22 | instructions: { 23 | ...state.instructions, 24 | [action.payload.whichInstruction]: action.payload.boolean, 25 | }, 26 | } 27 | case SET_COLOR_SCHEME: 28 | return { 29 | ...state, 30 | chosenColorScheme: action.payload.chosenColorScheme, 31 | } 32 | default: 33 | return state 34 | } 35 | } 36 | 37 | export default reducer 38 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | @import "../../styles/mixins/paragraph.scss"; 3 | 4 | .footerContainer { 5 | background-color: rgba($color: $gray-1, $alpha: 1.0); 6 | padding: 10px 20px; 7 | position: absolute; 8 | bottom: 0; 9 | width: calc(100% - 40px); 10 | z-index: 9; 11 | overflow: hidden; 12 | 13 | .innerContainer { 14 | display: flex; 15 | align-items: center; 16 | p { 17 | @include paragraph(14px, #ffffff, 0.42px); 18 | 19 | &.purple { 20 | a { 21 | @include paragraph(14px, $purple, 0.42px); 22 | text-decoration: none; 23 | } 24 | margin-left: 20px; 25 | } 26 | } 27 | } 28 | 29 | ul { 30 | margin: 0; 31 | padding: 0; 32 | list-style-type: none; 33 | display: flex; 34 | justify-content: flex-end; 35 | li { 36 | a { 37 | color: unset; 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/store/actions/segments.js: -------------------------------------------------------------------------------- 1 | import { GET_ALL_SEGMENTS, ADD_SEGGMENT, CLEAR_ALL_SEGMENTS, MODIFY_SEGMENT } from './actionTypes' 2 | 3 | export const setSegments = segments => dispatch => new Promise(resolve => { 4 | dispatch({ 5 | type: GET_ALL_SEGMENTS, 6 | payload: { 7 | segments, 8 | }, 9 | }) 10 | resolve(segments) 11 | }) 12 | 13 | export const clearSegments = segments => dispatch => new Promise(resolve => { 14 | dispatch({ 15 | type: CLEAR_ALL_SEGMENTS, 16 | payload: { 17 | segments: [], 18 | }, 19 | }) 20 | resolve(segments) 21 | }) 22 | 23 | export const modifySegment = segmentObject => dispatch => { 24 | dispatch({ 25 | type: MODIFY_SEGMENT, 26 | payload: { 27 | id: segmentObject.id, 28 | segmentObject, 29 | }, 30 | }) 31 | } 32 | 33 | export const addSegment = segmentObject => dispatch => { 34 | dispatch({ 35 | type: ADD_SEGGMENT, 36 | payload: { 37 | segment: segmentObject, 38 | }, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/findIndex.js: -------------------------------------------------------------------------------- 1 | import { clamp, distance } from '@popmotion/popcorn' 2 | 3 | const buffer = 5 4 | 5 | export const findIndex = ( 6 | i, 7 | yOffset, 8 | positions, 9 | ) => { 10 | let target = i 11 | const { top, height } = positions[i] 12 | const bottom = top + height 13 | 14 | // If moving down 15 | if (yOffset > 0) { 16 | const nextItem = positions[i + 1] 17 | if (nextItem === undefined) return i 18 | 19 | const swapOffset = distance(bottom, nextItem.top + nextItem.height / 2) + buffer 20 | if (yOffset > swapOffset) target = i + 1 21 | 22 | // If moving up 23 | } else if (yOffset < 0) { 24 | const prevItem = positions[i - 1] 25 | if (prevItem === undefined) return i 26 | 27 | const prevBottom = prevItem.top + prevItem.height 28 | const swapOffset = distance(top, prevBottom - prevItem.height / 2) + buffer 29 | if (yOffset < -swapOffset) target = i - 1 30 | } 31 | 32 | return clamp(0, positions.length, target) 33 | } 34 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "@zilahir/eslint-config/react", 4 | "parser": "babel-eslint", 5 | "env": { 6 | "browser": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "no-console": ["error", { "allow": ["debug", "error"] }], 11 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 12 | "semi": [2, "never"], 13 | "no-bitwise": ["error", { "allow": ["~"] }], 14 | "react/forbid-prop-types": 2, 15 | "react/jsx-wrap-multilines": 2, 16 | "arrow-parens": [1, "as-needed"], 17 | "jsx-a11y/no-static-element-interactions": [ 18 | "error", 19 | { 20 | "handlers": ["onClick"] 21 | } 22 | ] 23 | }, 24 | "parserOptions": { 25 | "ecmaVersion": 6, 26 | "sourceType": "module", 27 | "ecmaFeatures": { 28 | "jsx": true, 29 | "modules": true, 30 | "experimentalObjectRestSpread": true 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/store/reducers/segments.js: -------------------------------------------------------------------------------- 1 | import { GET_ALL_SEGMENTS, ADD_SEGGMENT, CLEAR_ALL_SEGMENTS, MODIFY_SEGMENT } from '../actions/actionTypes' 2 | 3 | const initialState = { 4 | segments: [], 5 | } 6 | 7 | const reducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case GET_ALL_SEGMENTS: 10 | return { 11 | ...state, 12 | segments: action.payload.segments, 13 | } 14 | case ADD_SEGGMENT: 15 | return { 16 | ...state, 17 | segments: state.segments.concat(action.payload.segment), 18 | } 19 | case CLEAR_ALL_SEGMENTS: 20 | return { 21 | ...state, 22 | segments: [], 23 | } 24 | case MODIFY_SEGMENT: { 25 | const modifiedArray = state.segments.map( 26 | currSegment => (( 27 | currSegment.id === action.payload.id) ? action.payload.segmentObject : currSegment 28 | ), 29 | ) 30 | return { 31 | ...state, 32 | segments: modifiedArray, 33 | } 34 | } 35 | default: 36 | return state 37 | } 38 | } 39 | 40 | export default reducer 41 | -------------------------------------------------------------------------------- /src/components/common/Input/Input.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables'; 2 | @import '../../../styles/mixins/paragraph'; 3 | 4 | .inputContainer { 5 | margin: 30px 0; 6 | 7 | .label { 8 | display: flex; 9 | flex-direction: column; 10 | @include paragraph(14px, #fff, 0.39px); 11 | 12 | .labelText { 13 | margin-bottom: 10px; 14 | display: flex; 15 | } 16 | 17 | div { 18 | margin-left: 10px; 19 | &:hover { 20 | cursor: pointer; 21 | } 22 | } 23 | .input { 24 | 25 | &:focus { 26 | outline: none; 27 | } 28 | 29 | border: none; 30 | border-radius: $border-radius; 31 | background: $gray-1; 32 | width: 220px; 33 | height: 45px; 34 | @include paragraph(14px, $gray-4, 0.39px); 35 | padding-left: 20px; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/components/Mobile/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | 4 | import Logo from '../common/Logo' 5 | import styles from './Mobile.module.scss' 6 | import Input from '../common/Input' 7 | import Button from '../common/Button' 8 | 9 | /** 10 | * @author zilahir 11 | * @function Mobile 12 | * */ 13 | 14 | const Mobile = () => { 15 | const history = useHistory() 16 | const [prompterSlug, setPrompterSlug] = useState(null) 17 | 18 | return ( 19 |
20 | 23 |
24 | setPrompterSlug(val)} 27 | inputClassName={styles.input} 28 | /> 29 |
35 |
36 | ) 37 | } 38 | 39 | export default Mobile 40 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master", 4 | { name: 'dev', prerelease: true }, 5 | { name: 'next', prerelease: true }, 6 | "next-major", 7 | ], 8 | "plugins": [ 9 | ["@semantic-release/commit-analyzer", { 10 | "preset": "conventionalcommits", 11 | "config": "conventional-changelog-conventionalcommits", 12 | "parserOpts": { 13 | "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"] 14 | } 15 | }], 16 | ["@semantic-release/release-notes-generator", { 17 | "preset": "conventionalcommits", 18 | "config": "conventional-changelog-conventionalcommits", 19 | "parserOpts": { 20 | "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"], 21 | }, 22 | "writerOpts": { 23 | "commitsSort": ["subject", "scope"], 24 | } 25 | }], 26 | ["@semantic-release/changelog", { 27 | "changelogFile": "./CHANGELOG.md" 28 | }], 29 | ["@semantic-release/git", { 30 | "assets": ["package.json", "./src/**/*.{js, scss, css, ts, tsx, json}", "./CHANGELOG.md"] 31 | } 32 | ] 33 | ] 34 | } -------------------------------------------------------------------------------- /src/components/common/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import classnames from 'classnames' 4 | import PropTypes from 'prop-types' 5 | 6 | import teleprompterLogo from '../../../assets/prompterme-logo-light.svg' 7 | import { COLOR_LIGHT } from '../../../utils/consts' 8 | import styles from './Logo.module.scss' 9 | 10 | /** 11 | * @author 12 | * @function Logo 13 | * */ 14 | 15 | const LogoImage = styled.img` 16 | max-width: ${props => props.size}px; 17 | ` 18 | 19 | const Logo = props => { 20 | const { size, type, className } = props 21 | return ( 22 |
27 | 32 |
33 | ) 34 | } 35 | 36 | Logo.defaultProps = { 37 | className: null, 38 | size: 200, 39 | type: COLOR_LIGHT, 40 | } 41 | 42 | Logo.propTypes = { 43 | className: PropTypes.string, 44 | size: PropTypes.number, 45 | type: PropTypes.string, 46 | } 47 | 48 | export default Logo 49 | -------------------------------------------------------------------------------- /src/store/reducers/prompter.js: -------------------------------------------------------------------------------- 1 | import { GET_ALL_PROMPTER, SET_PROMPTER_SLUG, SET_PROJECT_NAME, CLEAR_ALL_PROMPTER, COPY_PROMPTER_OBJECT, CLEAR_PROMPTER_OBJECT } from '../actions/actionTypes' 2 | 3 | const initialState = { 4 | usersPrompters: [], 5 | prompterSlug: '', 6 | projectName: null, 7 | prompterObject: null, 8 | } 9 | 10 | const reducer = (state = initialState, action) => { 11 | switch (action.type) { 12 | case GET_ALL_PROMPTER: 13 | return { 14 | ...state, 15 | usersPrompters: action.payload.usersPrompters, 16 | } 17 | case SET_PROMPTER_SLUG: 18 | return { 19 | ...state, 20 | prompterSlug: action.payload.prompterSlug, 21 | } 22 | case SET_PROJECT_NAME: 23 | return { 24 | ...state, 25 | projectName: action.payload.projectName, 26 | } 27 | case CLEAR_ALL_PROMPTER: 28 | return { 29 | ...state, 30 | usersPrompters: [], 31 | } 32 | case COPY_PROMPTER_OBJECT: 33 | return { 34 | ...state, 35 | prompterObject: action.payload.prompterObject, 36 | } 37 | case CLEAR_PROMPTER_OBJECT: 38 | return { 39 | ...state, 40 | prompterObject: null, 41 | } 42 | default: 43 | return state 44 | } 45 | } 46 | 47 | export default reducer 48 | -------------------------------------------------------------------------------- /src/utils/apiEndpoints.js: -------------------------------------------------------------------------------- 1 | const apiRoot = process.env.NODE_ENV === 'development' ? 'http://localhost:5000' : process.env.REACT_APP_BACKEND_V2 2 | 3 | export const apiEndpoints = { 4 | authUser: `${apiRoot}/auth`, 5 | newUser: `${apiRoot}/users`, 6 | newPrompter: `${apiRoot}/prompter`, 7 | getAllPrompterForUser: `${apiRoot}/allprompterbyuserid`, 8 | getPrompterBySlug: `${apiRoot}/prompter`, 9 | delPrompter: `${apiRoot}/prompter`, 10 | modifyPrompter: `${apiRoot}/prompter`, 11 | newPrompterWithoutAuth: `${apiRoot}/prompternoauth`, 12 | updatePrompterNoAuth: `${apiRoot}/prompternoauth`, 13 | modifyPassword: `${apiRoot}/users`, 14 | modifyUserName: `${apiRoot}/users`, 15 | getPasswordRecovery: `${apiRoot}/passwordrecovery`, 16 | requestPasswordRecovery: `${apiRoot}/passwordrecovery`, 17 | setPasswordRecoveryToUsed: `${apiRoot}/passwordrecovery`, 18 | resetpassword: `${apiRoot}/resetpassword`, 19 | getToken: `${apiRoot}/auth/token`, 20 | sendPasswordRecoveryEmail: `${apiRoot}/email/password`, 21 | checkPassword: `${apiRoot}/auth/checkpassword`, 22 | deleteUser: `${apiRoot}/users`, 23 | getPrompterBySlugNoAuth: `${apiRoot}/prompternoauth`, 24 | } 25 | 26 | export const socketEndpoint = 'ws://localhost:5000' 27 | -------------------------------------------------------------------------------- /.github/workflows/.release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | semantic: 10 | name: "Creating Release" 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: bahmutov/npm-install@v1 15 | - run: npx semantic-release 16 | release: 17 | name: 'Deploy to Netlify' 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v2 22 | 23 | - name: Set up Node.js 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: '14' 27 | 28 | - name: NPM install 29 | uses: bahmutov/npm-install@v1 30 | 31 | - name: Build Application 32 | run: npm run build-production 33 | 34 | - name: Deploy production to Netlify 35 | uses: South-Paw/action-netlify-deploy@v1.2.0 36 | with: 37 | github-token: ${{ secrets.GITHUB_TOKEN }} 38 | netlify-auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }} 39 | netlify-site-id: ${{ secrets.NETLIFY_SITE_ID }} 40 | build-dir: './build' 41 | comment-on-commit: true 42 | draft: false -------------------------------------------------------------------------------- /src/components/HowToUseModal/HowToUseModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | @import "../../styles/mixins/paragraph"; 3 | 4 | .howtoUseModalWrapper { 5 | width: 100%; 6 | overflow: hidden; 7 | overflow-y: auto; 8 | height: 100%; 9 | } 10 | 11 | .aboutModalOverlay { 12 | animation: opacity 0.3s; 13 | position: absolute; 14 | top: 0; 15 | width: 100%; 16 | height: 100%; 17 | background: $overlay-bg-color; 18 | opacity: 1; 19 | z-index: 9; 20 | backdrop-filter: blur($modal-backdrop-blur-value / 2); 21 | left: 0; 22 | } 23 | 24 | .aboutWrapper { 25 | max-height: 100vh; 26 | 27 | h2 { 28 | @include paragraph(30px, #ffffff, 0); 29 | margin-top: 40px; 30 | margin-bottom: 10px; 31 | } 32 | 33 | h3 { 34 | @include paragraph(20px, $purple, 0); 35 | margin-top: 40px; 36 | margin-bottom: 20px; 37 | } 38 | 39 | ul { 40 | list-style-type: none; 41 | margin: 0; 42 | padding: 0; 43 | 44 | li { 45 | margin-bottom: 5px; 46 | display: flex; 47 | align-items: center; 48 | 49 | &:before { 50 | content: '–'; 51 | display: inline-block; 52 | text-indent: -2em; 53 | color: $purple; 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, compose, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import createMigrate from 'redux-persist/es/createMigrate' 4 | import { persistStore, persistReducer } from 'redux-persist' 5 | import storage from 'redux-persist/lib/storage' 6 | 7 | import segments from './reducers/segments' 8 | import text from './reducers/text' 9 | import user from './reducers/user' 10 | import userPrompters from './reducers/prompter' 11 | import misc from './reducers/misc' 12 | import { migrateStore } from './migrations/ver2' 13 | 14 | const persistConfig = { 15 | key: 'root', 16 | storage, 17 | version: 3, 18 | migrate: createMigrate(migrateStore, { debug: true }), 19 | } 20 | 21 | const rootReducer = combineReducers({ 22 | segments, 23 | text, 24 | user, 25 | userPrompters, 26 | misc, 27 | }) 28 | 29 | const persistedReducer = persistReducer(persistConfig, rootReducer) 30 | 31 | // eslint-disable-next-line no-underscore-dangle 32 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 33 | 34 | export const store = createStore(persistedReducer, composeEnhancers(applyMiddleware(thunk))) 35 | export const persistor = persistStore(store) 36 | -------------------------------------------------------------------------------- /src/components/ColorPicker/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/control-has-associated-label */ 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import classnames from 'classnames' 5 | import { CirclePicker } from 'react-color' 6 | import PropTypes from 'prop-types' 7 | 8 | import styles from './ColorPicker.module.scss' 9 | 10 | /** 11 | * @author zilahir 12 | * @function ColorPicker 13 | * */ 14 | 15 | const ColorPicker = ({ 16 | isVisible, 17 | onClose, 18 | onChangeColor, 19 | }) => ( 20 | <> 21 | { 22 | isVisible 23 | ? ReactDOM.createPortal( 24 |
, document.body, 31 | ) 32 | : null 33 | } 34 |
39 | onChangeColor(color.hex)} 41 | /> 42 |
43 | 44 | ) 45 | 46 | ColorPicker.propTypes = { 47 | isVisible: PropTypes.bool.isRequired, 48 | onChangeColor: PropTypes.func.isRequired, 49 | onClose: PropTypes.func.isRequired, 50 | // segmentIndex: PropTypes.number.isRequired, 51 | } 52 | 53 | export default ColorPicker 54 | -------------------------------------------------------------------------------- /src/components/common/SliderAlt/Slider.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables'; 2 | @import '../../../styles/mixins/label'; 3 | 4 | .sliderContainer { 5 | width: 185px; 6 | margin-bottom: 10px; 7 | } 8 | 9 | .slider { 10 | display: block; 11 | .rc-slider-step { 12 | background: $gray-1; 13 | border-radius: 24px; 14 | height: 5px; 15 | } 16 | .rc-slider-handle { 17 | width: 20px; 18 | height: 20px; 19 | background: $purple; 20 | border: none; 21 | margin-top: -8px; 22 | box-shadow: 0 0 0 5px $gray-2; 23 | &:focus { 24 | box-shadow: 0 0 0 5px $gray-2; 25 | } 26 | } 27 | .rc-slider-track { 28 | height: unset; 29 | } 30 | } 31 | 32 | .labelText { 33 | @include labelText; 34 | margin-left: -10px; 35 | margin-bottom: 0; 36 | font-weight: 400; 37 | } 38 | 39 | .sliderInner { 40 | display: flex; 41 | align-items: center; 42 | width: 100%; 43 | 44 | } 45 | 46 | .top { 47 | display: flex; 48 | justify-content: space-between; 49 | margin: 0 10px; 50 | .sliderValue { 51 | position: relative; 52 | right: -10px; 53 | margin-left: 10px; 54 | color: #fff; 55 | font: 400 14px/17px 'Barlow'; 56 | } 57 | } 58 | 59 | .rc-slider-rail { 60 | background-color: unset; 61 | } -------------------------------------------------------------------------------- /src/components/Loader/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | import ReactLoading from 'react-loading' 5 | 6 | import styles from './Loader.module.scss' 7 | import { Colors, FULL_LOADER, INLINE_LOADER } from '../../utils/consts' 8 | 9 | /** 10 | * @author zilahir 11 | * @function Loader 12 | * */ 13 | 14 | const Loader = ({ isLoading, color, type, width, height }) => ( 15 | <> 16 | { 17 | isLoading && type === FULL_LOADER 18 | ? ( 19 |
20 | 26 |
27 | ) 28 | : isLoading && type === INLINE_LOADER 29 | ? ( 30 |
31 | 37 |
38 | ) 39 | : null 40 | } 41 | 42 | ) 43 | 44 | Loader.defaultProps = { 45 | color: Colors.purple, 46 | height: 10, 47 | isLoading: false, 48 | type: FULL_LOADER, 49 | width: 10, 50 | } 51 | 52 | Loader.propTypes = { 53 | color: PropTypes.string, 54 | height: PropTypes.number, 55 | isLoading: PropTypes.bool, 56 | type: PropTypes.string, 57 | width: PropTypes.number, 58 | } 59 | 60 | export default Loader 61 | -------------------------------------------------------------------------------- /src/components/common/Break/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { ic_pause as pauseIcon } from 'react-icons-kit/md/ic_pause' 5 | import CloseIcon from '@material-ui/icons/Close' 6 | import Icon from 'react-icons-kit' 7 | 8 | import styles from './Break.module.scss' 9 | import { setSegments } from '../../../store/actions/segments' 10 | 11 | /** 12 | * @author zilahir 13 | * @function Break 14 | * */ 15 | 16 | const Break = ({ 17 | id, 18 | }) => { 19 | const dispatch = useDispatch() 20 | const allSegments = useSelector(state => state.segments.segments) 21 | function handleDelete() { 22 | const filteredSegments = allSegments.filter(segment => segment.id !== id) 23 | dispatch(setSegments(filteredSegments)) 24 | } 25 | return ( 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | 40 |
41 |
42 | ) 43 | } 44 | 45 | Break.propTypes = { 46 | id: PropTypes.string.isRequired, 47 | } 48 | 49 | export default Break 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [1.0.4](https://github.com/zilahir/teleprompter/compare/v1.0.3...v1.0.4) (2021-04-07) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **saving:** saving prompter ([bc25da1](https://github.com/zilahir/teleprompter/commit/bc25da113b5db8375c6a4c587346d65034ebca42)) 7 | 8 | ### [1.0.3](https://github.com/zilahir/teleprompter/compare/v1.0.2...v1.0.3) (2021-03-27) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **prompter-saving:** fixed prompter saving object ([dd342bc](https://github.com/zilahir/teleprompter/commit/dd342bcda77e46bf0b85b50ad5e08423522756a9)) 14 | 15 | ### [1.0.2](https://github.com/zilahir/teleprompter/compare/v1.0.1...v1.0.2) (2021-03-27) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **player:** removing store.getState() ([eb5c074](https://github.com/zilahir/teleprompter/commit/eb5c074872d0427da071357e20043fd3f67fc95e)) 21 | * **visuals:** visual fixes in header and in sidebar ([e57b7d1](https://github.com/zilahir/teleprompter/commit/e57b7d19a6392c5b4c1a03f3bcba97a7d158365c)) 22 | 23 | ### [1.0.1](https://github.com/zilahir/teleprompter/compare/v1.0.0...v1.0.1) (2021-03-26) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **socket:** new api url ([28dc5a3](https://github.com/zilahir/teleprompter/commit/28dc5a39691a638dd74b4e285850fac34c09a0a7)) 29 | 30 | ## 1.0.0 (2021-03-18) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * **loading:** loading back saved prompter ([297fc85](https://github.com/zilahir/teleprompter/commit/297fc85ad9693a85836ae330daed2860f6c17aab)) 36 | -------------------------------------------------------------------------------- /src/components/common/TextPreview/TextPreview.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables'; 2 | @import '../../../styles/mixins/heading'; 3 | 4 | .textpreviewContainer { 5 | color: #ffffff; 6 | 7 | .innerContainer { 8 | display: flex; 9 | justify-content: center; 10 | } 11 | p { 12 | margin: 0; 13 | } 14 | .mirroredContainer { 15 | border-top-left-radius: 20px; 16 | border-top-right-radius: 20px; 17 | background: rgba($color: #000000, $alpha: 0.3); 18 | width: 240px; 19 | height: 116px; 20 | overflow: hidden; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | .mirrored { 25 | padding: 20px; 26 | @include heading; 27 | opacity: 0.9; 28 | word-break: break-all; 29 | font-weight: 600; 30 | } 31 | } 32 | .textContainer { 33 | border-bottom-left-radius: 20px; 34 | border-bottom-right-radius: 20px; 35 | background: rgba($color: #000000, $alpha: 1.0); 36 | width: 240px; 37 | height: 116px; 38 | overflow: hidden; 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | .text { 43 | padding: 20px; 44 | @include heading; 45 | word-break: break-all; 46 | font-weight: 600; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Row, Container, Col } from 'react-grid-system' 3 | import styled from 'styled-components' 4 | import Icon from 'react-icons-kit' 5 | import { github } from 'react-icons-kit/feather/github' 6 | 7 | import styles from './Footer.module.scss' 8 | import { Colors } from '../../utils/consts' 9 | 10 | /** 11 | * @author zilahir 12 | * @function Footer 13 | * */ 14 | 15 | const IconContainer = styled.div` 16 | color: ${props => props.color}; 17 | ` 18 | 19 | const Footer = () => ( 20 | 57 | ) 58 | 59 | export default Footer 60 | -------------------------------------------------------------------------------- /src/components/TextScroller/TextScroller.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/hideScrollbar'; 3 | 4 | .rootContainer { 5 | background-color: rgba($color: #000000, $alpha: 1.0); 6 | 7 | .scrollerContainer { 8 | overflow-wrap: break-word; 9 | margin-left: auto; 10 | margin-right: auto; 11 | height: calc(100vh - 90px); 12 | overflow: scroll; 13 | .scroller { 14 | padding: 50px; 15 | p { 16 | margin: 0; 17 | padding: 0; 18 | white-space: pre-wrap; 19 | } 20 | 21 | .breakContainer { 22 | margin-top: 100px; 23 | margin-bottom: 80px; 24 | } 25 | } 26 | 27 | .segment { 28 | border-width: 2px; 29 | border-style: solid; 30 | border-radius: 20px; 31 | margin: 50px 0; 32 | 33 | &:first-of-type { 34 | margin: 0; 35 | } 36 | 37 | .segmentTextContainer { 38 | p { 39 | padding: 10px 60px; 40 | } 41 | } 42 | 43 | .segmentTitleContainer { 44 | padding: 10px 60px; 45 | 46 | p { 47 | font-family: 'Barlow'; 48 | font-size: 40px; 49 | } 50 | } 51 | } 52 | } 53 | 54 | &.light { 55 | background: rgba($color: #ffffff, $alpha: 1); 56 | color: #000000; 57 | } 58 | 59 | &.dark { 60 | background: rgba($color: #000000, $alpha: 1); 61 | color: #ffffff; 62 | } 63 | } 64 | 65 | @include hideScrollBar('scrollerContainer') -------------------------------------------------------------------------------- /src/components/common/Break/Break.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables'; 2 | 3 | .oneBreak { 4 | display: flex; 5 | height: 30px; 6 | background: rgba($color: $gray-2, $alpha: 1.0); 7 | border-radius: $border-radius * 2; 8 | padding: 0 10px; 9 | 10 | &:focus { 11 | outline: none; 12 | } 13 | 14 | .middle { 15 | flex: 1; 16 | display: flex; 17 | position: relative; 18 | color: #ffffff; 19 | justify-content: center; 20 | align-items: center; 21 | 22 | .icon { 23 | &:before { 24 | content: ''; 25 | height: 2px; 26 | width: calc((100% / 2) - 20px); 27 | position: absolute; 28 | background-color: rgba($color: #ffffff, $alpha: 1.0); 29 | left: 0; 30 | top: 50%; 31 | } 32 | 33 | &:after { 34 | content: ''; 35 | height: 2px; 36 | width: calc((100% / 2) - 20px); 37 | position: absolute; 38 | background-color: rgba($color: #ffffff, $alpha: 1.0); 39 | right: 0; 40 | top: 50%; 41 | } 42 | } 43 | } 44 | 45 | .deleteIconContainer { 46 | color: rgba($color: #ffffff, $alpha: 1.0); 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | width: 30px; 51 | 52 | .deleteBtn { 53 | border: 0; 54 | background: none; 55 | margin: 0; 56 | padding: 0; 57 | color: rgba($color: #ffffff, $alpha: 1.0); 58 | display: inline-block; 59 | font-size: unset; 60 | width: 24px; 61 | height: 24px; 62 | 63 | &:focus { 64 | outline: none; 65 | } 66 | 67 | &:hover { 68 | cursor: pointer; 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/store/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const TEST = 'TEST' 2 | export const SET_TEXT = 'SET_TEXT' 3 | export const CLEAR_TEXT = 'CLEAR_TEXT' 4 | export const SET_SPEED = 'SET_SPEED' 5 | export const SET_FONT_SIZE = 'SET_FONT_SIZE' 6 | export const GET_ALL_SEGMENTS = 'GET_ALL_SEGMENTs' 7 | export const REMOVE_SEGMENT = 'REMOVE_SEGMENT' 8 | export const REMOVE_ALL_SEGMENT = 'REMOVE_ALL_SEGMENT' 9 | export const SET_LINE_HEIGHT = 'SET_LINE_HEIGHT' 10 | export const SET_SCROLL_SPEED = 'SET_SCROLL_SPEED' 11 | export const SET_LETTER_SPACING = 'SET_LETTER_SPACING' 12 | export const TOGGLE_FLIPPED = 'TOGGLE_FLIPPED' 13 | export const SET_SCROLL_WIDTH = 'SET_SCROLL_WIDTH' 14 | export const AUTH_USER = 'AUTH_USER' 15 | export const REMOVE_USER = 'REMOVE_USER' 16 | export const GET_ALL_PROMPTER = 'GET_ALL_PROMPTER' 17 | export const CLEAR_ALL_PROMPTER = 'CLEAR_ALL_PROMPTER' 18 | export const SET_PROMPTER_SLUG = 'SET_PROMPTER_SLUG' 19 | export const SET_PROJECT_NAME = 'SET_PROMPTER_SLUG' 20 | export const COPY_PROMPTER_OBJECT = 'COPY_PROMPTER_OBJECT' 21 | export const CLEAR_PROMPTER_OBJECT = 'CLEAR_PROMPTER_OBJECT' 22 | export const TOGGLE_UPDATE_BTN = 'TOGGLE_UPDATE_BTN' 23 | export const HIDE_INSTRUCTION = 'HIDE_INSTRUCTION' 24 | export const RESET_PROMPTER = 'RESET_PROMPTER' 25 | export const SET_COLOR_SCHEME = 'SET_COLOR_SCHEME' 26 | export const SET_FONT = 'SET_FONT' 27 | export const ADD_SEGGMENT = 'ADD_SEGGMENT' 28 | export const CLEAR_ALL_SEGMENTS = 'CLEAR_ALL_SEGMENTS' 29 | export const MODIFY_SEGMENT = 'MODIFY_SEGMENT' 30 | export const SET_TEXT_ALIGNMENT = 'SET_TEXT_ALIGNMENT' 31 | -------------------------------------------------------------------------------- /src/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, BrowserRouter as Router } from 'react-router-dom' 3 | import { isMobile } from 'react-device-detect' 4 | import { Helmet } from 'react-helmet' 5 | import ReactGA from 'react-ga' 6 | 7 | import Player from '../components/Player' 8 | import Main from '../components/Main' 9 | import Mobile from '../components/Mobile' 10 | import MobileController from '../components/MobileController' 11 | import Policy from '../components/Policy' 12 | import About from '../components/About' 13 | import Password from '../components/Password' 14 | import { PLAYER, REMOTE, POLICY, ABOUT, FORGOTTEN_PW, HOME } from '../utils/consts' 15 | 16 | /** 17 | * @author 18 | * @function App 19 | * */ 20 | 21 | const App = () => { 22 | ReactGA.initialize('UA-163692111-1', { 23 | debug: true, 24 | }) 25 | ReactGA.pageview(`/${HOME}`) 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | ) 42 | } 43 | 44 | export default App 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Richard Zilahi, Mikko Oitinen 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/components/EditorSidebar/Toggle.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/label'; 3 | 4 | .toggleWrapper { 5 | display: flex; 6 | flex-direction: column; 7 | width: 185px; 8 | margin: 10px -10px; 9 | p { 10 | margin: 0; 11 | padding: 0; 12 | @include labelText; 13 | margin-bottom: 20px; 14 | margin-left: -10px; 15 | font-weight: 300; 16 | } 17 | .react-toggle { 18 | position: relative; 19 | margin-left: -10px; 20 | &.react-toggle--checked { 21 | .react-toggle-thumb { 22 | border: none; 23 | background-color: $purple; 24 | box-shadow: none; 25 | } 26 | } 27 | .react-toggle-track { 28 | background-color: $gray-1; 29 | width: 65px; 30 | height: 25px; 31 | } 32 | .react-toggle-thumb { 33 | width: 36px; 34 | height: 25px; 35 | border-radius: 37px; 36 | border: none; 37 | &:focus { 38 | outline: none; 39 | } 40 | } 41 | } 42 | } 43 | 44 | .react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { 45 | background: rgba($color: $gray-1, $alpha: 0.5); 46 | } 47 | 48 | .react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { 49 | background: rgba($color: $gray-1, $alpha: 0.5); 50 | } 51 | 52 | .react-toggle--focus { 53 | .react-toggle-thumb { 54 | &:focus { 55 | outline: none; 56 | } 57 | box-shadow: none; 58 | } 59 | } -------------------------------------------------------------------------------- /src/components/EditorSidebar/EditorSidebar.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/label'; 3 | @import '../../styles/mixins/paragraph'; 4 | 5 | .editorSidebarContainer { 6 | background-color: $gray-2; 7 | padding-top: 20px; 8 | 9 | .innerContainer { 10 | display: flex; 11 | justify-content: flex-end; 12 | align-items: flex-end; 13 | margin-right: 45px; 14 | flex-direction: column; 15 | 16 | .selectorContainer { 17 | position: relative; 18 | left: -10px; 19 | margin: 0px -10px; 20 | margin-bottom: 10px; 21 | margin-top: 10px; 22 | 23 | .widthLabel { 24 | margin-top: 0; 25 | @include labelText; 26 | font-weight: 400; 27 | } 28 | } 29 | } 30 | } 31 | 32 | .footerContanier { 33 | display: flex; 34 | justify-content: flex-end; 35 | flex-direction: column; 36 | align-items: flex-end; 37 | padding: 4px 0px; 38 | margin-right: 45px; 39 | padding-bottom: 10px; 40 | padding-top: 20px; 41 | 42 | .github { 43 | color: inherit; 44 | margin-top: 10px; 45 | svg { 46 | fill: $purple; 47 | } 48 | } 49 | p { 50 | @include paragraph(14px, #ffffff, 0.42px); 51 | a { 52 | color: inherit; 53 | text-decoration: none; 54 | } 55 | margin-left: 20px; 56 | 57 | span { 58 | &.purple { 59 | color: $purple; 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/components/Player/Player.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .app { 5 | display: grid; 6 | padding: 2.5em; 7 | text-align: center; 8 | justify-items: center; 9 | } 10 | 11 | .controls { 12 | align-items: center; 13 | display: grid; 14 | justify-content: center; 15 | grid-auto-flow: column; 16 | grid-column-gap: 1em; 17 | } 18 | 19 | .githubLink { 20 | font-size: xx-large; 21 | } 22 | 23 | .header { 24 | display: grid; 25 | grid-auto-flow: column; 26 | } 27 | 28 | .playerHeader { 29 | display: flex; 30 | width: 100%; 31 | flex: 1; 32 | height: 70px; 33 | align-items: center; 34 | background-color: rgba($color: $gray-5, $alpha: 1.0); 35 | padding: 10px 0; 36 | position: relative; 37 | z-index: 2; 38 | 39 | .innerContainer { 40 | width: calc(100% - 40px); 41 | display: flex; 42 | justify-content: center; 43 | 44 | div { 45 | margin-right: 20px; 46 | display: flex; 47 | justify-content: center; 48 | flex-direction: column; 49 | } 50 | p { 51 | @include paragraph(14px, $gray-4, 0); 52 | &.shorten { 53 | width: 80%; 54 | } 55 | span { 56 | font-weight: 700; 57 | } 58 | } 59 | 60 | .updateBtnContainer { 61 | .updateBtn { 62 | button { 63 | width: 130px; 64 | height: 45px; 65 | align-items: center; 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/components/common/Instruction/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { useDispatch } from 'react-redux' 4 | import classnames from 'classnames' 5 | import styled from 'styled-components' 6 | 7 | import styles from './Instruction.module.scss' 8 | import { hideInstruction } from '../../../store/actions/misc' 9 | 10 | /** 11 | * @author zilahir 12 | * @function Instruction 13 | * */ 14 | 15 | const InstructionContainer = styled.p` 16 | max-width: ${props => props.maxWidth}px; 17 | ` 18 | 19 | const Instruction = props => { 20 | const { text, hasPadding, maxWidth, type, noHide } = props 21 | const dispatch = useDispatch() 22 | function hideThisInfoBox() { 23 | dispatch(hideInstruction(type, false)) 24 | } 25 | return ( 26 |
32 | 35 | {text} 36 | { 37 | !noHide 38 | ? ( 39 | hideThisInfoBox()} 41 | role="button" 42 | onKeyDown={null} 43 | tabIndex={-1} 44 | > 45 | Hide this guide 46 | 47 | ) 48 | : null 49 | } 50 | 51 |
52 | ) 53 | } 54 | 55 | Instruction.defaultProps = { 56 | hasPadding: true, 57 | maxWidth: 'unset', 58 | noHide: false, 59 | } 60 | 61 | Instruction.propTypes = { 62 | hasPadding: PropTypes.bool, 63 | maxWidth: PropTypes.number, 64 | noHide: PropTypes.bool, 65 | text: PropTypes.string.isRequired, 66 | type: PropTypes.string.isRequired, 67 | } 68 | 69 | export default Instruction 70 | -------------------------------------------------------------------------------- /src/components/Policy/Policy.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .aboutWrapper { 5 | &.about { 6 | background: transparent; 7 | } 8 | 9 | .dark, 10 | &.dark, 11 | { 12 | background: rgba($color: $gray-2, $alpha: 1.0); 13 | } 14 | 15 | .middle { 16 | padding: 50px 20px; 17 | .textContainer { 18 | display: flex; 19 | justify-content: center; 20 | flex-direction: column; 21 | max-width: 620px; 22 | margin-left: auto; 23 | margin-right: auto; 24 | 25 | .title { 26 | @include paragraph(60px, #ffffff, 0.39px); 27 | margin-bottom: 20px; 28 | } 29 | } 30 | 31 | .buttonContainer { 32 | padding: 50px 20px; 33 | padding-top: 0; 34 | max-width: 620px; 35 | margin-left: auto; 36 | margin-right: auto; 37 | } 38 | } 39 | 40 | p { 41 | @include paragraph(15px, #ffffff, 0.39px); 42 | line-height: 1.7; 43 | margin: 10px 0; 44 | ul { 45 | list-style-type: none; 46 | } 47 | 48 | a { 49 | color: $purple; 50 | text-decoration: none; 51 | margin-left: 3px; 52 | 53 | &:hover { 54 | cursor: pointer; 55 | text-decoration: underline; 56 | } 57 | } 58 | } 59 | 60 | h1 { 61 | @include paragraph(20px, $purple, 0.39px); 62 | line-height: 1.7; 63 | margin: 10px 0; 64 | font-weight: 300; 65 | } 66 | } 67 | 68 | 69 | .closeBtnContainer { 70 | display: flex; 71 | justify-content: flex-end; 72 | 73 | &.policyBtn { 74 | max-width: 620px; 75 | margin: 0 auto; 76 | } 77 | 78 | .closeBtn { 79 | background: none; 80 | border: 0; 81 | margin: 0; 82 | padding: 0; 83 | box-shadow: 0; 84 | 85 | &:hover { 86 | cursor: pointer; 87 | } 88 | 89 | &:focus { 90 | outline: none; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/store/actions/text.js: -------------------------------------------------------------------------------- 1 | import { SET_FONT_SIZE, SET_TEXT, SET_LETTER_SPACING, SET_LINE_HEIGHT, TOGGLE_FLIPPED, SET_SCROLL_WIDTH, SET_SCROLL_SPEED, CLEAR_TEXT, RESET_PROMPTER, SET_FONT, SET_TEXT_ALIGNMENT } from './actionTypes' 2 | 3 | export const setFontSize = fontSize => ({ 4 | type: SET_FONT_SIZE, 5 | payload: { 6 | fontSize, 7 | }, 8 | }) 9 | 10 | export const setText = text => ({ 11 | type: SET_TEXT, 12 | payload: { 13 | text, 14 | }, 15 | }) 16 | 17 | export const clearText = () => ({ 18 | type: CLEAR_TEXT, 19 | payload: {}, 20 | }) 21 | 22 | export const setLetterSpacing = letterSpacing => ({ 23 | type: SET_LETTER_SPACING, 24 | payload: { 25 | letterSpacing, 26 | }, 27 | }) 28 | 29 | export const setLineHeight = lineHeight => ({ 30 | type: SET_LINE_HEIGHT, 31 | payload: { 32 | lineHeight, 33 | }, 34 | }) 35 | 36 | export const toggleMirror = isFlipped => ({ 37 | type: TOGGLE_FLIPPED, 38 | payload: { 39 | isFlipped, 40 | }, 41 | }) 42 | 43 | export const setScrollWidth = scrollWidth => ({ 44 | type: SET_SCROLL_WIDTH, 45 | payload: { 46 | scrollWidth, 47 | }, 48 | }) 49 | 50 | export const setScrollSpeed = scrollSpeed => ({ 51 | type: SET_SCROLL_SPEED, 52 | payload: { 53 | scrollSpeed, 54 | }, 55 | }) 56 | 57 | export const resetPrompter = () => dispatch => new Promise(resolve => { 58 | dispatch({ 59 | type: RESET_PROMPTER, 60 | payload: {}, 61 | }) 62 | resolve({ 63 | success: true, 64 | }) 65 | }) 66 | 67 | export const setFont = chosenFont => dispatch => { 68 | dispatch({ 69 | type: SET_FONT, 70 | payload: { 71 | chosenFont, 72 | }, 73 | }) 74 | } 75 | 76 | export const setTextAlignment = textAlignment => dispatch => { 77 | dispatch({ 78 | type: SET_TEXT_ALIGNMENT, 79 | payload: { 80 | textAlignment, 81 | }, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /src/components/common/Selector/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | 5 | import { theme } from '../../../utils/theme' 6 | import { Colors as teleprompterColors } from '../../../utils/consts' 7 | import styles from './Selector.module.scss' 8 | 9 | /** 10 | * @author 11 | * @function Selector 12 | * */ 13 | 14 | const Item = styled.div` 15 | background-color: ${props => (props.isActive ? teleprompterColors.purple : teleprompterColors.gray1)}; 16 | border-top-left-radius: ${props => (props.isFirst ? `${theme.misc.borderRadius}px` : 0)}; 17 | border-bottom-left-radius: ${props => (props.isFirst ? `${theme.misc.borderRadius}px` : 0)}; 18 | border-top-right-radius: ${props => (props.isLast ? `${theme.misc.borderRadius}px` : 0)}; 19 | border-bottom-right-radius: ${props => (props.isLast ? `${theme.misc.borderRadius}px` : 0)}; 20 | 21 | ` 22 | 23 | const Selector = props => { 24 | const { items, activeId, onClick } = props 25 | 26 | function handleChange(chosenId) { 27 | onClick(chosenId) 28 | } 29 | return ( 30 |
31 | { 32 | items.map((item, index) => ( 33 | handleChange(item.id)} 39 | className={styles.selectorItem} 40 | > 41 |

42 | {item.label} 43 |

44 |
45 | )) 46 | } 47 |
48 | ) 49 | } 50 | 51 | Selector.propTypes = { 52 | activeId: PropTypes.number.isRequired, 53 | items: PropTypes.arrayOf( 54 | PropTypes.any, 55 | ).isRequired, 56 | onClick: PropTypes.func.isRequired, 57 | } 58 | 59 | export default Selector 60 | -------------------------------------------------------------------------------- /src/components/common/Checkbox/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | 5 | import { Colors } from '../../../utils/consts' 6 | 7 | const CheckboxContainer = styled.div` 8 | display: inline-block; 9 | vertical-align: middle; 10 | ` 11 | 12 | const Icon = styled.svg` 13 | fill: none; 14 | stroke: white; 15 | stroke-width: 2px; 16 | ` 17 | const HiddenCheckbox = styled.input.attrs({ type: 'checkbox' })` 18 | border: 0; 19 | clip: rect(0 0 0 0); 20 | clippath: inset(50%); 21 | height: 1px; 22 | margin: -1px; 23 | overflow: hidden; 24 | padding: 0; 25 | position: absolute; 26 | white-space: nowrap; 27 | width: 1px; 28 | ` 29 | 30 | const StyledCheckbox = styled.div` 31 | display: inline-block; 32 | width: 16px; 33 | height: 16px; 34 | background: #2D2D2D; 35 | border: 2px solid #ffffff; 36 | border-radius: 3px; 37 | transition: all 150ms; 38 | 39 | ${HiddenCheckbox}:focus + & { 40 | box-shadow: 0 0 0 3px pink; 41 | } 42 | 43 | ${Icon} { 44 | visibility: ${props => (props.checked ? 'visible' : 'hidden')}; 45 | stroke: ${Colors.purple}; 46 | stroke-width: 4px; 47 | } 48 | ` 49 | 50 | /** 51 | * @author zilahir 52 | * @function Checkbox 53 | * */ 54 | 55 | const Checkbox = ({ className, checked, onChange }) => ( 56 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ) 70 | 71 | Checkbox.propTypes = { 72 | checked: PropTypes.bool.isRequired, 73 | className: PropTypes.string.isRequired, 74 | onChange: PropTypes.func.isRequired, 75 | } 76 | 77 | export default Checkbox 78 | -------------------------------------------------------------------------------- /src/components/common/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables'; 2 | @import '../../../styles/mixins/paragraph'; 3 | 4 | .buttonContainer { 5 | .button { 6 | min-width: 242px; 7 | height: 57px; 8 | border-radius: 32px; 9 | background-color: rgba($color: $purple, $alpha: 0.9); 10 | text-transform: uppercase; 11 | @include paragraph(14px, #fff, 0.85px); 12 | border: none; 13 | font-weight: 800; 14 | 15 | &:focus { 16 | outline: none; 17 | } 18 | 19 | &:hover { 20 | cursor: pointer; 21 | background-color: rgba($color: $purple, $alpha: 1); 22 | transition: all .2s ease; 23 | } 24 | 25 | &:focus { 26 | outline: none; 27 | } 28 | 29 | &.negative { 30 | background: rgba($color: $gray-6, $alpha: 1.0); 31 | } 32 | &:disabled { 33 | background: rgba($color: $gray-6, $alpha: 1.0); 34 | } 35 | 36 | &.hasIcon { 37 | display: flex; 38 | justify-content: center; 39 | } 40 | 41 | .icon { 42 | margin-right: 10px; 43 | width: 24px; 44 | height: 24px; 45 | } 46 | } 47 | .linkButton { 48 | border: none; 49 | background-color: transparent; 50 | @include paragraph(14px, #fff, 0.85px); 51 | transition: all .2s ease; 52 | 53 | &:hover { 54 | cursor: pointer; 55 | color: $purple; 56 | transition: all .2s ease; 57 | } 58 | 59 | &:focus { 60 | outline: none; 61 | } 62 | 63 | &:disabled { 64 | opacity: 0.5; 65 | 66 | &:hover { 67 | pointer-events: none; 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/store/reducers/text.js: -------------------------------------------------------------------------------- 1 | import { SET_FONT_SIZE, SET_TEXT, SET_LINE_HEIGHT, SET_LETTER_SPACING, SET_SCROLL_WIDTH, SET_SCROLL_SPEED, CLEAR_TEXT, RESET_PROMPTER, TOGGLE_FLIPPED, SET_FONT, SET_TEXT_ALIGNMENT } from '../actions/actionTypes' 2 | import { SANS } from '../../utils/consts' 3 | 4 | export const textState = { 5 | fontSize: 2, 6 | text: '', 7 | lineHeight: 1, 8 | letterSpacing: 1, 9 | scrollWidth: '100%', 10 | scrollSpeed: 1, 11 | isFlipped: false, 12 | chosenFont: SANS, 13 | textAlignment: 0, 14 | } 15 | 16 | const reducer = (state = textState, action) => { 17 | switch (action.type) { 18 | case SET_FONT_SIZE: 19 | return { 20 | ...state, 21 | fontSize: action.payload.fontSize, 22 | } 23 | case SET_TEXT: 24 | return { 25 | ...state, 26 | text: action.payload.text, 27 | } 28 | case SET_LINE_HEIGHT: 29 | return { 30 | ...state, 31 | lineHeight: action.payload.lineHeight, 32 | } 33 | case SET_LETTER_SPACING: 34 | return { 35 | ...state, 36 | letterSpacing: action.payload.letterSpacing, 37 | } 38 | case SET_SCROLL_WIDTH: { 39 | return { 40 | ...state, 41 | scrollWidth: action.payload.scrollWidth, 42 | } 43 | } 44 | case SET_SCROLL_SPEED: { 45 | return { 46 | ...state, 47 | scrollSpeed: action.payload.scrollSpeed, 48 | } 49 | } 50 | case TOGGLE_FLIPPED: { 51 | return { 52 | ...state, 53 | isFlipped: action.payload.isFlipped, 54 | } 55 | } 56 | case CLEAR_TEXT: { 57 | return { 58 | ...state, 59 | text: '', 60 | } 61 | } 62 | case RESET_PROMPTER: 63 | return { 64 | ...textState, 65 | } 66 | case SET_FONT: 67 | return { 68 | ...state, 69 | chosenFont: action.payload.chosenFont, 70 | } 71 | case SET_TEXT_ALIGNMENT: 72 | return { 73 | ...state, 74 | textAlignment: action.payload.textAlignment, 75 | } 76 | default: 77 | return state 78 | } 79 | } 80 | 81 | export default reducer 82 | -------------------------------------------------------------------------------- /src/components/common/Modal/Modal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles/variables"; 2 | @import "../../../styles/mixins/keyframes"; 3 | 4 | @include keyframes(opacity) { 5 | 0% { 6 | opacity: 0; 7 | } 8 | 9 | 100% { 10 | opacity: 1; 11 | } 12 | } 13 | 14 | .modalOverlay { 15 | animation: opacity 0.3s; 16 | position: absolute; 17 | top: 0; 18 | width: 100%; 19 | height: 100%; 20 | background: $overlay-bg-color; 21 | opacity: 0.9; 22 | z-index: 9; 23 | backdrop-filter: blur($modal-backdrop-blur-value / 2); 24 | left: 0; 25 | } 26 | 27 | .modalWrapper { 28 | width: fit-content; 29 | height: fit-content; 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | right: 0; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | background-color: rgba($color: $modal-background-color, $alpha: 0.9); 38 | margin-left: auto; 39 | margin-right: auto; 40 | margin-top: auto; 41 | margin-bottom: auto; 42 | bottom: 0; 43 | outline: none; 44 | border-radius: $login-box-border-radius; 45 | z-index: 9; 46 | animation: opacity 0.3s; 47 | 48 | .modal { 49 | display: flex; 50 | flex-direction: column; 51 | padding: 30px; 52 | 53 | div { 54 | .header { 55 | padding: 10px; 56 | display: flex; 57 | justify-content: space-between; 58 | align-items: center; 59 | 60 | button { 61 | border: 0; 62 | display: flex; 63 | justify-content: flex-end; 64 | flex: 1; 65 | outline: none; 66 | 67 | &:hover { 68 | cursor: pointer; 69 | outline: none; 70 | } 71 | } 72 | } 73 | 74 | .closeBtn { 75 | background: none; 76 | border: none; 77 | box-shadow: none; 78 | align-self: flex-end; 79 | display: flex; 80 | justify-content: flex-end; 81 | width: 100%; 82 | 83 | &:focus { 84 | outline: none; 85 | } 86 | 87 | &:hover { 88 | cursor: pointer; 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/Player/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import Icon from 'react-icons-kit' 4 | import { refresh } from 'react-icons-kit/fa/refresh' 5 | import PropTypes from 'prop-types' 6 | 7 | import styles from '../Player.module.scss' 8 | import Button from '../../common/Button' 9 | 10 | /** 11 | * @author zilahir 12 | * @function Header 13 | * */ 14 | 15 | const Header = props => { 16 | const { userPrompters } = useSelector(store => store) 17 | const { isUpdateBtnVisible, updateBtnClick } = props 18 | return ( 19 |
20 |
21 |
22 |

23 | Keyboard 24 |

25 |

26 | Keyboard Space to play/pause, up/down and pgup/pgdown to navigate, 27 | left/right to adjust speed 28 |

29 |
30 |
31 |

32 | Mouse 33 |

34 |

35 | Mouse Left click to play/pause, scroll to navigate 36 |

37 |
38 |
39 | { 40 | isUpdateBtnVisible 41 | ? ( 42 |
54 |
55 |

56 | Your session ID is {userPrompters.prompterSlug} 57 |

58 |
59 |
60 |

61 | Pres F6 to toggle fullscreen 62 |

63 |
64 |
65 |
66 | ) 67 | } 68 | 69 | Header.defaultProps = { 70 | isUpdateBtnVisible: false, 71 | } 72 | 73 | Header.propTypes = { 74 | isUpdateBtnVisible: PropTypes.bool, 75 | updateBtnClick: PropTypes.func.isRequired, 76 | } 77 | 78 | export default Header 79 | -------------------------------------------------------------------------------- /src/components/ActionHeader/ActionHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .btnList { 5 | display: flex; 6 | list-style-type: none; 7 | li { 8 | margin-right: 20px; 9 | } 10 | 11 | &.flexStart { 12 | padding-left: 0; 13 | } 14 | 15 | &.flexEnd { 16 | li { 17 | &:last-of-type { 18 | margin-right: 0; 19 | } 20 | } 21 | } 22 | 23 | &.hidden { 24 | display: none; 25 | visibility: hidden; 26 | } 27 | } 28 | 29 | .innerContainer { 30 | display: flex; 31 | align-items: center; 32 | } 33 | 34 | .topHeaderRoot { 35 | padding-left: 0; 36 | padding-right: 0; 37 | background: rgba($color: $gray-1, $alpha: 1.0); 38 | position: relative; 39 | z-index: 99; 40 | 41 | .logoContainer { 42 | justify-content: flex-end; 43 | display: flex; 44 | position: relative; 45 | left: 20px; 46 | overflow: hidden; 47 | flex: 0 0 25%; 48 | width: 25%; 49 | left: auto; 50 | right: auto; 51 | 52 | div { 53 | width: 250px; 54 | img { 55 | max-width: 133px; 56 | } 57 | } 58 | } 59 | 60 | .middleContainer { 61 | width: 50%; 62 | flex: 0 0 50%; 63 | display: flex; 64 | justify-content: space-between; 65 | z-index: 9; 66 | } 67 | 68 | .rightContainer { 69 | flex: 0 0 25%; 70 | width: 25%; 71 | left: auto; 72 | right: auto; 73 | position: relative; 74 | 75 | .btnList { 76 | margin-left: 20px; 77 | } 78 | } 79 | } 80 | 81 | .modal { 82 | padding: 30px; 83 | 84 | h3 { 85 | @include paragraph(14px, #ffffff); 86 | font-weight: 300; 87 | margin: 0; 88 | margin-bottom: 20px; 89 | max-width: 500px; 90 | } 91 | 92 | p { 93 | @include paragraph(14px, #ffffff); 94 | } 95 | 96 | .buttonContainer { 97 | display: flex; 98 | padding-top: 30px; 99 | button { 100 | margin: 0 10px; 101 | font-weight: 300; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/components/common/SliderAlt/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Slider from 'rc-slider' 4 | import { useDispatch } from 'react-redux' 5 | import 'rc-slider/assets/index.css' 6 | 7 | import { SET_FONT_SIZE, SET_LETTER_SPACING, SET_LINE_HEIGHT, SET_SCROLL_SPEED } from '../../../store/actions/actionTypes' 8 | import { setFontSize, setLetterSpacing, setLineHeight, setScrollSpeed } from '../../../store/actions/text' 9 | import './Slider.scss' 10 | 11 | /** 12 | * @author zilahir 13 | * @function SliderAlt 14 | * */ 15 | 16 | const SliderAlt = props => { 17 | const { labelText, sliderName, initialValue, step, maxValue, minValue } = props 18 | 19 | const dispatch = useDispatch() 20 | function handleValeChange(v) { 21 | if (sliderName === SET_FONT_SIZE) { 22 | dispatch(setFontSize(v)) 23 | } else if (sliderName === SET_LETTER_SPACING) { 24 | dispatch(setLetterSpacing(v)) 25 | } else if (sliderName === SET_LINE_HEIGHT) { 26 | dispatch(setLineHeight(v)) 27 | } else if (sliderName === SET_SCROLL_SPEED) { 28 | dispatch(setScrollSpeed(v)) 29 | } 30 | } 31 | 32 | return ( 33 |
36 |
37 |

38 | {labelText} 39 |

40 |

41 | {initialValue} 42 |

43 |
44 |
45 | handleValeChange(val)} 49 | name={sliderName} 50 | step={step} 51 | max={maxValue} 52 | min={minValue} 53 | /> 54 |
55 |
56 | ) 57 | } 58 | 59 | SliderAlt.defaultProps = { 60 | initialValue: 10, 61 | maxValue: 100, 62 | minValue: 1, 63 | step: 1, 64 | } 65 | 66 | SliderAlt.propTypes = { 67 | initialValue: PropTypes.number, 68 | labelText: PropTypes.string.isRequired, 69 | maxValue: PropTypes.number, 70 | minValue: PropTypes.number, 71 | sliderName: PropTypes.string.isRequired, 72 | step: PropTypes.number, 73 | } 74 | 75 | export default SliderAlt 76 | -------------------------------------------------------------------------------- /src/store/actions/authUser.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { AUTH_USER, REMOVE_USER } from './actionTypes' 4 | import { apiEndpoints } from '../../utils/apiEndpoints' 5 | 6 | const headers = { 7 | 'Content-Type': 'application/json', 8 | } 9 | 10 | export const setUser = user => dispatch => new Promise(resolve => { 11 | dispatch({ 12 | type: AUTH_USER, 13 | payload: { 14 | user, 15 | }, 16 | }) 17 | resolve(user) 18 | }) 19 | 20 | export const removeUser = user => dispatch => new Promise(resolve => { 21 | dispatch({ 22 | type: REMOVE_USER, 23 | payload: {}, 24 | }) 25 | resolve(user) 26 | }) 27 | 28 | export const authUser = user => dispatch => new Promise(resolve => { 29 | const authObject = { 30 | email: `${user.email}`, 31 | password: `${user.password}`, 32 | } 33 | axios.post(apiEndpoints.authUser, JSON.stringify(authObject), { 34 | headers, 35 | }) 36 | .then(resp => { 37 | dispatch(setUser(resp.data)) 38 | resolve(resp.data) 39 | }) 40 | }) 41 | 42 | export const refreshToken = token => dispatch => new Promise(resolve => { 43 | axios.get(apiEndpoints.refreshToken, { 44 | params: { 45 | refresh_token: token, 46 | }, 47 | }) 48 | .then(resp => { 49 | dispatch(setUser(resp.data)) 50 | resolve(resp.data) 51 | }) 52 | }) 53 | 54 | export const logOutUser = () => dispatch => new Promise(resolve => { 55 | dispatch(removeUser()) 56 | resolve(true) 57 | }) 58 | 59 | export const createNewUser = newUserObject => new Promise(resolve => { 60 | axios.post(apiEndpoints.newUser, JSON.stringify(newUserObject), { 61 | headers, 62 | }) 63 | .then(resp => { 64 | resolve(resp.data) 65 | }) 66 | }) 67 | 68 | export const checkPassword = userObject => new Promise(resolve => { 69 | axios.post(apiEndpoints.checkPassword, JSON.stringify(userObject), { 70 | headers, 71 | }) 72 | .then(resp => { 73 | resolve(resp.data) 74 | }) 75 | }) 76 | 77 | export const deleteAccount = (userId, authToken) => new Promise(resolve => { 78 | axios.defaults.headers.common.authorization = `Bearer ${authToken}` 79 | axios.delete(`${apiEndpoints.deleteUser}/${userId}`, { 80 | headers, 81 | }) 82 | .then(res => { 83 | resolve(res.data) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /src/components/ActionSidebar/ActionSidebar.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph.scss'; 3 | 4 | .actionSidebarContainer { 5 | background-color: $gray-2; 6 | .innerContainer { 7 | margin: 30px 40px; 8 | max-width: 230px; 9 | 10 | .testAnimation { 11 | padding: 10px 0; 12 | display: flex; 13 | justify-content: flex-start; 14 | } 15 | 16 | .playButtonContainer { 17 | 18 | button { 19 | font-weight: 500; 20 | text-transform: none; 21 | font-size: 16px; 22 | letter-spacing: 0.45px; 23 | justify-content: center; 24 | align-items: center; 25 | display: flex; 26 | } 27 | 28 | .updateBtn { 29 | margin-bottom: 20px; 30 | display: flex; 31 | 32 | button { 33 | box-shadow: 3px 3px 20px rgba($color: #000000, $alpha: 0.5); 34 | } 35 | } 36 | 37 | .playBtn { 38 | transition: all 0.2 ease-in-out; 39 | 40 | &.hidden { 41 | opacity: 0; 42 | } 43 | 44 | &.visible { 45 | opacity: 1; 46 | } 47 | 48 | button { 49 | box-shadow: 3px 3px 20px rgba($color: #000000, $alpha: 0.5); 50 | text-transform: capitalize; 51 | 52 | div { 53 | position: relative; 54 | top: 2px; 55 | } 56 | } 57 | } 58 | } 59 | 60 | .about { 61 | a { 62 | @include paragraph(14px, #ffffff, 0.42px); 63 | text-decoration: none; 64 | 65 | &:hover { 66 | cursor: pointer; 67 | text-decoration: underline; 68 | color: $purple; 69 | } 70 | } 71 | } 72 | 73 | .addressInput { 74 | opacity: 0.5; 75 | pointer-events: none; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 30 | Prompter.me 31 | 32 | 33 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/utils/fakeApi/segments.js: -------------------------------------------------------------------------------- 1 | import random from 'random' 2 | 3 | import { colors } from '../consts' 4 | 5 | const segmentsApi = { 6 | segments: [ 7 | { id: 1, segmentTitle: 'Lorem ipsum', segmentText: 'lofasz', segmentColor: colors[random.int(0, colors.length - 1)] }, 8 | { id: 2, segmentTitle: 'Lorem ipsum', segmentText: 'Eu duis Lorem pariatur sit aute enim. Deserunt ea amet veniam ex sit incididunt officia excepteur. Consectetur do dolor nisi non quis laboris eu consectetur nisi esse labore. Ea nostrud qui culpa excepteur voluptate est amet tempor. Dolore exercitation proident officia laboris. Enim sunt nisi sit deserunt duis aute dolore elit cupidatat ipsum fugiat irure est occaecat.', segmentColor: colors[random.int(0, colors.length - 1)] }, 9 | { id: 3, segmentTitle: 'Lorem ipsum', segmentText: 'Eu duis Lorem pariatur sit aute enim. Deserunt ea amet veniam ex sit incididunt officia excepteur. Consectetur do dolor nisi non quis laboris eu consectetur nisi esse labore. Ea nostrud qui culpa excepteur voluptate est amet tempor. Dolore exercitation proident officia laboris. Enim sunt nisi sit deserunt duis aute dolore elit cupidatat ipsum fugiat irure est occaecat.', segmentColor: colors[random.int(0, colors.length - 1)] }, 10 | { id: 4, segmentTitle: 'Lorem ipsum', segmentText: 'Eu duis Lorem pariatur sit aute enim. Deserunt ea amet veniam ex sit incididunt officia excepteur. Consectetur do dolor nisi non quis laboris eu consectetur nisi esse labore. Ea nostrud qui culpa excepteur voluptate est amet tempor. Dolore exercitation proident officia laboris. Enim sunt nisi sit deserunt duis aute dolore elit cupidatat ipsum fugiat irure est occaecat.', segmentColor: colors[random.int(0, colors.length - 1)] }, 11 | { id: 5, segmentTitle: 'Lorem ipsum', segmentText: 'Eu duis Lorem pariatur sit aute enim. Deserunt ea amet veniam ex sit incididunt officia excepteur. Consectetur do dolor nisi non quis laboris eu consectetur nisi esse labore. Ea nostrud qui culpa excepteur voluptate est amet tempor. Dolore exercitation proident officia laboris. Enim sunt nisi sit deserunt duis aute dolore elit cupidatat ipsum fugiat irure est occaecat.', segmentColor: colors[random.int(0, colors.length - 1)] }, 12 | ], 13 | 14 | getAllSegments() { return this.segments }, 15 | } 16 | 17 | export default segmentsApi 18 | -------------------------------------------------------------------------------- /src/components/common/Input/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | import React, { useState } from 'react' 3 | import PropTypes from 'prop-types' 4 | import classnames from 'classnames' 5 | 6 | import styles from './Input.module.scss' 7 | 8 | /** 9 | * @author zilahir 10 | * @function Input 11 | * */ 12 | 13 | const Input = props => { 14 | const { 15 | labelText, 16 | isDisabled, 17 | inheritedValue, 18 | inputClassName, 19 | inputType, 20 | getBackValue, 21 | placeholder, 22 | children, 23 | hasKeyDownEvent, 24 | keyDownEvent, 25 | onFocusOut, 26 | } = props 27 | const [value, setValue] = useState(null) 28 | 29 | function handleChange(v) { 30 | setValue(v) 31 | if (getBackValue) { 32 | getBackValue(v) 33 | } 34 | } 35 | return ( 36 |
41 | 63 |
64 | ) 65 | } 66 | 67 | Input.defaultProps = { 68 | children: null, 69 | getBackValue: null, 70 | hasKeyDownEvent: false, 71 | inheritedValue: '', 72 | inputClassName: null, 73 | inputType: 'text', 74 | isDisabled: false, 75 | keyDownEvent: null, 76 | labelText: '', 77 | onFocusOut: () => {}, 78 | placeholder: '', 79 | } 80 | 81 | Input.propTypes = { 82 | children: PropTypes.node, 83 | getBackValue: PropTypes.func, 84 | hasKeyDownEvent: PropTypes.bool, 85 | inheritedValue: PropTypes.string, 86 | inputClassName: PropTypes.string, 87 | inputType: PropTypes.string, 88 | isDisabled: PropTypes.bool, 89 | keyDownEvent: PropTypes.func, 90 | labelText: PropTypes.string, 91 | onFocusOut: PropTypes.func, 92 | placeholder: PropTypes.string, 93 | } 94 | 95 | export default Input 96 | -------------------------------------------------------------------------------- /src/components/Main/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { v4 as uuidv4 } from 'uuid' 4 | import { Row, Container } from 'react-grid-system' 5 | import shortid from 'shortid' 6 | import random from 'random' 7 | 8 | import EditorSidebar from '../EditorSidebar' 9 | import ActionSidebar from '../ActionSidebar' 10 | import Preview from '../Preview' 11 | import styles from './Main.module.scss' 12 | import { setPrompterSlug, getAllUserPrompter, clearPrompterObject } from '../../store/actions/prompter' 13 | import { toggleMirror } from '../../store/actions/text' 14 | import { toggleUpdateBtn } from '../../store/actions/misc' 15 | import ActionHeader from '../ActionHeader' 16 | import { setSegments } from '../../store/actions/segments' 17 | import { colors, SEGMENT } from '../../utils/consts' 18 | import RootContext from './rootContext' 19 | 20 | /** 21 | * @author zilahir 22 | * @function Main 23 | * */ 24 | 25 | const Main = () => { 26 | const dispatch = useDispatch() 27 | const [textPreview, setTextPreview] = useState('') 28 | const { user } = useSelector(state => state) 29 | useEffect(() => { 30 | Promise.all([ 31 | dispatch(setSegments([{ 32 | segmentTitle: '', 33 | segmentText: '', 34 | segmentColor: colors[random.int(0, colors.length - 1)], 35 | id: shortid.generate(), 36 | type: SEGMENT.toLowerCase(), 37 | }])), 38 | dispatch(toggleMirror(false)), 39 | dispatch(clearPrompterObject()), 40 | dispatch(toggleUpdateBtn(false)), 41 | dispatch(setPrompterSlug(uuidv4().split('-')[0])), 42 | ]).then(() => { 43 | if (user.loggedIn) { 44 | dispatch(getAllUserPrompter(user.user.userId)) 45 | } 46 | }) 47 | }, []) 48 | return ( 49 | <> 50 |
51 | 52 | 56 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | ) 74 | } 75 | 76 | export default Main 77 | -------------------------------------------------------------------------------- /src/components/Password/Password.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .passwordRecoveryWrapper { 5 | overflow: hidden; 6 | 7 | .dark { 8 | background: rgba($color: $gray-2, $alpha: 1.0); 9 | height: 100vh; 10 | } 11 | 12 | .middle { 13 | background: rgba($color: $gray-3, $alpha: 1); 14 | padding: 50px 20px; 15 | height: 100vh; 16 | 17 | .titleContainer { 18 | .logo { 19 | padding-bottom: 100px; 20 | } 21 | 22 | max-width: 620px; 23 | margin-left: auto; 24 | margin-right: auto; 25 | } 26 | 27 | .inputContainer { 28 | display: flex; 29 | justify-content: space-between; 30 | flex-direction: row; 31 | max-width: 620px; 32 | margin-left: auto; 33 | margin-right: auto; 34 | } 35 | .buttonContainer { 36 | padding-top: 0; 37 | max-width: 620px; 38 | margin-left: auto; 39 | margin-right: auto; 40 | 41 | button { 42 | height: 45px; 43 | } 44 | } 45 | 46 | .info { 47 | max-width: 620px; 48 | margin-left: auto; 49 | margin-right: auto; 50 | 51 | svg { 52 | stroke: #ffffff; 53 | } 54 | 55 | &.hidden { 56 | display: none; 57 | } 58 | 59 | &.success { 60 | background: rgba($color: $success, $alpha: 0.9); 61 | } 62 | 63 | &.warning { 64 | background: rgba($color: $warning, $alpha: 0.9); 65 | } 66 | 67 | &.error { 68 | background: rgba($color: $error, $alpha: 0.9); 69 | } 70 | 71 | display: flex; 72 | align-items: center; 73 | padding: 20px; 74 | border-radius: $border-radius; 75 | p { 76 | @include paragraph(14px, #ffffff, 0.42px); 77 | margin-left: 10px; 78 | } 79 | } 80 | } 81 | 82 | p { 83 | @include paragraph(15px, #ffffff, 0.39px); 84 | line-height: 1.7; 85 | margin: 10px 0; 86 | ul { 87 | list-style-type: none; 88 | } 89 | 90 | a { 91 | color: $purple; 92 | text-decoration: none; 93 | 94 | &:hover { 95 | cursor: pointer; 96 | text-decoration: underline; 97 | } 98 | } 99 | } 100 | 101 | h1 { 102 | @include paragraph(25px, #ffffff, 0.39px); 103 | line-height: 1.7; 104 | margin: 10px 0; 105 | font-weight: 700; 106 | } 107 | } -------------------------------------------------------------------------------- /src/components/common/Button/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import React from 'react' 3 | import classnames from 'classnames' 4 | import PropTypes from 'prop-types' 5 | 6 | import { BUTTON, LINK } from '../../../utils/consts' 7 | import styles from './Button.module.scss' 8 | 9 | /** 10 | * @author zilahir 11 | * @function Button 12 | * */ 13 | 14 | const Button = props => { 15 | const { 16 | labelText, 17 | onClick, 18 | type, 19 | buttonClass, 20 | isNegative, 21 | disabled, 22 | isVisible, 23 | icon, 24 | } = props 25 | return ( 26 | <> 27 | { 28 | type === BUTTON && isVisible 29 | ? ( 30 |
35 | 55 |
56 | ) 57 | : type === LINK && isVisible 58 | ? ( 59 |
60 | 68 |
69 | ) 70 | : null 71 | } 72 | 73 | ) 74 | } 75 | 76 | Button.defaultProps = { 77 | buttonClass: null, 78 | disabled: false, 79 | icon: null, 80 | isNegative: false, 81 | isVisible: true, 82 | type: BUTTON, 83 | } 84 | 85 | Button.propTypes = { 86 | buttonClass: PropTypes.string, 87 | disabled: PropTypes.bool, 88 | icon: PropTypes.node, 89 | isNegative: PropTypes.bool, 90 | isVisible: PropTypes.oneOfType([ 91 | PropTypes.bool, 92 | PropTypes.number, 93 | ]), 94 | labelText: PropTypes.string.isRequired, 95 | onClick: PropTypes.func.isRequired, 96 | type: PropTypes.string, 97 | } 98 | 99 | export default Button 100 | -------------------------------------------------------------------------------- /src/components/common/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | import ReactDOM from 'react-dom' 4 | import styled from 'styled-components' 5 | import propTypes from 'prop-types' 6 | import CloseIcon from '@material-ui/icons/Close' 7 | 8 | import ModalStyle from './Modal.module.scss' 9 | 10 | const ModalOverlay = styled.div` 11 | background: ${props => props.overlayColor}; 12 | ` 13 | 14 | const Modal = ( 15 | { 16 | isShowing, 17 | hide, 18 | children, 19 | modalClassName, 20 | overlayClassName, 21 | modalTitle, 22 | overlayColor, 23 | hasCloseIcon, 24 | selector, 25 | wrapperClassname, 26 | }, 27 | ) => (isShowing ? ReactDOM.createPortal( 28 | <> 29 | 34 |
44 |
49 |
50 | { 51 | modalTitle 52 | ? ( 53 |

54 | {modalTitle} 55 |

56 | ) 57 | : null 58 | } 59 | { 60 | hasCloseIcon 61 | ? ( 62 | 71 | ) 72 | : null 73 | } 74 |
75 | {children} 76 |
77 |
78 | , selector, 79 | ) : null) 80 | 81 | Modal.defaultProps = { 82 | hasCloseIcon: true, 83 | modalClassName: null, 84 | modalTitle: null, 85 | overlayClassName: ModalStyle.modalOverlay, 86 | overlayColor: null, 87 | selector: document.body, 88 | wrapperClassname: null, 89 | } 90 | 91 | Modal.propTypes = { 92 | hasCloseIcon: propTypes.bool.isRequired, 93 | modalClassName: propTypes.string, 94 | modalTitle: propTypes.string, 95 | overlayClassName: propTypes.string, 96 | overlayColor: propTypes.string, 97 | selector: propTypes.string, 98 | wrapperClassname: propTypes.string, 99 | } 100 | 101 | export default Modal 102 | -------------------------------------------------------------------------------- /src/components/Player/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React, { useState, useEffect } from 'react' 3 | import { useParams } from 'react-router-dom' 4 | import ReactGA from 'react-ga' 5 | import { useSelector } from 'react-redux' 6 | import { useSocket } from '@zilahir/use-socket.io-client' 7 | 8 | import TextScroller from '../TextScroller' 9 | import Loader from '../Loader' 10 | import Header from './Header' 11 | import { PLAYER } from '../../utils/consts' 12 | import { getPrompterBySlug } from '../../store/actions/prompter' 13 | 14 | /** 15 | * @author zilahir 16 | * @function Player 17 | * */ 18 | 19 | const Player = () => { 20 | ReactGA.pageview(`/${PLAYER}`) 21 | const [socket] = useSocket(process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:5000' : process.env.REACT_APP_BACKEND_V2) 22 | const [isLoading, toggleIsLoading] = useState(false) 23 | const [isUpdateBtnVisible, toggleUpdateBtn] = useState(false) 24 | const [prompterObject, setPrompterObject] = useState(undefined) 25 | const [segments, setSegments] = useState([]) 26 | const [updatedPrompterObject, updatePrompterObject] = useState({}) 27 | const { slug } = useParams() 28 | const { text } = useSelector(store => store) 29 | 30 | useEffect(() => { 31 | toggleIsLoading(true) 32 | getPrompterBySlug(slug).then(result => { 33 | if (result.isSuccess) { 34 | setPrompterObject(result.prompter.meta) 35 | setSegments(result.prompter.segments) 36 | } 37 | toggleIsLoading(false) 38 | }) 39 | }, []) 40 | 41 | if (socket) { 42 | socket.on('updatePrompter', updatedPrompter => { 43 | if (updatedPrompter.slug === slug) { 44 | toggleUpdateBtn(true) 45 | updatePrompterObject(updatedPrompter) 46 | } 47 | }) 48 | } 49 | 50 | function handleUpdate() { 51 | console.log('updatedPrompterObject', updatedPrompterObject) 52 | setPrompterObject(updatedPrompterObject.meta) 53 | setSegments(updatedPrompterObject.segments) 54 | toggleUpdateBtn(false) 55 | } 56 | 57 | return ( 58 | <> 59 |
handleUpdate()} 62 | /> 63 |
64 | { 65 | !isLoading && prompterObject 66 | ? ( 67 | 73 | ) 74 | : 75 | } 76 |
77 | 78 | ) 79 | } 80 | 81 | export default Player 82 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '33 4 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /src/components/ForgottenPasswordModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { v4 as uuidv4 } from 'uuid' 4 | 5 | import Modal from '../common/Modal' 6 | import styles from './ForgottenPasswordModal.module.scss' 7 | import Input from '../common/Input' 8 | import Button from '../common/Button' 9 | import { requestPasswordRecovery, getToken, sendPasswordRecoveryEmail } from '../../store/actions/user' 10 | import Loader from '../Loader' 11 | import { INLINE_LOADER } from '../../utils/consts' 12 | 13 | /** 14 | * @author zilahir 15 | * @function ForgottenPasswordModal 16 | * */ 17 | 18 | const ForgottenPasswordModal = props => { 19 | const { showPasswordModal, requestClose } = props 20 | const [email, setEmail] = useState(null) 21 | const [isEmailsent, toggleEmailSent] = useState(false) 22 | const [isLoading, togleLoading] = useState(false) 23 | 24 | function sendForgottenPasswordEmail() { 25 | const slug = uuidv4().split('-')[0] 26 | togleLoading(isLoading) 27 | const requestPassword = requestPasswordRecovery(slug, email) 28 | requestPassword.then(() => { 29 | const token = getToken(email) 30 | token.then(tokenRes => { 31 | const sendEmail = sendPasswordRecoveryEmail(email, slug, tokenRes.token) 32 | sendEmail.then(() => { 33 | toggleEmailSent(true) 34 | }) 35 | }) 36 | }) 37 | } 38 | 39 | return ( 40 | <> 41 | 47 | { 48 | !isEmailsent ? ( 49 | <> 50 |

51 | Enter your email address, and we will send a recovery email 52 |

53 |
54 | setEmail(v)} 58 | /> 59 |
60 |
61 |
74 | 75 | ) : ( 76 |
77 |

We have sent you an email. Check your inbox!

78 |
83 | ) 84 | } 85 |
86 | 87 | ) 88 | } 89 | 90 | ForgottenPasswordModal.propTypes = { 91 | requestClose: PropTypes.func.isRequired, 92 | showPasswordModal: PropTypes.bool.isRequired, 93 | } 94 | 95 | export default ForgottenPasswordModal 96 | -------------------------------------------------------------------------------- /src/components/Preview/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useDispatch } from 'react-redux' 3 | import shortid from 'shortid' 4 | import { Col } from 'react-grid-system' 5 | import random from 'random' 6 | import PostAddIcon from '@material-ui/icons/PostAdd' 7 | import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd' 8 | import classnames from 'classnames' 9 | 10 | import { colors, SEGMENT, BREAK } from '../../utils/consts' 11 | import TextEditor from '../TextEditor' 12 | import styles from './Preview.module.scss' 13 | import { addSegment } from '../../store/actions/segments' 14 | 15 | /** 16 | * @author zilahir 17 | * @function Preview 18 | * */ 19 | 20 | const Preview = () => { 21 | const [activeButton, setActiveButton] = useState(1) 22 | const dispatch = useDispatch() 23 | 24 | function handleNewSegment(type) { 25 | dispatch(addSegment({ 26 | segmentTitle: '', 27 | segmentText: '', 28 | segmentColor: colors[random.int(0, colors.length - 1)], 29 | id: shortid.generate(), 30 | type: type.toLowerCase(), 31 | })) 32 | } 33 | return ( 34 | <> 35 | 39 |
40 |
41 |
42 | 52 | 62 |
63 |
64 |
65 | 77 | 89 |
90 | 91 |
92 |
93 |
94 | 95 | 96 | ) 97 | } 98 | 99 | export default Preview 100 | -------------------------------------------------------------------------------- /src/components/Segment/Segment.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .oneSegment { 5 | display: flex; 6 | padding: 5px; 7 | border-radius: 10px; 8 | border-width: 2px; 9 | border-style: solid; 10 | flex-direction: column; 11 | position: relative; 12 | 13 | &:hover { 14 | cursor: pointer; 15 | } 16 | 17 | .segmentHeader { 18 | margin-top: 4px; 19 | 20 | .segmentName { 21 | border-radius: $border-radius; 22 | margin: 0; 23 | height: 30px; 24 | display: flex; 25 | flex: 1; 26 | margin-right: 10px; 27 | 28 | label { 29 | height: 100%; 30 | width: 100%; 31 | input { 32 | @include paragraph(14px, #ffffff, 0.42px); 33 | width: 100%; 34 | background: rgba($color: $gray-2, $alpha: 1); 35 | padding: 10px; 36 | } 37 | } 38 | } 39 | 40 | display: flex; 41 | justify-content: space-between; 42 | 43 | ul { 44 | list-style-type: none; 45 | margin: 0; 46 | padding: 0; 47 | width: 70px; 48 | display: flex; 49 | justify-content: flex-end; 50 | align-items: center; 51 | position: relative; 52 | top: 3px; 53 | 54 | li { 55 | display: flex; 56 | align-items: center; 57 | 58 | .segmentColorIndicator { 59 | width: 16px; 60 | height: 16px; 61 | display: block; 62 | border-radius: 100%; 63 | margin: 0 10px; 64 | } 65 | 66 | .deleteBtn { 67 | color: #ffffff; 68 | background: none; 69 | box-shadow: none; 70 | border: none; 71 | margin: 0; 72 | padding: 0; 73 | font-size: unset; 74 | width: 24px; 75 | height: 24px; 76 | 77 | &:hover { 78 | cursor: pointer; 79 | } 80 | 81 | &:focus { 82 | outline: none; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | .segmentBody { 90 | margin-top: 10px; 91 | 92 | .segmentText { 93 | @include paragraph(14px, #ffffff, 0.42px) 94 | resize: none; 95 | border: 0; 96 | background: rgba($color: $gray-2, $alpha: 1); 97 | width: calc(100% - 20px); 98 | padding: 10px; 99 | min-height: fit-content; 100 | border-radius: $border-radius; 101 | 102 | &:focus { 103 | outline: none; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/components/UserSettingsModal/UserSettingsModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | @import "../../styles/mixins/paragraph"; 3 | @import "../../styles/mixins/heading"; 4 | 5 | .userSettingsModal { 6 | width: 520px; 7 | 8 | 9 | .topContainer { 10 | display: flex; 11 | justify-content: center; 12 | flex-direction: column; 13 | 14 | h1 { 15 | @include heading(); 16 | font-size: 25px; 17 | letter-spacing: 0.75px; 18 | text-align: center; 19 | margin: 10px 0; 20 | margin-bottom: 0; 21 | color: #ffffff; 22 | } 23 | 24 | h2 { 25 | @include heading(); 26 | font-size: 15px; 27 | letter-spacing: 0.75px; 28 | text-align: center; 29 | margin: 10px 0; 30 | margin-top: 0; 31 | font-weight: 400; 32 | color: #ffffff; 33 | } 34 | 35 | p { 36 | @include paragraph(14px, #ffffff, 0.42px); 37 | text-align: center; 38 | margin: 0; 39 | padding: 0; 40 | &:hover { 41 | color: $purple; 42 | cursor: pointer; 43 | } 44 | } 45 | 46 | .info { 47 | &.hidden { 48 | display: none; 49 | } 50 | 51 | &.success { 52 | background: rgba($color: $success, $alpha: 0.9); 53 | } 54 | 55 | &.warning { 56 | background: rgba($color: $warning, $alpha: 0.9); 57 | } 58 | 59 | &.error { 60 | background: rgba($color: $error, $alpha: 0.9); 61 | } 62 | 63 | display: flex; 64 | align-items: center; 65 | padding: 10px; 66 | border-radius: $border-radius; 67 | p { 68 | @include paragraph(14px, #ffffff, 0.42px); 69 | margin-left: 10px; 70 | } 71 | } 72 | } 73 | .footerBtnContainer { 74 | display: flex; 75 | justify-content: space-between; 76 | margin-top: 40px; 77 | .btnClass { 78 | button { 79 | height: 45px; 80 | } 81 | } 82 | } 83 | 84 | .inputContainer { 85 | input { 86 | border: 2px solid transparent; 87 | } 88 | 89 | .newUsername { 90 | input { 91 | border: 2px solid $orange; 92 | } 93 | } 94 | .newPasswordContainer { 95 | display: flex; 96 | justify-content: space-between; 97 | } 98 | 99 | p { 100 | @include paragraph(14px, #fff, 0.39px); 101 | margin: 0; 102 | } 103 | 104 | .settingsInput { 105 | margin: 10px 0; 106 | } 107 | } 108 | 109 | .deleteContainer { 110 | margin: 20px 0; 111 | p { 112 | @include paragraph(14px, #fff, 0.39px); 113 | } 114 | 115 | .deleteInner { 116 | .checkBoxContainer { 117 | display: flex; 118 | justify-content: center; 119 | align-items: center; 120 | position: relative; 121 | top: 5px; 122 | } 123 | 124 | display: flex; 125 | justify-content: space-between; 126 | align-items: center; 127 | 128 | p { 129 | margin-left: 10px; 130 | } 131 | .settingsInput { 132 | margin: 10px 0; 133 | } 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/components/TextEditor/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | /* eslint-disable react/jsx-props-no-spreading */ 3 | import React, { useState, useEffect } from 'react' 4 | import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' 5 | import { useStore, useDispatch } from 'react-redux' 6 | 7 | import segmentsApi from '../../utils/fakeApi/segments' 8 | import Segment from '../Segment' 9 | import { setSegments } from '../../store/actions/segments' 10 | import { SEGMENT } from '../../utils/consts' 11 | import Break from '../common/Break' 12 | 13 | const TextEditor = () => { 14 | const reorder = (list, startIndex, endIndex) => { 15 | const result = Array.from(list) 16 | const [removed] = result.splice(startIndex, 1) 17 | result.splice(endIndex, 0, removed) 18 | 19 | return result 20 | } 21 | 22 | const [segments, setAllSegments] = useState(segmentsApi.getAllSegments()) 23 | const store = useStore() 24 | const dispatch = useDispatch() 25 | 26 | function onDragEnd(result) { 27 | if (!result.destination) { 28 | return 29 | } 30 | 31 | const newItems = reorder( 32 | segments, 33 | result.source.index, 34 | result.destination.index, 35 | ) 36 | 37 | setAllSegments(newItems) 38 | dispatch(setSegments(newItems)) 39 | } 40 | 41 | const grid = 8 42 | 43 | const getItemStyle = (isDragging, draggableStyle) => ({ 44 | userSelect: 'none', 45 | padding: `${grid}px 0`, 46 | margin: `0 0 ${grid}px 0`, 47 | background: isDragging ? 'transport' : 'transport', 48 | ...draggableStyle, 49 | }) 50 | 51 | useEffect(() => store.subscribe(() => { 52 | const currentSegmentList = store.getState().segments.segments 53 | setAllSegments(currentSegmentList) 54 | }), [store]) 55 | 56 | return ( 57 | onDragEnd(result)}> 58 | 59 | {provided => ( 60 |
64 | {segments.map((currSegment, index) => ( 65 | 66 | {(provided, snapshot) => ( 67 |
76 | { 77 | currSegment.type === SEGMENT.toLowerCase() ? ( 78 | 85 | ) : 86 | } 87 |
88 | )} 89 |
90 | ))} 91 | {provided.placeholder} 92 |
93 | )} 94 |
95 |
96 | ) 97 | } 98 | 99 | export default TextEditor 100 | -------------------------------------------------------------------------------- /src/components/Preview/Preview.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .segmentsHeader { 5 | display: flex; 6 | align-items: center; 7 | margin-bottom: 20px; 8 | justify-content: flex-start; 9 | .button { 10 | background: transparent; 11 | border: none; 12 | box-shadow: none; 13 | width: fit-content; 14 | display: flex; 15 | align-items: center; 16 | padding-left: 0; 17 | 18 | &:last-of-type { 19 | @include paragraph(14px, #fff); 20 | font-weight: 400; 21 | margin-left: 30px; 22 | } 23 | 24 | &:focus { 25 | outline: none; 26 | } 27 | 28 | &:hover { 29 | p, svg { 30 | text-decoration: none; 31 | cursor: pointer; 32 | color: $purple; 33 | transition: all .2s ease; 34 | } 35 | } 36 | } 37 | 38 | .addPrompterIcon { 39 | color: #ffffff; 40 | width: 24px; 41 | height: 24px; 42 | } 43 | 44 | p { 45 | @include paragraph(15px, #fff, 0); 46 | margin-left: 5px; 47 | } 48 | } 49 | 50 | .previewContainer { 51 | display: flex; 52 | flex-direction: column; 53 | flex: 1; 54 | overflow-x: hidden; 55 | overflow-y: auto; 56 | 57 | .tabContainer { 58 | display: none; 59 | position: relative; 60 | margin: 30px; 61 | 62 | &:after { 63 | content: ''; 64 | width: 100%; 65 | height: 2px; 66 | background-color: $purple; 67 | position: absolute; 68 | bottom: 0; 69 | left: 0; 70 | } 71 | 72 | .tabButton { 73 | border: none; 74 | background: transparent; 75 | @include paragraph(15px, #fff); 76 | height: 50px; 77 | padding: 8px 50px; 78 | margin-right: 10px; 79 | 80 | &:focus { 81 | outline: none; 82 | } 83 | 84 | &.tabButtonActive { 85 | border-top-left-radius: 15px; 86 | border-top-right-radius: 15px; 87 | border-width: 2px; 88 | border: 2px solid $purple; 89 | border-bottom: 0; 90 | border-style: solid; 91 | background-color: $purple; 92 | color: #fff; 93 | } 94 | } 95 | } 96 | .innerContainer { 97 | margin: 0 40px; 98 | padding: 20px 0; 99 | } 100 | 101 | &::-webkit-scrollbar-thumb { 102 | background: #3C3F48; 103 | border-radius: 20px; 104 | position: relative; 105 | } 106 | &::-webkit-scrollbar { 107 | width: 6px; 108 | } 109 | &::-webkit-scrollbar-track { 110 | margin: 30px 0; 111 | } 112 | } 113 | 114 | .previewRoot { 115 | background: rgba($color: $gray-3, $alpha: 1); 116 | display: flex; 117 | flex-direction: column; 118 | overflow-y: auto; 119 | } 120 | 121 | .innerContainer { 122 | align-self: stretch; 123 | display: flex; 124 | flex-direction: column; 125 | flex: 1; 126 | padding-bottom: 100px; 127 | } -------------------------------------------------------------------------------- /src/components/About/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactGA from 'react-ga' 3 | import PropTypes from 'prop-types' 4 | import classnames from 'classnames' 5 | import { Row, Container, Col } from 'react-grid-system' 6 | import CloseIcon from '@material-ui/icons/Close' 7 | 8 | import styles from '../Policy/Policy.module.scss' 9 | import { ABOUT } from '../../utils/consts' 10 | 11 | /** 12 | * @author zilahir 13 | * @function About 14 | * */ 15 | 16 | const About = ({ 17 | onClose, 18 | }) => { 19 | ReactGA.pageview(`${ABOUT}`) 20 | return ( 21 |
26 | 29 | 30 | 34 |
35 | 42 |
43 |
44 |

45 | What is Prompter.me? 46 |

47 |

48 | Prompter.me is a free, open source teleprompter on the web. Using it doesn't 49 | require you to download anything or to sign up for anything. 50 | It was made to give content creators an actually useful free teleprompter, 51 | which would allow them to use it on their own without any additional apps or hacks. 52 | After all, a lot of video content creators out there are one-person operations, 53 | and we know using a prompter without help can be a real pain in the tuchus. 54 |

55 |

56 | Prompter.me was designed by Mikko Oittinen and developed by Richard Zilahi. 57 |

58 |

59 | Mikko is a Finnish designer and content creator, who knows the pain of using 60 | bad prompters all too well. Mikko is a graphic designer by trade, and you 61 | canlook at (and buy) some of his work at Club Camomile, an online store for 62 | sustainable and ethical print streetwear he co-founded. For his occasional videos 63 | (which almost always utilize teleprompters), check out his YouTube channel 64 | Expert Opinion. 65 |

66 |

67 | Richard is a fullstack developer originally from Hungary, 68 | now living in Finland. Richard is enthusiastic about modern stacks, 69 | clean code and open source technology. Interested in big 70 | data and deep data analysis. 71 | Richard is the co-host of the 72 | podcast "Szauna Szenátus", in Hungarian language. 73 | Check out some of Richard's work on his GitHub page and his website. 74 |

75 |

76 | Legal stuff 77 |

78 |

79 | Read our privacy policy. 80 |

81 |
82 | 83 |
84 |
85 |
86 | ) 87 | } 88 | 89 | About.propTypes = { 90 | onClose: PropTypes.func.isRequired, 91 | } 92 | 93 | export default About 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Netlify Status](https://api.netlify.com/api/v1/badges/80ef0317-b439-43a0-baf8-cd7646408ee4/deploy-status)](https://app.netlify.com/sites/prompterme/deploys) ![Dependabot](https://badgen.net/dependabot/zilahir/teleprompter?icon=dependabot) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 2 | 3 | 4 |

5 | 6 | Prompter.me 9 | 10 |

11 |

12 | An opensource teleprompter application for the browser. 13 |

14 | 15 | # Teleprompter 16 | 17 | ![preview](./images/preview1.png) 18 | 19 | ## What is Prompter.me? 20 | 21 | Prompter.me is a _free_, _open source_ teleprompter on the web. Using it doesn't require you to download anything or to sign up for anything. It was made to give content creators an actually useful free teleprompter, which would allow them to use it on their own without any additional apps or hacks. After all, a lot of video content creators out there are one-person operations, and we know using a prompter without help can be a real pain in the tuchus. 22 | 23 | ## Server 24 | 25 | The project's server side repository can be found [here](https://github.com/zilahir/teleprompter-server). 26 | 27 | The server is currently deployed to `AWS – Lambda`. You can crate your own instane if you wish to have a _self-hosted_ version of prompter. Or anywhere else if you wish, you need a `node` environment, and a `MongoDB`. 28 | 29 | ## Contributors 30 | 31 | - :nail_care: _design_: Mikko Oitinen 🇫🇮 ([design](https://xd.adobe.com/view/614443a6-af97-49a7-603e-e82e2c667a77-1775/)) 32 | - :computer: _dev_: Richard Zilahi [http](https://richardzilahi.hu) 🇭🇺 33 | 34 | ## Project dependencies 35 | 36 | This project heavily depends on the following `open source` projects. 37 | 38 | 1. [`redux`](https://github.com/reduxjs/redux) 39 | 2. [`react-redux`](https://github.com/reduxjs/react-redux) 40 | 3. [`redux-thunk`](https://github.com/reduxjs/redux-thunk) 41 | 4. [`redux-persist`](https://github.com/rt2zz/redux-persist) 42 | 5. [`styled-components`](https://github.com/styled-components) 43 | 6. [`use-socket.io-client`](https://github.com/iamgyz/use-socket.io-client) 44 | 7. [`uuid`](https://github.com/uuidjs/uuid) 45 | 8. [`classnames`](https://github.com/JedWatson/classnames) 46 | 9. [`prop-tpyes`](https://github.com/facebook/prop-types) 47 | 10. [`hex-to-rgba`](https://github.com/misund/hex-to-rgba) 48 | 11. [... and a lot other](https://github.com/zilahir/teleprompter/blob/master/package.json) 49 | 50 | ## Developing 51 | 52 | Contribution is very welcome! You need to clone this, and the [server](https://github.com/zilahir/teleprompter-server) repository. 53 | 54 | For the client: 55 | 56 | 1. `npm i` 57 | 2. `npm run start` 58 | 3. The client is listening on `:4444` 59 | 60 | For the server: 61 | 62 | 1. `npm install` 63 | 2. `npm run dev` 64 | 3. The server is listening on `:5000` 65 | 66 | ## Misc 67 | 68 | This project is deployed via [netlify](https://netlify.com). 69 | 70 | Special thanks to [@munkacsimark](https://github.com/munkacsimark/) for helping me out with the scrolling function. :wave: 71 | 72 | [This](https://open.spotify.com/track/6ULAF7fV7JPQPPHz1aP3vc?si=M8ieNoCJR6Ob8JatZ8JEAA) is most listened song during the development of [promoter.me](https://http://prompter.me/). 73 | 74 | ## Licence 75 | 76 | This is a _free_ product, and it's licenced under `BSD 3-Clause License`. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at info@prompter.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teleprompter", 3 | "version": "2.1.0", 4 | "private": true, 5 | "author": { 6 | "name": "Richard Zilahi", 7 | "email": "zilahi@gmail.com", 8 | "url": "https://richardzilahi.hu" 9 | }, 10 | "engines": { 11 | "node": ">=12 < 15" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome": "^1.1.8", 15 | "@fortawesome/fontawesome-free-brands": "^5.0.13", 16 | "@fortawesome/react-fontawesome": "0.0.20", 17 | "@material-ui/core": "^4.11.0", 18 | "@material-ui/icons": "^4.9.1", 19 | "@popmotion/popcorn": "^0.4.4", 20 | "@zilahir/use-socket.io-client": "^1.2.0", 21 | "array-move": "^3.0.1", 22 | "axios": "^0.21.1", 23 | "classnames": "^2.2.6", 24 | "env-cmd": "^10.1.0", 25 | "framer-motion": "^4.0.3", 26 | "hex-to-rgba": "^2.0.1", 27 | "lodash": "^4.17.20", 28 | "moment": "^2.24.0", 29 | "prop-types": "^15.7.2", 30 | "random": "^2.2.0", 31 | "rc-slider": "^9.2.3", 32 | "react": "^17.0.1", 33 | "react-autosize-textarea": "^7.1.0", 34 | "react-beautiful-dnd": "^13.1.0", 35 | "react-color": "^2.18.1", 36 | "react-copy-to-clipboard": "^5.0.2", 37 | "react-device-detect": "^1.17.0", 38 | "react-dom": "^16.13.1", 39 | "react-ga": "^3.3.0", 40 | "react-grid-system": "^7.1.2", 41 | "react-helmet": "^6.0.0", 42 | "react-icons-kit": "^1.3.1", 43 | "react-intersection-observer": "^8.26.1", 44 | "react-keyboard-event-handler": "^1.5.4", 45 | "react-loading": "^2.0.3", 46 | "react-mde": "^11.0.6", 47 | "react-redux": "^7.1.3", 48 | "react-router": "^5.1.2", 49 | "react-router-dom": "^5.1.2", 50 | "react-scripts": "^4.0.3", 51 | "react-scroll": "^1.7.9", 52 | "react-svg-morph": "^0.2.1", 53 | "react-toggle": "^4.1.2", 54 | "react-waypoint": "^9.0.3", 55 | "redux": "^4.0.5", 56 | "redux-devtools-extension": "^2.13.8", 57 | "redux-persist": "^6.0.0", 58 | "redux-thunk": "^2.3.0", 59 | "shortid": "^2.2.15", 60 | "styled-components": "^5.2.0", 61 | "uuid": "^8.3.0" 62 | }, 63 | "scripts": { 64 | "cm": "cz", 65 | "start": "CI=false PORT=4444 react-scripts start", 66 | "build": "react-scripts build", 67 | "build-production": "env-cmd -f config/.env.prod react-scripts build", 68 | "test": "react-scripts test", 69 | "eject": "react-scripts eject", 70 | "format": "prettier --write", 71 | "lint:js": "eslint --ext .js,.jsx .", 72 | "lint:scss": "stylelint" 73 | }, 74 | "eslintConfig": { 75 | "extends": "react-app" 76 | }, 77 | "browserslist": { 78 | "production": [ 79 | ">0.2%", 80 | "not dead", 81 | "not op_mini all" 82 | ], 83 | "development": [ 84 | "last 1 chrome version", 85 | "last 1 firefox version", 86 | "last 1 safari version" 87 | ] 88 | }, 89 | "devDependencies": { 90 | "@semantic-release/changelog": "^5.0.1", 91 | "@semantic-release/commit-analyzer": "^8.0.1", 92 | "@semantic-release/git": "^9.0.0", 93 | "@semantic-release/npm": "github:semantic-release/npm", 94 | "@semantic-release/release-notes-generator": "^9.0.2", 95 | "@typescript-eslint/parser": "^4.19.0", 96 | "@zilahir/eslint-config": "^1.0.1", 97 | "@zilahir/stylelint-config": "^3.0.7", 98 | "commitizen": "^4.2.3", 99 | "conventional-changelog-conventionalcommits": "^4.5.0", 100 | "cz-conventional-changelog": "^3.3.0", 101 | "eslint-config-airbnb": "^18.0.1", 102 | "eslint-config-prettier": "^8.1.0", 103 | "eslint-plugin-import": "^2.22.0", 104 | "eslint-plugin-jsx-a11y": "^6.2.3", 105 | "eslint-plugin-promise": "^4.2.1", 106 | "eslint-plugin-react": "^7.20.6", 107 | "husky": "^5.1.3", 108 | "lint-staged": "^10.4.2", 109 | "node-sass": "^4.14.1", 110 | "prettier": "^2.2.1", 111 | "sass-loader": "^10.0.2", 112 | "semantic-release": "^17.4.2", 113 | "yarn": "^1.17.3" 114 | }, 115 | "lint-staged": { 116 | "*.{js,jsx}": [ 117 | "yarn lint:js", 118 | "git add" 119 | ] 120 | }, 121 | "eslint-plugin-jsx-a11y": "^6.2.3" 122 | } 123 | -------------------------------------------------------------------------------- /src/store/actions/user.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { AUTH_USER, REMOVE_USER } from './actionTypes' 4 | import { apiEndpoints } from '../../utils/apiEndpoints' 5 | 6 | const headers = { 7 | 'Content-Type': 'application/json', 8 | } 9 | 10 | export const setUser = user => dispatch => new Promise(resolve => { 11 | dispatch({ 12 | type: AUTH_USER, 13 | payload: { 14 | user, 15 | }, 16 | }) 17 | resolve(user) 18 | }) 19 | 20 | export const removeUser = () => dispatch => new Promise(resolve => { 21 | dispatch({ 22 | type: REMOVE_USER, 23 | payload: {}, 24 | }) 25 | resolve(true) 26 | }) 27 | 28 | export const authUser = user => dispatch => new Promise(resolve => { 29 | const authObject = { 30 | email: `${user.email}`, 31 | password: `${user.password}`, 32 | } 33 | axios.post(apiEndpoints.authUser, JSON.stringify(authObject), { 34 | headers, 35 | }) 36 | .then(resp => { 37 | dispatch(setUser(resp.data)) 38 | resolve(resp.data) 39 | }) 40 | }) 41 | 42 | export const refreshToken = token => dispatch => new Promise(resolve => { 43 | axios.get(apiEndpoints.refreshToken, { 44 | params: { 45 | refresh_token: token, 46 | }, 47 | }) 48 | .then(resp => { 49 | dispatch(setUser(resp.data)) 50 | resolve(resp.data) 51 | }) 52 | }) 53 | 54 | export const logOutUser = () => dispatch => new Promise(resolve => { 55 | dispatch(removeUser()) 56 | resolve(true) 57 | }) 58 | 59 | export const modifyPassword = (authToken, userId, newPassword) => new Promise(resolve => { 60 | axios.defaults.headers.common.authorization = `Bearer ${authToken}` 61 | axios.patch(`${apiEndpoints.modifyPassword}/${userId}`, { password: newPassword }, { 62 | headers, 63 | }) 64 | .then(res => { 65 | resolve({ 66 | isSuccess: true, 67 | ...res, 68 | }) 69 | }) 70 | }) 71 | 72 | export const getPasswordResetObject = slug => new Promise(resolve => { 73 | axios.get(`${apiEndpoints.getPasswordRecovery}/${slug}`, { 74 | headers, 75 | }) 76 | .then(res => { 77 | resolve({ 78 | isSuccess: true, 79 | ...res.data, 80 | }) 81 | }) 82 | }) 83 | 84 | export const resetPassword = (newPassword, authToken, userId) => new Promise(resolve => { 85 | axios.defaults.headers.common.authorization = `Bearer ${authToken}` 86 | axios.patch(`${apiEndpoints.resetpassword}/${userId}`, JSON.stringify({ 87 | password: newPassword, 88 | }), { 89 | headers, 90 | }) 91 | .then(res => { 92 | resolve({ 93 | isSuccess: true, 94 | ...res, 95 | }) 96 | }) 97 | }) 98 | 99 | export const requestPasswordRecovery = (slug, email) => new Promise(resolve => { 100 | axios.post(`${apiEndpoints.requestPasswordRecovery}`, JSON.stringify({ 101 | slug, 102 | email, 103 | }), { 104 | headers, 105 | }) 106 | .then(res => { 107 | resolve({ 108 | ...res.data, 109 | }) 110 | }) 111 | }) 112 | 113 | export const sendPasswordRecoveryEmail = (username, slug, token) => new Promise(resolve => { 114 | axios.post(`${apiEndpoints.sendPasswordRecoveryEmail}`, JSON.stringify({ 115 | slug, 116 | username, 117 | token, 118 | }), { 119 | headers, 120 | }) 121 | .then(res => { 122 | resolve({ 123 | ...res.data, 124 | }) 125 | }) 126 | }) 127 | 128 | export const getToken = username => new Promise(resolve => { 129 | axios.post(apiEndpoints.getToken, JSON.stringify({ 130 | username, 131 | }), { 132 | headers, 133 | }) 134 | .then(res => { 135 | resolve({ 136 | ...res.data, 137 | }) 138 | }) 139 | }) 140 | 141 | export const setPasswordRecoveryToUsed = slug => new Promise(resolve => { 142 | axios.patch(`${apiEndpoints.setPasswordRecoveryToUsed}/${slug}`, { 143 | headers, 144 | }) 145 | .then(res => { 146 | resolve({ 147 | ...res.data, 148 | }) 149 | }) 150 | }) 151 | 152 | export const modifyUsername = (authToken, userId, newUsername) => new Promise(resolve => { 153 | axios.defaults.headers.common.authorization = `Bearer ${authToken}` 154 | axios.patch(`${apiEndpoints.modifyUserName}/${userId}`, { username: newUsername }, { 155 | headers, 156 | }) 157 | .then(res => { 158 | resolve({ 159 | isSuccess: true, 160 | ...res, 161 | }) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /src/components/Password/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import ReactGA from 'react-ga' 3 | import { Row, Container, Col } from 'react-grid-system' 4 | import { useParams } from 'react-router-dom' 5 | import classnames from 'classnames' 6 | import { alertTriangle } from 'react-icons-kit/feather/alertTriangle' 7 | import Icon from 'react-icons-kit' 8 | 9 | import styles from './Password.module.scss' 10 | import Button from '../common/Button' 11 | import Input from '../common/Input' 12 | import Logo from '../common/Logo' 13 | import { getPasswordResetObject, resetPassword, setPasswordRecoveryToUsed } from '../../store/actions/user' 14 | import { FORGOTTEN_PW, PASSWORD } from '../../utils/consts' 15 | 16 | /** 17 | * @author zilahir 18 | * @function Password 19 | * */ 20 | 21 | const Password = () => { 22 | const [newPassword, setNewPassword] = useState(null) 23 | const [confirmNewPassword, setConfirmNewpassword] = useState(null) 24 | const [alertMessage, setAlertMessage] = useState({}) 25 | const [isHidden, toggleHidden] = useState(true) 26 | const [userId, setUserId] = useState(null) 27 | const { slug } = useParams() 28 | const { token } = useParams() 29 | 30 | ReactGA.pageview(`${FORGOTTEN_PW}`) 31 | function handlePasswordUpdate() { 32 | const passwordReset = resetPassword(newPassword, token, userId) 33 | passwordReset.then(() => { 34 | setPasswordRecoveryToUsed(slug).then(res => { 35 | if (res.success) { 36 | toggleHidden(true) 37 | setAlertMessage({ 38 | text: 'Your passwsord has been changed!', 39 | state: 'success', 40 | }) 41 | } 42 | }) 43 | }) 44 | } 45 | 46 | useEffect(() => { 47 | const passwordRecoveryRequest = getPasswordResetObject(slug) 48 | passwordRecoveryRequest.then(res => { 49 | if (res.isUsed || res.expiresAt < new Date().getMinutes()) { 50 | setAlertMessage({ 51 | text: 'This password reset had expired', 52 | state: 'error', 53 | }) 54 | } else { 55 | setUserId(res.email) 56 | toggleHidden(false) 57 | } 58 | }) 59 | }, []) 60 | return ( 61 |
62 | 65 | 66 | 67 | 68 |
69 |
70 | 71 | { 72 | isHidden 73 | ? ( 74 |
80 | 81 |

82 | { 83 | alertMessage.text 84 | } 85 |

86 |
87 | ) : null 88 | } 89 |
90 | { 91 | !isHidden 92 | ? ( 93 | <> 94 |
95 |

96 | Password reset 97 |

98 |
99 |
100 | setNewPassword(v)} 103 | inputType={PASSWORD} 104 | labelText="New password" 105 | /> 106 | setConfirmNewpassword(v)} 110 | labelText="Confirm new password" 111 | /> 112 |
113 |
125 | 126 | 127 |
128 |
129 |
130 | ) 131 | } 132 | 133 | export default Password 134 | -------------------------------------------------------------------------------- /src/store/actions/prompter.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { headers } from '../../utils/consts' 4 | import { apiEndpoints } from '../../utils/apiEndpoints' 5 | import { GET_ALL_PROMPTER, SET_PROMPTER_SLUG, SET_PROJECT_NAME, CLEAR_ALL_PROMPTER, COPY_PROMPTER_OBJECT, CLEAR_PROMPTER_OBJECT } from './actionTypes' 6 | 7 | export const setAllPrompterForUser = usersPrompters => dispatch => new Promise(resolve => { 8 | dispatch({ 9 | type: GET_ALL_PROMPTER, 10 | payload: { 11 | usersPrompters, 12 | }, 13 | }) 14 | resolve(usersPrompters) 15 | }) 16 | 17 | export const clearUserPrompters = () => dispatch => new Promise(resolve => { 18 | dispatch({ 19 | type: CLEAR_ALL_PROMPTER, 20 | payload: {}, 21 | }) 22 | resolve({ 23 | success: true, 24 | }) 25 | }) 26 | 27 | export const getAllUserPrompter = (userId, authToken) => dispatch => new Promise(resolve => { 28 | axios.defaults.headers.common.authorization = `Bearer ${authToken}` 29 | axios.get(`${apiEndpoints.getAllPrompterForUser}/${userId}`, { 30 | headers, 31 | }) 32 | .then(resp => { 33 | dispatch(clearUserPrompters()) 34 | dispatch(setAllPrompterForUser(resp.data)) 35 | resolve(resp.data) 36 | }) 37 | }) 38 | 39 | export const setPrompterSlug = prompterSlug => dispatch => new Promise(resolve => { 40 | dispatch({ 41 | type: SET_PROMPTER_SLUG, 42 | payload: { 43 | prompterSlug, 44 | }, 45 | }) 46 | resolve(prompterSlug) 47 | }) 48 | 49 | export const setPrompterProjectName = projectName => dispatch => new Promise(resolve => { 50 | dispatch({ 51 | type: SET_PROJECT_NAME, 52 | payload: { 53 | projectName, 54 | }, 55 | }) 56 | resolve(projectName) 57 | }) 58 | 59 | export const copyPrompterObject = prompterObject => dispatch => new Promise(resolve => { 60 | dispatch({ 61 | type: COPY_PROMPTER_OBJECT, 62 | payload: { 63 | prompterObject, 64 | }, 65 | }) 66 | resolve(prompterObject) 67 | }) 68 | 69 | export const clearPrompterObject = () => dispatch => new Promise(resolve => { 70 | dispatch({ 71 | type: CLEAR_PROMPTER_OBJECT, 72 | payload: {}, 73 | }) 74 | resolve(true) 75 | }) 76 | 77 | export const createNewPrompterNoAuth = ( 78 | newPrompterObject, endPoint, 79 | ) => new Promise(resolve => { 80 | axios.post(`${endPoint}`, newPrompterObject, { 81 | headers, 82 | }) 83 | .then(res => { 84 | resolve({ 85 | isSuccess: true, 86 | ...res, 87 | }) 88 | }) 89 | }) 90 | 91 | export const createNewPrompter = ( 92 | newPrompterObject, authToken, 93 | ) => new Promise(resolve => { 94 | axios.defaults.headers.common.authorization = `Bearer ${authToken}` 95 | axios.post(`${apiEndpoints.newPrompter}`, newPrompterObject, { 96 | headers, 97 | }) 98 | .then(res => { 99 | resolve({ 100 | isSuccess: true, 101 | ...res, 102 | }) 103 | }) 104 | }) 105 | 106 | export const deletePrompter = (idToDel, authToken) => new Promise(resolve => { 107 | axios.defaults.headers.common.authorization = `Bearer ${authToken}` 108 | axios.delete(`${apiEndpoints.delPrompter}/${idToDel}`, { 109 | headers, 110 | }) 111 | .then(res => { 112 | resolve({ 113 | isSuccess: true, 114 | ...res, 115 | }) 116 | }) 117 | }) 118 | 119 | export const updatePrompterNoAuth = updatedPrompterObject => new Promise(resolve => { 120 | axios.patch(`${apiEndpoints.updatePrompterNoAuth}/${updatedPrompterObject.slug}`, updatedPrompterObject, { 121 | headers, 122 | }) 123 | .then(res => { 124 | resolve({ 125 | isSuccess: true, 126 | ...res, 127 | }) 128 | }) 129 | }) 130 | 131 | export const isProverSaved = slug => new Promise(resolve => { 132 | axios.get(`${apiEndpoints.getPrompterBySlug}/${slug}`, { 133 | headers, 134 | }) 135 | .then(res => { 136 | resolve({ 137 | ...res.data, 138 | }) 139 | }) 140 | }) 141 | 142 | export const updatePrompter = updatedPrompterObject => new Promise(resolve => { 143 | axios.patch(`${apiEndpoints.modifyPrompter}/${updatedPrompterObject.slug}`, updatedPrompterObject, { 144 | headers, 145 | }) 146 | .then(res => { 147 | resolve({ 148 | isSuccess: true, 149 | ...res, 150 | }) 151 | }) 152 | }) 153 | 154 | export const getPrompterBySlug = slug => new Promise(resolve => { 155 | axios.get(`${apiEndpoints.getPrompterBySlugNoAuth}/${slug}`, { 156 | headers, 157 | }) 158 | .then(response => { 159 | resolve(response.data) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /src/components/Segment/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 2 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 3 | import React, { useContext, useRef, useState } from 'react' 4 | import PropTypes from 'prop-types' 5 | import styled from 'styled-components' 6 | import { useSelector, useDispatch } from 'react-redux' 7 | import CloseIcon from '@material-ui/icons/Close' 8 | import TextareaAutosize from 'react-autosize-textarea' 9 | 10 | import rootContext from '../Main/rootContext' 11 | import styles from './Segment.module.scss' 12 | import Input from '../common/Input' 13 | import ColorPicker from '../ColorPicker' 14 | import { modifySegment, setSegments } from '../../store/actions/segments' 15 | 16 | /** 17 | * @author zilahir 18 | * @function Segemnt 19 | * */ 20 | 21 | const OnseSegment = styled.div` 22 | border-color: ${props => props.borderColor}; 23 | ` 24 | 25 | const SegmentIndicator = styled.span` 26 | background-color: ${props => props.segmentColor}; 27 | ` 28 | 29 | const Segment = ({ 30 | segmentColor, 31 | segmentTitle, 32 | segmentKey, 33 | segmentId, 34 | }) => { 35 | const thisSegmentRef = useRef(null) 36 | const [isColorPickerOpen, toggleColorPickerOpen] = useState(false) 37 | const dispatch = useDispatch() 38 | const context = useContext(rootContext) 39 | 40 | const thisSegment = useSelector( 41 | state => state.segments.segments.find(segment => segment.id === segmentId), 42 | ) 43 | 44 | const allSegments = useSelector(state => state.segments.segments) 45 | 46 | function handleSegmentNameChange(newSegmentTitle) { 47 | dispatch(modifySegment({ 48 | ...thisSegment, 49 | segmentTitle: newSegmentTitle, 50 | })) 51 | } 52 | 53 | function handleSegmentTextChange(newSegmentText) { 54 | dispatch(modifySegment({ 55 | ...thisSegment, 56 | segmentText: newSegmentText, 57 | })) 58 | } 59 | 60 | function segmentTextOnBlur(event) { 61 | context.setTextPreview(event.target.value) 62 | } 63 | 64 | function handleSegmentColorChange(newColor) { 65 | dispatch(modifySegment({ 66 | ...thisSegment, 67 | segmentColor: newColor, 68 | })) 69 | toggleColorPickerOpen(false) 70 | } 71 | 72 | function handleSegmentDelete() { 73 | const filteredSegments = allSegments.filter(segment => segment.id !== segmentId) 74 | dispatch(setSegments(filteredSegments)) 75 | } 76 | 77 | function handleFocusOut(event) { 78 | handleSegmentNameChange(event.target.value) 79 | } 80 | 81 | return ( 82 | <> 83 | 87 |
88 | handleFocusOut(event)} 93 | inheritedValue={segmentTitle} 94 | /> 95 |
    96 |
  • toggleColorPickerOpen(currStatus => !currStatus)} 98 | > 99 | 103 |
  • 104 |
  • 105 | 112 |
  • 113 |
114 |
115 |
116 | handleSegmentTextChange(event.target.value)} 118 | className={styles.segmentText} 119 | value={thisSegment.segmentText} 120 | ref={thisSegmentRef} 121 | onBlur={event => segmentTextOnBlur(event)} 122 | /> 123 |
124 | toggleColorPickerOpen(false)} 127 | segmentIndex={segmentKey} 128 | segmentColor={segmentColor} 129 | onChangeColor={color => handleSegmentColorChange(color)} 130 | /> 131 |
132 | 133 | ) 134 | } 135 | 136 | Segment.propTypes = { 137 | segmentColor: PropTypes.string.isRequired, 138 | segmentId: PropTypes.string.isRequired, 139 | segmentKey: PropTypes.number.isRequired, 140 | segmentTitle: PropTypes.string.isRequired, 141 | } 142 | 143 | export default Segment 144 | -------------------------------------------------------------------------------- /src/utils/consts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ic_format_align_left as alignLeft } from 'react-icons-kit/md/ic_format_align_left' 3 | import { ic_format_align_right as alignRight } from 'react-icons-kit/md/ic_format_align_right' 4 | import { ic_format_align_center as alignCenter } from 'react-icons-kit/md/ic_format_align_center' 5 | import Icon from 'react-icons-kit' 6 | 7 | export const COLOR_DARK = 'COLOR_DARK' 8 | export const COLOR_LIGHT = 'COLOR_LIGHT' 9 | 10 | export const Colors = { 11 | purple: '#8380FF', 12 | gray1: '#1E1E1E', 13 | gray2: '#2D2D2D', 14 | gray3: '$gray-3', 15 | gray4: '#3A3A3A', 16 | } 17 | 18 | export const scrollWidthSettngs = [ 19 | { id: 0, label: '50%' }, 20 | { id: 1, label: '75%' }, 21 | { id: 2, label: '100%' }, 22 | ] 23 | 24 | export const segmentColors = ['#DF4BCB', '#CFEB70', '#C1C1C1', '#5FA3E8', '#F4A836'] 25 | 26 | export const BUTTON = 'BUTTON' 27 | export const LINK = 'LINK' 28 | export const LOGIN = 'LOGIN' 29 | export const REGISTER = 'REGISTER' 30 | export const PASSWORD = 'PASSWORD' 31 | export const SAVE = 'SAVE' 32 | export const SAVE_AS_COPY = 'SAVE_AS_COPY' 33 | export const LOAD = 'LOAD' 34 | export const LOGGED_IN = 'loggedIn' 35 | export const NEW_PROMPTER = 'NEW_PROMPTER' 36 | export const MAC_OS = 'Mac OS' 37 | 38 | export const HELPER_TOP = 'Write or paste your script into the input field below. You can adjust the prompter settings on the left and see a live preview on the top right. Press "Create" to create you prompter. You can then press "Open" to open the prompter in a new tab, or copy the "Stream address" to open the prompter on another computer. You can also copy the "Remote phone address" to open a remote control on your phone. If you make changes to your prompter, click "Update" in your editor first and then in your prompter to apply the updates.' 39 | export const HELPER_SIDEBAR = 'This is an alpha release, which means it\'s not finished. You can report bugs, give feedback or suggest features by emailing info@prompter.me.' 40 | export const INFOBOX_TOP = 'INFOBOX_TOP' 41 | export const INFOBOX_SIDEBAR = 'INFOBOX_SIDEBAR' 42 | export const FULL_LOADER = 'FULL_LOADER' 43 | export const INLINE_LOADER = 'INLINE_LOADER' 44 | 45 | export const headers = { 46 | 'Content-Type': 'application/json', 47 | } 48 | 49 | export const SPACE = 'space' 50 | export const PAGEUP = 'pageup' 51 | export const PAGE_DOWN = 'pagedown' 52 | export const UP = 'up' 53 | export const DOWN = 'down' 54 | export const F6 = 'f6' 55 | export const LEFT = 'left' 56 | export const RIGHT = 'right' 57 | export const ENTER = 'enter' 58 | 59 | export const keyListeners = [ 60 | SPACE, 61 | PAGEUP, 62 | PAGE_DOWN, 63 | UP, 64 | DOWN, 65 | F6, 66 | LEFT, 67 | RIGHT, 68 | ] 69 | 70 | export const INC_SPEED = 'INC_SPEED' 71 | export const DEC_SPEED = 'DEC_SPEED' 72 | 73 | export const HOME = 'home' 74 | export const PLAYER = 'player' 75 | export const REMOTE = 'remote' 76 | export const POLICY = 'policy' 77 | export const ABOUT = 'about' 78 | export const FORGOTTEN_PW = 'password' 79 | export const CREATE = 'Create Prompter' 80 | export const CREATED = 'CREATED' 81 | export const OPEN = 'OPEN' 82 | export const DARK_THEME = 'DARK' 83 | export const LIGHT_THEME = 'LIGHT' 84 | export const SANS = 'SANS' 85 | export const SERIF = 'SERIF' 86 | export const MONO = 'MONO' 87 | 88 | export const colorSchemeSettings = [ 89 | { id: 0, label: `${DARK_THEME.toLowerCase()}` }, 90 | { id: 1, label: `${LIGHT_THEME.toLowerCase()}` }, 91 | ] 92 | 93 | export const fontOptions = [ 94 | { id: 0, label: `${SANS.toLowerCase()}` }, 95 | { id: 1, label: `${SERIF.toLowerCase()}` }, 96 | { id: 2, label: `${MONO.toLowerCase()}` }, 97 | ] 98 | 99 | export const colors = [ 100 | '#f44336', 101 | '#e91e63', 102 | '#9c27b0', 103 | '#673ab7', 104 | '#3f51b5', 105 | '#2196f3', 106 | '#03a9f4', 107 | '#00bcd4', 108 | '#009688', 109 | '#4caf50', 110 | '#8bc34a', 111 | '#cddc39', 112 | '#ffeb3b', 113 | '#ffc107', 114 | '#ff9800', 115 | '#ff5722', 116 | '#795548', 117 | '#607d8b', 118 | ] 119 | 120 | export const SEGMENT = 'SEGMENT' 121 | export const BREAK = 'BREAK' 122 | export const CENTER = 'CENTER' 123 | 124 | export const alignmentOptions = [ 125 | { id: 0, label: , option: LEFT }, 126 | { id: 1, label: , option: CENTER }, 127 | { id: 2, label: , option: RIGHT }, 128 | ] 129 | -------------------------------------------------------------------------------- /src/components/common/TextPreview/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | import React, { useEffect, useState, useRef, createRef, useContext } from 'react' 3 | import PropTypes from 'prop-types' 4 | import { useSelector } from 'react-redux' 5 | import styled from 'styled-components' 6 | 7 | import styles from './TextPreview.module.scss' 8 | import { getFontFamily } from '../../../utils/getFontFamily' 9 | import { alignmentOptions } from '../../../utils/consts' 10 | import rootContext from '../../Main/rootContext' 11 | 12 | const Text = styled.div` 13 | p { 14 | font-size: ${props => props.fontSize * 10}px; 15 | line-height: ${props => props.lineHeight} !important; 16 | letter-spacing: ${props => props.letterSpacing}vw !important; 17 | max-width: ${props => props.scrollWidth}; 18 | font-family: ${props => props.fontFamily}; 19 | text-align: ${props => props.textAlignment}; 20 | } 21 | ` 22 | 23 | const TextMirrored = styled.div` 24 | p { 25 | font-size: ${props => props.fontSize * 10}px; 26 | line-height: ${props => props.lineHeight} !important; 27 | letter-spacing: ${props => props.letterSpacing}vw !important; 28 | max-width: ${props => props.scrollWidth}; 29 | transform: scaleY(-1); 30 | font-family: ${props => props.fontFamily}; 31 | text-align: ${props => props.textAlignment}; 32 | } 33 | ` 34 | 35 | /** 36 | * @author zilahir 37 | * @function TextPreview 38 | * */ 39 | 40 | const useInterval = (callback, delay) => { 41 | const savedCallback = useRef() 42 | 43 | useEffect(() => { 44 | savedCallback.current = callback 45 | }, [callback]) 46 | 47 | useEffect(() => { 48 | function tick() { 49 | savedCallback.current() 50 | } 51 | if (delay !== null) { 52 | const id = setInterval(tick, delay) 53 | return () => { 54 | clearInterval(id) 55 | } 56 | } 57 | }, [delay]) 58 | } 59 | 60 | const TextPreview = props => { 61 | const { isAnimationRunning, scrollSpeed } = props 62 | const { textPreview } = useContext(rootContext) 63 | const [position, setPosition] = useState(0) 64 | const [scrollerRefs, setScrollerRefs] = useState([]) 65 | const scrollSpeedValue = scrollSpeed * 10 66 | const { text } = useSelector(state => state) 67 | 68 | const { fontSize, lineHeight, letterSpacing, scrollWidth, fontFamily, textAlignment } = text 69 | 70 | const STEP = 5 71 | 72 | const chosenTextAlignment = alignmentOptions.find( 73 | alignment => alignment.id === textAlignment, 74 | ).option.toLowerCase() 75 | 76 | useInterval(() => { 77 | setPosition(position + STEP) 78 | scrollerRefs.forEach(currRef => currRef.current.scroll({ 79 | top: position, 80 | })) 81 | }, isAnimationRunning ? scrollSpeedValue : null) 82 | 83 | const scrollHandler = event => { 84 | setPosition(event.currentTarget.scrollTop) 85 | } 86 | 87 | useEffect(() => { 88 | scrollerRefs.forEach(currRef => currRef.current.scroll({ top: position })) 89 | }, [position]) 90 | 91 | useEffect(() => { 92 | scrollerRefs.forEach(currRef => currRef.current.addEventListener('scroll', scrollHandler)) 93 | setScrollerRefs(ref => ( 94 | Array(2).fill().map((_, index) => ref[index] || createRef()) 95 | )) 96 | return () => scrollerRefs.forEach(currRef => currRef.current.removeEventListener('scroll', scrollHandler)) 97 | }, []) 98 | 99 | return ( 100 |
101 |
105 | 114 |
117 |

118 | {textPreview} 119 |

120 |
121 |
122 |
123 |
127 | 136 |
139 |

140 | {textPreview} 141 |

142 |
143 |
144 |
145 |
146 | ) 147 | } 148 | 149 | TextPreview.propTypes = { 150 | isAnimationRunning: PropTypes.bool.isRequired, 151 | scrollSpeed: PropTypes.number.isRequired, 152 | } 153 | 154 | export default TextPreview 155 | -------------------------------------------------------------------------------- /src/components/MobileController/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import ReactGA from 'react-ga' 3 | import { useParams } from 'react-router-dom' 4 | import { useSocket } from '@zilahir/use-socket.io-client' 5 | import classnames from 'classnames' 6 | import styled from 'styled-components' 7 | import { MorphReplace } from 'react-svg-morph' 8 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp' 9 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown' 10 | import FastRewindIcon from '@material-ui/icons/FastRewind' 11 | import FastForwardIcon from '@material-ui/icons/FastForward' 12 | import InfoIcon from '@material-ui/icons/Info' 13 | 14 | import styles from './MobileContainer.module.scss' 15 | import arrowBg from '../../assets/controls/bg.svg' 16 | import { REMOTE } from '../../utils/consts' 17 | import Logo from '../common/Logo' 18 | 19 | /** 20 | * @author zilahir 21 | * @function MobileController 22 | * */ 23 | 24 | const BTN = styled.div` 25 | &:before { 26 | content: ''; 27 | background-image: url(${arrowBg}) 28 | } 29 | ` 30 | 31 | const MobileController = () => { 32 | ReactGA.pageview(`${REMOTE}`) 33 | const { slug } = useParams() 34 | const [isPlaying, togglePlaying] = useState(false) 35 | const [socket] = useSocket(process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:5000' : process.env.REACT_APP_BACKEND_V2) 36 | function handleStartStop() { 37 | togglePlaying(!isPlaying) 38 | } 39 | 40 | function incScrollingSpeed() { 41 | if (socket) { 42 | socket.emit('incSpeed', { 43 | prompterId: slug, 44 | }) 45 | } 46 | } 47 | 48 | function decScrollingSpeed() { 49 | if (socket) { 50 | socket.emit('decSpeed', { 51 | prompterId: slug, 52 | }) 53 | } 54 | } 55 | 56 | function jumpUp() { 57 | if (socket) { 58 | socket.emit('jumpUp', { 59 | prompterId: slug, 60 | }) 61 | } 62 | } 63 | 64 | function jumpDown() { 65 | if (socket) { 66 | socket.emit('jumpDown', { 67 | prompterId: slug, 68 | }) 69 | } 70 | } 71 | 72 | useEffect(() => { 73 | if (socket) { 74 | socket.emit('isPlaying', { 75 | prompterId: slug, 76 | isPlaying, 77 | }) 78 | } 79 | }, [isPlaying]) 80 | 81 | return ( 82 |
83 |
84 | 85 |

86 | {slug} 87 | 88 | 89 | 90 |

91 |
92 | jumpUp()} 98 | > 99 | 100 | 101 |
102 | decScrollingSpeed()} 108 | > 109 | 110 | 111 |
handleStartStop()} 118 | onKeyDown={null} 119 | tabIndex={-1} 120 | > 121 | 125 | { 126 | !isPlaying 127 | ? ( 128 | 129 | ) 130 | : ( 131 | 132 | ) 133 | } 134 | 135 |
136 | incScrollingSpeed()} 145 | > 146 | 147 | 148 |
149 | jumpDown()} 155 | > 156 | 157 | 158 |
159 | ) 160 | } 161 | 162 | export default MobileController 163 | -------------------------------------------------------------------------------- /src/components/MobileController/MobileContainer.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .mainContainer { 5 | overflow: hidden; 6 | background: rgba($color: $gray-6, $alpha: 1.0); 7 | 8 | .logoContainer { 9 | width: 100%; 10 | display: flex; 11 | justify-content: center; 12 | position: absolute; 13 | flex-direction: column; 14 | top: 5%; 15 | 16 | p { 17 | @include paragraph(16px, $gray-4, 0); 18 | text-align: center; 19 | position: relative; 20 | margin: 20px; 21 | } 22 | 23 | div { 24 | width: inherit; 25 | margin-left: 0; 26 | display: flex; 27 | justify-content: center; 28 | } 29 | 30 | .infoIcon { 31 | margin-left: 10px; 32 | position: absolute; 33 | right: 0; 34 | } 35 | } 36 | 37 | div { 38 | &:focus { 39 | outline: none; 40 | } 41 | } 42 | 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | flex-direction: column; 47 | flex: 1; 48 | height: 100vh; 49 | .top { 50 | 51 | svg { 52 | position: absolute; 53 | bottom: 15px; 54 | } 55 | 56 | &:active { 57 | opacity: .4; 58 | } 59 | 60 | &:before { 61 | width: 135px; 62 | height: 135px; 63 | background-repeat: no-repeat; 64 | position: absolute; 65 | bottom: 0px; 66 | transform: rotate(180deg); 67 | display: flex; 68 | justify-content: flex-end; 69 | } 70 | position: relative; 71 | display: flex; 72 | flex: 1; 73 | justify-content: center; 74 | width: 150px; 75 | height: 200px; 76 | top: 20px; 77 | z-index: 9; 78 | img { 79 | transform: scale(1); 80 | display: flex; 81 | align-self: flex-end; 82 | position: relative; 83 | top: -20px; 84 | } 85 | } 86 | .middle { 87 | display: flex; 88 | justify-content: center; 89 | align-items: center; 90 | position: relative; 91 | flex: 1; 92 | width: 100%; 93 | .oneButton { 94 | svg { 95 | z-index: 9; 96 | } 97 | 98 | &:active { 99 | opacity: .4; 100 | } 101 | 102 | position: relative; 103 | height: 200px; 104 | width: 150px; 105 | display: flex; 106 | justify-content: center; 107 | align-items: center; 108 | &:first-of-type { 109 | position: absolute; 110 | left: 0; 111 | &:before { 112 | width: 135px; 113 | height: 135px; 114 | position: absolute; 115 | background-repeat: no-repeat; 116 | transform: scale(1); 117 | transform: rotate(90deg); 118 | top: unset; 119 | left: -30px; 120 | z-index: 1; 121 | } 122 | } 123 | &:last-of-type { 124 | position: absolute; 125 | right: 0; 126 | &:before { 127 | width: 135px; 128 | height: 135px; 129 | position: absolute; 130 | background-repeat: no-repeat; 131 | transform: scale(1); 132 | transform: rotate(-90deg); 133 | top: unset; 134 | left: 45px; 135 | z-index: 1; 136 | } 137 | } 138 | &.playPause { 139 | background: rgba($color: $purple, $alpha: 1.0); 140 | width: 140px; 141 | height: 140px; 142 | border-radius: 100px; 143 | display: flex; 144 | justify-content: center; 145 | align-items: center; 146 | } 147 | &.dirButton { 148 | 149 | } 150 | } 151 | } 152 | .bottom { 153 | svg { 154 | position: absolute; 155 | top: -10px; 156 | } 157 | &:active { 158 | opacity: .4; 159 | } 160 | &:before { 161 | width: 135px; 162 | height: 135px; 163 | background-repeat: no-repeat; 164 | position: absolute; 165 | top: -30px; 166 | display: flex; 167 | justify-content: flex-end; 168 | } 169 | top: 15px; 170 | position: relative; 171 | display: flex; 172 | flex: 1; 173 | width: 100%; 174 | justify-content: center; 175 | img { 176 | transform: scale(1); 177 | display: flex; 178 | align-self: flex-start; 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | // This optional code is used to register a service worker. 3 | // register() is not called by default. 4 | 5 | // This lets the app load faster on subsequent visits in production, and gives 6 | // it offline capabilities. However, it also means that developers (and users) 7 | // will only see deployed updates on subsequent visits to a page, after all the 8 | // existing tabs open on the page have been closed, since previously cached 9 | // resources are updated in the background. 10 | 11 | // To learn more about the benefits of this model and instructions on how to 12 | // opt-in, read https://bit.ly/CRA-PWA 13 | 14 | const isLocalhost = Boolean( 15 | window.location.hostname === 'localhost' || 16 | // [::1] is the IPv6 localhost address. 17 | window.location.hostname === '[::1]' || 18 | // 127.0.0.1/8 is considered localhost for IPv4. 19 | window.location.hostname.match( 20 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 21 | ) 22 | ); 23 | 24 | export function register(config) { 25 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 26 | // The URL constructor is available in all browsers that support SW. 27 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 28 | if (publicUrl.origin !== window.location.origin) { 29 | // Our service worker won't work if PUBLIC_URL is on a different origin 30 | // from what our page is served on. This might happen if a CDN is used to 31 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 32 | return; 33 | } 34 | 35 | window.addEventListener('load', () => { 36 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 37 | 38 | if (isLocalhost) { 39 | // This is running on localhost. Let's check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl, config); 41 | 42 | // Add some additional logging to localhost, pointing developers to the 43 | // service worker/PWA documentation. 44 | navigator.serviceWorker.ready.then(() => { 45 | console.log( 46 | 'This web app is being served cache-first by a service ' + 47 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 48 | ); 49 | }); 50 | } else { 51 | // Is not localhost. Just register service worker 52 | registerValidSW(swUrl, config); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | function registerValidSW(swUrl, config) { 59 | navigator.serviceWorker 60 | .register(swUrl) 61 | .then(registration => { 62 | registration.onupdatefound = () => { 63 | const installingWorker = registration.installing; 64 | if (installingWorker == null) { 65 | return; 66 | } 67 | installingWorker.onstatechange = () => { 68 | if (installingWorker.state === 'installed') { 69 | if (navigator.serviceWorker.controller) { 70 | // At this point, the updated precached content has been fetched, 71 | // but the previous service worker will still serve the older 72 | // content until all client tabs are closed. 73 | console.log( 74 | 'New content is available and will be used when all ' + 75 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 76 | ); 77 | 78 | // Execute callback 79 | if (config && config.onUpdate) { 80 | config.onUpdate(registration); 81 | } 82 | } else { 83 | // At this point, everything has been precached. 84 | // It's the perfect time to display a 85 | // "Content is cached for offline use." message. 86 | console.log('Content is cached for offline use.'); 87 | 88 | // Execute callback 89 | if (config && config.onSuccess) { 90 | config.onSuccess(registration); 91 | } 92 | } 93 | } 94 | }; 95 | }; 96 | }) 97 | .catch(error => { 98 | console.error('Error during service worker registration:', error); 99 | }); 100 | } 101 | 102 | function checkValidServiceWorker(swUrl, config) { 103 | // Check if the service worker can be found. If it can't reload the page. 104 | fetch(swUrl) 105 | .then(response => { 106 | // Ensure service worker exists, and that we really are getting a JS file. 107 | const contentType = response.headers.get('content-type'); 108 | if ( 109 | response.status === 404 || 110 | (contentType != null && contentType.indexOf('javascript') === -1) 111 | ) { 112 | // No service worker found. Probably a different app. Reload the page. 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister().then(() => { 115 | window.location.reload(); 116 | }); 117 | }); 118 | } else { 119 | // Service worker found. Proceed as normal. 120 | registerValidSW(swUrl, config); 121 | } 122 | }) 123 | .catch(() => { 124 | console.log( 125 | 'No internet connection found. App is running in offline mode.' 126 | ); 127 | }); 128 | } 129 | 130 | export function unregister() { 131 | if ('serviceWorker' in navigator) { 132 | navigator.serviceWorker.ready.then(registration => { 133 | registration.unregister(); 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /images/prompterme-logo-dark.svg: -------------------------------------------------------------------------------- 1 | Asset 4 -------------------------------------------------------------------------------- /src/assets/prompterme-logo-dark.svg: -------------------------------------------------------------------------------- 1 | Asset 4 -------------------------------------------------------------------------------- /src/components/Login/Login.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | @import '../../styles/mixins/paragraph'; 3 | 4 | .loginBoxContainer { 5 | position: absolute; 6 | border-radius: $login-box-border-radius; 7 | box-shadow: 3px 3px 20px rgba($color: #000000, $alpha: 0.8); 8 | background-color: $login-box-bg-color; 9 | top: 50px; 10 | right: unset; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | padding: 20px; 16 | z-index: 2; 17 | 18 | .additionalInfo { 19 | width: 275px; 20 | 21 | p { 22 | @include paragraph(14px, #ffffff); 23 | letter-spacing: 0; 24 | line-height: 1.7; 25 | padding: 10px 20px; 26 | 27 | a { 28 | color: $purple; 29 | text-decoration: none; 30 | 31 | &:hover { 32 | text-decoration: underline; 33 | } 34 | } 35 | } 36 | } 37 | 38 | &.registering { 39 | transition: all .3s ease; 40 | width: 280px; 41 | height: 100px; 42 | padding-top: 0; 43 | padding-bottom: 0; 44 | } 45 | 46 | &.saveContainer { 47 | width: 280px; 48 | height: 150px; 49 | .success { 50 | &.hidden { 51 | display: none; 52 | } 53 | &.visible { 54 | display: block; 55 | } 56 | width: 100%; 57 | position: absolute; 58 | bottom: 0; 59 | overflow: hidden; 60 | border-bottom-right-radius: 5px; 61 | border-bottom-left-radius: 5px; 62 | display: flex; 63 | justify-content: center; 64 | align-items: center; 65 | background-color: rgba($color: $success, $alpha: 0.8); 66 | p { 67 | @include paragraph(12px, #ffffff, 0); 68 | text-transform: uppercase; 69 | padding: 20px 0; 70 | } 71 | } 72 | } 73 | &.hidden { 74 | display: none; 75 | } 76 | &.visible { 77 | display: block; 78 | } 79 | .loginInput { 80 | margin: 10px; 81 | input { 82 | font-size: 14px !important; 83 | } 84 | } 85 | .loginInputWError { 86 | margin: 0 10px; 87 | input { 88 | font-size: 14px !important; 89 | border: 2px solid $orange !important; 90 | } 91 | } 92 | .loginBtn { 93 | margin: 0; 94 | margin-top: 20px; 95 | button { 96 | height: 45px; 97 | font-weight: 300; 98 | } 99 | } 100 | &.itemBoxContainer { 101 | right: unset; 102 | padding: 0; 103 | .savedItems { 104 | list-style-type: none; 105 | padding: 0; 106 | width: 260px; 107 | li { 108 | @include paragraph(15px, #ffffff); 109 | padding: 10px 0; 110 | transition: all .2s ease; 111 | display: flex; 112 | justify-content: space-between; 113 | padding: 10px 10px; 114 | padding-left: 20px; 115 | &:hover { 116 | cursor: pointer; 117 | background-color: rgba($color: $gray-3, $alpha: 1.0); 118 | transition: all .2s ease; 119 | } 120 | .rootIcon { 121 | display: flex; 122 | align-items: center; 123 | .icon { 124 | opacity: 0.6; 125 | margin-left: 15px; 126 | &:focus { 127 | outline: none; 128 | } 129 | &:hover { 130 | transition: all .2s ease; 131 | opacity: 1; 132 | cursor: pointer; 133 | } 134 | margin-left: 15px; 135 | &.rotate { 136 | transform: rotate(90deg); 137 | position: relative; 138 | top: 2px; 139 | } 140 | color: $purple; 141 | svg { 142 | fill: $purple; 143 | } 144 | } 145 | } 146 | &:last-of-type { 147 | border: none; 148 | } 149 | } 150 | } 151 | } 152 | 153 | .errorContainer { 154 | &.hidden { 155 | display: none; 156 | } 157 | padding-top: 10px; 158 | p { 159 | @include paragraph(14px, $orange, 0); 160 | } 161 | } 162 | 163 | .forgottenContainer { 164 | &:focus { 165 | outline: none 166 | } 167 | 168 | p { 169 | @include paragraph(14px, $purple, 0); 170 | margin-top: 20px; 171 | opacity: .8; 172 | &:hover { 173 | cursor: pointer; 174 | opacity: 1; 175 | transition: all 0.2s ease; 176 | text-decoration: underline; 177 | } 178 | } 179 | } 180 | } 181 | 182 | .modal { 183 | padding: 30px; 184 | 185 | h3 { 186 | @include paragraph(14px, #ffffff); 187 | font-weight: 300; 188 | margin: 0; 189 | margin-bottom: 20px; 190 | } 191 | 192 | p { 193 | @include paragraph(14px, #ffffff); 194 | } 195 | 196 | .buttonContainer { 197 | display: flex; 198 | padding-top: 30px; 199 | button { 200 | margin: 0 10px; 201 | font-weight: 300; 202 | } 203 | } 204 | } 205 | 206 | .overLay { 207 | position: absolute; 208 | width: 100%; 209 | height: 100%; 210 | left: 0; 211 | top: 0; 212 | } -------------------------------------------------------------------------------- /src/components/EditorSidebar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { Col } from 'react-grid-system' 4 | import Toggle from 'react-toggle' 5 | import 'react-toggle/style.css' 6 | import Icon from 'react-icons-kit' 7 | import { github } from 'react-icons-kit/fa/github' 8 | 9 | import SliderAlt from '../common/SliderAlt' 10 | import Selector from '../common/Selector' 11 | import { scrollWidthSettngs, colorSchemeSettings, fontOptions, alignmentOptions, Colors } from '../../utils/consts' 12 | import { SET_FONT_SIZE, SET_LINE_HEIGHT, SET_LETTER_SPACING, SET_SCROLL_SPEED } from '../../store/actions/actionTypes' 13 | import styles from './EditorSidebar.module.scss' 14 | import './Toggle.scss' 15 | import { toggleMirror, setScrollWidth, setFont, setTextAlignment } from '../../store/actions/text' 16 | import { setColorScheme } from '../../store/actions/misc' 17 | 18 | /** 19 | * @author zilahir 20 | * @function EditorSidebar 21 | * */ 22 | 23 | const EditorSidebar = () => { 24 | const dispatch = useDispatch() 25 | function handleFlip(boolean) { 26 | dispatch(toggleMirror(boolean.target.checked)) 27 | } 28 | 29 | const fontSize = useSelector(state => state.text.fontSize) 30 | const letterSpacing = useSelector(state => state.text.letterSpacing) 31 | const lineHeight = useSelector(state => state.text.lineHeight) 32 | const scrollSpeed = useSelector(state => state.text.scrollSpeed) 33 | const flipped = useSelector(state => state.text.isFlipped) 34 | 35 | const scrollWidth = useSelector(state => state.text.scrollWidth) 36 | const activeScrollId = scrollWidthSettngs.find(curr => ( 37 | curr.label === scrollWidth 38 | )) 39 | 40 | const colorScheme = useSelector(state => state.misc.chosenColorScheme) 41 | const activeColorScheme = colorSchemeSettings.find(currColorScheme => ( 42 | currColorScheme.label === colorScheme.toLowerCase() 43 | )) 44 | 45 | const selectedFont = useSelector(state => state.text.chosenFont) 46 | const activeFont = fontOptions.find(currentFont => ( 47 | currentFont.label === selectedFont.toLowerCase() 48 | )) 49 | 50 | const selectedAlignment = useSelector(state => state.text.textAlignment) 51 | const activeAlignment = alignmentOptions.find(curentAlignment => ( 52 | curentAlignment.id === selectedAlignment 53 | )) 54 | 55 | function handleScrollWidthChange(chosenScrollWidthId) { 56 | const chosenValue = scrollWidthSettngs.find(item => ( 57 | item.id === chosenScrollWidthId 58 | )) 59 | 60 | dispatch(setScrollWidth(chosenValue.label)) 61 | } 62 | 63 | function handleColorSchemeChange(chosenColorSchemeId) { 64 | const thisColorScheme = colorSchemeSettings.find( 65 | currentColorScheme => currentColorScheme.id === chosenColorSchemeId, 66 | ) 67 | dispatch(setColorScheme(thisColorScheme.label)) 68 | } 69 | 70 | function handleFontChange(chosenFontId) { 71 | const thisChosenFont = fontOptions.find( 72 | currentFont => currentFont.id === chosenFontId, 73 | ) 74 | dispatch(setFont(thisChosenFont.label)) 75 | } 76 | 77 | function handleAlignmentChange(chosenAlignmentId) { 78 | dispatch(setTextAlignment(chosenAlignmentId)) 79 | } 80 | 81 | return ( 82 | <> 83 | 87 |
88 | 95 | 103 | 111 |
112 |

113 | Scroll width 114 |

115 | handleScrollWidthChange(id)} 119 | /> 120 |
121 | 128 |
129 |

130 | Color Scheme 131 |

132 | handleColorSchemeChange(id)} 136 | /> 137 |
138 |
139 |

140 | Font 141 |

142 | handleFontChange(id)} 146 | /> 147 |
148 |
149 |

150 | Alignment 151 |

152 | handleAlignmentChange(id)} 156 | /> 157 |
158 |
159 |

160 | Flip for reflection 161 |

162 | handleFlip(bool)} 164 | checked={flipped} 165 | icons={null} 166 | /> 167 |
168 |
169 | 183 | 184 | 185 | ) 186 | } 187 | 188 | export default EditorSidebar 189 | --------------------------------------------------------------------------------