├── .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 | history.push(`/remote/${prompterSlug}`)}
32 | buttonClass={styles.goBtn}
33 | />
34 |
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 |
32 |
33 | handleDelete()}
37 | >
38 |
39 |
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 |
21 |
22 |
25 |
26 |
40 |
41 |
42 |
53 |
54 |
55 |
56 |
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 |
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 |
34 | You need to enable JavaScript to run this app.
35 |
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 |
42 | {
43 | labelText && (
44 |
45 | {labelText}
46 | {
47 | children && children
48 | }
49 |
50 | )
51 | }
52 | handleChange(e.target.value)}
56 | value={value || inheritedValue}
57 | disabled={isDisabled}
58 | placeholder={placeholder}
59 | onKeyDown={e => (hasKeyDownEvent ? keyDownEvent(e.key) : null)}
60 | onBlur={onFocusOut}
61 | />
62 |
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 |
45 | {
46 | icon
47 | ? (
48 |
49 | {icon}
50 |
51 | ) : null
52 | }
53 | {labelText}
54 |
55 |
56 | )
57 | : type === LINK && isVisible
58 | ? (
59 |
60 |
66 | {labelText}
67 |
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 |
69 |
70 |
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 |
66 | : 'Send'
69 | }
70 | disabled={!email || isLoading}
71 | onClick={() => sendForgottenPasswordEmail()}
72 | />
73 |
74 | >
75 | ) : (
76 |
77 |
We have sent you an email. Check your inbox!
78 |
82 |
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 | setActiveButton(1)}
49 | >
50 | Text
51 |
52 | setActiveButton(2)}
59 | >
60 | Segments
61 |
62 |
63 |
64 |
65 |
handleNewSegment(SEGMENT)}
68 | className={styles.button}
69 | >
70 |
73 |
74 | Add segment
75 |
76 |
77 |
handleNewSegment(BREAK)}
80 | className={styles.button}
81 | >
82 |
85 |
86 | Add pause
87 |
88 |
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 |
40 |
41 |
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 | [](https://app.netlify.com/sites/prompterme/deploys)  [](https://github.com/semantic-release/semantic-release)
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 | An opensource teleprompter application for the browser.
13 |
14 |
15 | # Teleprompter
16 |
17 | 
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 |
handlePasswordUpdate()}
116 | buttonClass={styles.buttonContainer}
117 | disabled={
118 | !newPassword || (newPassword !== confirmNewPassword)
119 | }
120 | />
121 | >
122 | ) : null
123 | }
124 |
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 | handleSegmentDelete()}
109 | >
110 |
111 |
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 |
--------------------------------------------------------------------------------