├── .env.dist ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .nojekyll ├── .stylintrc ├── .travis.yml ├── CNAME ├── README.md ├── babel.config.json ├── intro.gif ├── linode-storage ├── bg-stars.png ├── deathstar.png ├── tfa.mp3 └── tfa.ogg ├── package-lock.json ├── package.json ├── src ├── assets │ ├── KasselLabsLogo.png │ ├── bb8_thumbs_up.webp │ ├── bbic.jpg │ ├── favicon.png │ ├── gotic.jpg │ ├── intro.mp3 │ ├── intro.ogg │ ├── logo.svg │ ├── porg.gif │ ├── preview.png │ ├── stic.jpg │ ├── tlouic.jpg │ └── wic.jpg ├── donation_cancelled │ └── index.html ├── donation_success │ └── index.html ├── index.html ├── js │ ├── App.js │ ├── ApplicationState.js │ ├── AudioController.js │ ├── DownloadPage │ │ ├── AddEmailForm.js │ │ ├── Atat.js │ │ ├── CheckForDonation.js │ │ ├── ContactButton.js │ │ ├── DownloadPage.js │ │ ├── DownloadVideoButton.js │ │ ├── EmailRequestField.js │ │ ├── ImageUploadButton.js │ │ ├── Loader.js │ │ ├── PaymentModule.js │ │ ├── RenderedPage.js │ │ ├── RenderingPage.js │ │ ├── SocialButtons.js │ │ ├── TermsOfServiceAcceptance.js │ │ ├── constants.js │ │ ├── newFlow │ │ │ ├── DonateOrNotDonateNew.js │ │ │ ├── HelpButton.js │ │ │ ├── NotQueuedPageNew.js │ │ │ ├── RequestDownloadPageNew.js │ │ │ ├── VideoOptions.js │ │ │ ├── VideoQueuedPageNew.js │ │ │ └── VideoRequestSentNew.js │ │ └── paymentEventsHandler.js │ ├── ImageAdjustmentModal │ │ ├── Dialog.js │ │ └── ImageAdjustmentModal.js │ ├── StarWarsAnimation.js │ ├── ViewController.js │ ├── __mocks__ │ │ ├── ApplicationState.js │ │ ├── config.js │ │ └── sweetalert2.js │ ├── api │ │ ├── Counter.js │ │ ├── Http.js │ │ ├── actions.js │ │ ├── firebaseApi.js │ │ ├── firebaseApi.test.js │ │ ├── serverApi.js │ │ └── tracking.js │ ├── config.js │ ├── extras │ │ ├── UrlHandler.js │ │ ├── UserIdentifier.js │ │ ├── auxiliar.js │ │ ├── auxiliar.test.js │ │ ├── detectIE.js │ │ ├── escapeHtml.js │ │ ├── escapeHtml.test.js │ │ ├── facebookpixel.js │ │ ├── googleanalytics.js │ │ ├── isFirefoxDesktop.js │ │ ├── pageAfterDonation.js │ │ ├── tawkToChat.js │ │ └── utils.js │ ├── hooks │ │ └── useWindowSize.js │ ├── index.js │ ├── mountDownloadPage.js │ ├── old │ │ └── createdIntros.js │ └── util │ │ ├── getCORSImage.js │ │ ├── getCORSURL.js │ │ ├── getCroppedImages.js │ │ ├── getResizedImages.js │ │ └── getURLFromFile.js ├── renderer │ ├── index.html │ └── rendererPage.js └── styles │ ├── Loader.styl │ ├── animation.styl │ ├── atat.css │ ├── bb8.css │ ├── bodyStates.styl │ ├── checkForDonation.styl │ ├── configForm.styl │ ├── donation_after.styl │ ├── downloadPage.styl │ ├── fonts │ ├── NewsCycleFont.css │ ├── SWCrawlTitle.sfd │ ├── SWCrawlTitle3.sfd │ ├── SWCrawlTitle3.ttf │ ├── SWCrawlTitle_original.ttf │ └── Starjedi.ttf │ ├── footer.styl │ ├── imageAdjustmentDialog.styl │ ├── main.styl │ ├── normalize.css │ ├── punchItBB8.styl │ ├── scrollbar.styl │ ├── socialButtons.styl │ ├── sweetalert.styl │ └── videoOptions.styl ├── ss1.png ├── ss2.png ├── static └── script.js ├── webpack.config.js └── webpack.config.render.js /.env.dist: -------------------------------------------------------------------------------- 1 | FIREBASE_INITIAL= 2 | FIREBASE_A= 3 | FIREBASE_B= 4 | FIREBASE_C= 5 | FIREBASE_D= 6 | FIREBASE_E= 7 | FIREBASE_F= 8 | SERVER_API=https://5mitidksxm7xfn4g4-mock.stoplight-proxy.io/ 9 | PAYMENT_PAGE_URL=https://payment.kassellabs.io/ 10 | FACEBOOK_PIXEL= 11 | FIREBASE_S= 12 | NEWSLETTER_API_URL= 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | src/js/extras/facebookpixel.js 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "plugin:react/recommended", 9 | "airbnb" 10 | ], 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react" 20 | ], 21 | "rules": { 22 | "no-underscore-dangle": 0, 23 | "react/jsx-props-no-spreading": 0, 24 | "react/jsx-filename-extension": 0, 25 | "react/require-default-props": 0, 26 | "react/forbid-prop-types": 0 27 | }, 28 | "globals": { 29 | "jest": false, 30 | "it": false, 31 | "describe": false, 32 | "expect": false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/* 3 | *.log 4 | .cache/ 5 | coverage/ 6 | .env 7 | public/ -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/.nojekyll -------------------------------------------------------------------------------- /.stylintrc: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": false, 3 | "brackets": "always", 4 | "colons": "always", 5 | "colors": "always", 6 | "commaSpace": "always", 7 | "commentSpace": "always", 8 | "cssLiteral": "never", 9 | "customProperties": [], 10 | "depthLimit": 4, 11 | "duplicates": true, 12 | "efficient": "always", 13 | "exclude": [], 14 | "extendPref": false, 15 | "globalDupe": true, 16 | "groupOutputByFile": true, 17 | "indentPref": 2, 18 | "leadingZero": "never", 19 | "maxErrors": false, 20 | "maxWarnings": false, 21 | "mixed": false, 22 | "mixins": [], 23 | "namingConvention": false, 24 | "namingConventionStrict": false, 25 | "none": "never", 26 | "noImportant": true, 27 | "parenSpace": false, 28 | "placeholders": "always", 29 | "prefixVarsWithDollar": "always", 30 | "quotePref": "single", 31 | "reporterOptions": { 32 | "columns": ["lineData", "severity", "description", "rule"], 33 | "columnSplitter": " ", 34 | "showHeaders": false, 35 | "truncate": true 36 | }, 37 | "semicolons": "aways", 38 | "sortOrder": "grouped", 39 | "stackedProperties": "never", 40 | "trailingWhitespace": "never", 41 | "universal": false, 42 | "valid": true, 43 | "zeroUnits": "never", 44 | "zIndexNormalize": false 45 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run test:coverage 9 | - npm run build 10 | 11 | notifications: 12 | webhooks: https://fathomless-fjord-24024.herokuapp.com/notify -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | starwarsintrocreator.kassellabs.io -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | # Star Wars Intro Creator 8 | 9 | Create your own Star Wars movie opening. 10 | Fill the inputs with any text and hit Play. 11 | You can share the URL generated and anyone can see your intro. 12 | Access: [https://StarWarsIntroCreator.KasselLabs.io](https://StarWarsIntroCreator.KasselLabs.io) 13 | 14 | ## Credits 15 | Build on top of the [work by Tim Pietrusky](http://timpietrusky.com/star-wars-opening-crawl-from-1977) 16 | By [Bruno Orlandi](https://github.com/BrOrlandi) and [Nihey Takizawa](https://github.com/nihey) 17 | 18 | ## Supported by 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots 25 | ![Intro 1](ss1.png) 26 | 27 | ![Intro 2](ss2.png) 28 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /intro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/intro.gif -------------------------------------------------------------------------------- /linode-storage/bg-stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/linode-storage/bg-stars.png -------------------------------------------------------------------------------- /linode-storage/deathstar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/linode-storage/deathstar.png -------------------------------------------------------------------------------- /linode-storage/tfa.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/linode-storage/tfa.mp3 -------------------------------------------------------------------------------- /linode-storage/tfa.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/linode-storage/tfa.ogg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "star-wars-intro-creator", 3 | "version": "2.0.0", 4 | "description": "Star Wars Intro Creator by Kassel Labs", 5 | "author": "Kassel Labs ", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "20" 9 | }, 10 | "scripts": { 11 | "clear": "rm -rf dist .cache public", 12 | "build": "npm run clear && NODE_OPTIONS=--openssl-legacy-provider webpack --mode production && NODE_OPTIONS=--openssl-legacy-provider webpack --mode production -c webpack.config.render.js --output-path dist-rendering && cp .nojekyll dist && cp dist-rendering/* dist/", 13 | "start": "NODE_OPTIONS=--openssl-legacy-provider webpack serve --mode development", 14 | "start-render": "NODE_OPTIONS=--openssl-legacy-provider webpack serve --mode development -c webpack.config.render.js", 15 | "test": "jest", 16 | "test:watch": "npm run test -- --watchAll", 17 | "test:coverage": "npm run test -- --coverage", 18 | "test:coverage:results": "google-chrome coverage/lcov-report/index.html", 19 | "lint": "eslint src/" 20 | }, 21 | "jest": { 22 | "testURL": "http://localhost/", 23 | "moduleDirectories": [ 24 | "node_modules", 25 | "src" 26 | ] 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.13.15", 30 | "@babel/eslint-parser": "^7.13.14", 31 | "@babel/plugin-proposal-class-properties": "^7.13.0", 32 | "@babel/preset-env": "^7.13.15", 33 | "@babel/preset-react": "^7.13.13", 34 | "babel-jest": "^26.6.3", 35 | "babel-loader": "^8.2.2", 36 | "babel-polyfill": "^6.26.0", 37 | "copy-webpack-plugin": "^8.1.1", 38 | "css-loader": "^5.2.1", 39 | "dotenv-webpack": "^7.0.2", 40 | "eslint": "^7.24.0", 41 | "eslint-config-airbnb": "^18.2.1", 42 | "eslint-plugin-import": "^2.22.1", 43 | "eslint-plugin-jsx-a11y": "^6.4.1", 44 | "eslint-plugin-react": "^7.23.2", 45 | "eslint-plugin-react-hooks": "^4.2.0", 46 | "file-loader": "^6.2.0", 47 | "html-loader": "^2.1.2", 48 | "html-webpack-plugin": "^5.3.1", 49 | "jest": "^26.6.3", 50 | "raw-loader": "^4.0.2", 51 | "style-loader": "^2.0.0", 52 | "stylint": "^1.5.9", 53 | "stylus": "^0.54.5", 54 | "stylus-loader": "^5.0.0", 55 | "webpack": "^5.33.2", 56 | "webpack-cli": "^4.6.0", 57 | "webpack-dev-server": "^3.11.2" 58 | }, 59 | "dependencies": { 60 | "@distributed/utm": "^0.1.3", 61 | "@material-ui/core": "^4.12.4", 62 | "@material-ui/icons": "^4.11.3", 63 | "@sentry/browser": "^6.4.1", 64 | "axios": "^0.18.0", 65 | "bowser": "^1.9.4", 66 | "classnames": "^2.3.1", 67 | "devtools-detect": "^4.0.0", 68 | "exponential-backoff": "^3.1.0", 69 | "gif-frames": "^1.0.1", 70 | "lodash": "^4.17.21", 71 | "lodash.isequal": "^4.5.0", 72 | "lodash.uniq": "^4.5.0", 73 | "prop-types": "^15.7.2", 74 | "react": "^16.8.6", 75 | "react-dom": "^16.8.6", 76 | "react-easy-crop": "^3.5.3", 77 | "sweetalert2": "^7.15.1", 78 | "usehooks-ts": "^2.10.0" 79 | }, 80 | "staticFiles": { 81 | "staticPath": "static", 82 | "watcherGlob": "**/*" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/KasselLabsLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/KasselLabsLogo.png -------------------------------------------------------------------------------- /src/assets/bb8_thumbs_up.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/bb8_thumbs_up.webp -------------------------------------------------------------------------------- /src/assets/bbic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/bbic.jpg -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/gotic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/gotic.jpg -------------------------------------------------------------------------------- /src/assets/intro.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/intro.mp3 -------------------------------------------------------------------------------- /src/assets/intro.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/intro.ogg -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 14 | 15 | 16 | 40 | 41 | 42 | 52 | 53 | 54 | 64 | 65 | 66 | 72 | 73 | 74 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/assets/porg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/porg.gif -------------------------------------------------------------------------------- /src/assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/preview.png -------------------------------------------------------------------------------- /src/assets/stic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/stic.jpg -------------------------------------------------------------------------------- /src/assets/tlouic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/tlouic.jpg -------------------------------------------------------------------------------- /src/assets/wic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/assets/wic.jpg -------------------------------------------------------------------------------- /src/donation_cancelled/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Donation cancelled - Star Wars Intro Creator 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

Star waRS
intro CreatoR

21 |
22 |

payment cancelled

23 |
24 | Sad porg animation 25 |
26 |

27 | You didn't have finished the payment on PayPal. 28 | If you have questions about the service, please read our FAQ and Terms of Service: 29 |

30 | 31 | FAQ and Contact 32 | 33 | 34 |

35 | By using this website you are agreeing to our 36 | 40 | Terms of Service 41 | 42 |

43 | 44 | 45 | BACK TO MAIN PAGE 46 | 47 |
48 |
49 | 50 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/donation_success/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Payment made successfully - Star Wars Intro Creator 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

Star waRS
intro CreatoR

21 |
22 |

payment made successfully!

23 |
24 | BB-8 thumbs up animation 25 |
26 |

27 | Thanks for your support! 28 | If you paid enough for the video you should receive a confirmation email from us in a few minutes! 29 | If you don't receive it, please check your spam box or contact us: 30 |

31 | 32 | FAQ and Contact 33 | 34 | 35 |

36 | By using this website you are agreeing to our 37 | 41 | Terms of Service 42 | 43 |

44 | 45 | 46 | BACK TO MAIN PAGE 47 | 48 |
49 |
50 | 51 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/js/App.js: -------------------------------------------------------------------------------- 1 | import swal from 'sweetalert2'; 2 | import bowser from 'bowser'; 3 | 4 | import { sendGAPageView } from './extras/googleanalytics'; 5 | import ApplicationState, { PLAYING, EDITING } from './ApplicationState'; 6 | import UrlHandler from './extras/UrlHandler'; 7 | import UserIdentifier from './extras/UserIdentifier'; 8 | import AudioController from './AudioController'; 9 | import { documentReady, urlHashChange } from './extras/utils'; 10 | import { 11 | loadAndPlay, 12 | loadDownloadPage, 13 | setCreateMode, 14 | loadAndEdit, 15 | } from './api/actions'; 16 | import { defaultOpening, defaultKey } from './config'; 17 | 18 | let lastPage = ''; 19 | 20 | const startApplication = () => { 21 | UserIdentifier.setUser('SWIC'); 22 | urlHashChange(() => { 23 | sendGAPageView(); 24 | swal.close(); 25 | 26 | const { key, page, subpage } = UrlHandler.getParams(); 27 | if (key) { 28 | if (!page) { 29 | lastPage = ''; 30 | loadAndPlay(key); 31 | return; 32 | } 33 | 34 | if ('edit' === page) { 35 | lastPage = page; 36 | if (ApplicationState.state.key === key) { 37 | // interrupt animation if it's playing 38 | const interruptAnimation = !AudioController.audio.paused; 39 | ApplicationState.setState(EDITING, { interruptAnimation }); 40 | return; 41 | } 42 | 43 | loadAndEdit(key); 44 | return; 45 | } 46 | 47 | if ('download' === page) { 48 | const samePage = lastPage === page; 49 | 50 | if (samePage) { 51 | return; 52 | } 53 | 54 | lastPage = page; 55 | loadDownloadPage(key, subpage, samePage); 56 | return; 57 | } 58 | } 59 | 60 | if (ApplicationState.state.page === PLAYING) { 61 | setCreateMode({ interruptAnimation: true }); 62 | return; 63 | } 64 | 65 | setCreateMode({ 66 | opening: defaultOpening, 67 | key: defaultKey, 68 | }); 69 | }); 70 | 71 | documentReady(() => { 72 | if (!window.isIE) { 73 | window.dispatchEvent(new Event('hashchange')); 74 | } 75 | 76 | if (bowser.msedge) { 77 | swal( 78 | 'microsoft edge', 79 | 'This website is not optimized to work with Microsoft Edge, we recommend to use Firefox or Chrome for the best experience. Sorry for the inconvenience.', 80 | 'warning', 81 | ); 82 | } 83 | }); 84 | }; 85 | 86 | export default startApplication; 87 | -------------------------------------------------------------------------------- /src/js/ApplicationState.js: -------------------------------------------------------------------------------- 1 | import ViewController from './ViewController'; 2 | import AudioController from './AudioController'; 3 | import UrlHandler from './extras/UrlHandler'; 4 | 5 | export const CREATING = 'CREATING'; 6 | export const LOADING = 'LOADING'; 7 | export const PLAYING = 'PLAYING'; 8 | export const EDITING = 'EDITING'; 9 | export const DOWNLOAD = 'DOWNLOAD'; 10 | 11 | class ApplicationState { 12 | constructor() { 13 | this.state = { 14 | page: LOADING, 15 | }; 16 | 17 | AudioController.loadAudio(); 18 | this.renderState(); 19 | } 20 | 21 | setState(page, props = {}) { 22 | // previous state undo changes 23 | if (this.state.page !== page) { 24 | switch (this.state.page) { 25 | case LOADING: 26 | ViewController.unsetLoading(); 27 | break; 28 | 29 | case PLAYING: 30 | ViewController.stopPlaying(props.interruptAnimation); 31 | break; 32 | 33 | case EDITING: 34 | ViewController.unsetRunningVideo(); 35 | ViewController.hideDownloadButton(); 36 | ViewController.killTimer(); 37 | break; 38 | 39 | case DOWNLOAD: 40 | ViewController.unsetDownloadPage(); 41 | break; 42 | 43 | default: 44 | ViewController.unsetLoading(); 45 | } 46 | } 47 | 48 | this.state = { 49 | ...this.state, 50 | page, 51 | ...props, 52 | }; 53 | // console.log(this.state); 54 | this.renderState(); 55 | } 56 | 57 | renderState = async () => { 58 | const { opening, key } = this.state; 59 | 60 | // next state changes 61 | switch (this.state.page) { 62 | case LOADING: 63 | if (window.renderer) { 64 | return; 65 | } 66 | ViewController.setLoading(); 67 | break; 68 | 69 | case PLAYING: 70 | try { 71 | await ViewController.playOpening(opening); 72 | } catch (error) { 73 | const isAudioPlayError = error.message === 'AutoPlayError'; 74 | if (!isAudioPlayError) { 75 | throw error; 76 | } 77 | await ViewController.requestWindowInteraction(); 78 | await ViewController.playOpening(opening); 79 | } 80 | 81 | UrlHandler.goToEditPage(key); 82 | break; 83 | 84 | case EDITING: 85 | ViewController.setFormValues(opening); 86 | ViewController.showDownloadButton(); 87 | break; 88 | 89 | case DOWNLOAD: 90 | ViewController.setDownloadPage(); 91 | break; 92 | 93 | default: 94 | ViewController.unsetLoading(); 95 | } 96 | } 97 | } 98 | 99 | export default new ApplicationState(); 100 | -------------------------------------------------------------------------------- /src/js/DownloadPage/AddEmailForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EmailRequestField from './EmailRequestField'; 3 | import Atat from './Atat'; 4 | 5 | 6 | const AddEmailForm = ({ openingKey, finishRequestHandle }) => ( 7 |
8 | 9 |

10 | You can add more emails to receive the video in the form below. 11 |

12 | 17 |
18 | ); 19 | 20 | export default AddEmailForm; 21 | -------------------------------------------------------------------------------- /src/js/DownloadPage/Atat.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../styles/atat.css'; 3 | 4 | const Atat = () => ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 | 63 | 64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 | 73 | 74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 |
86 |
87 |
88 | 89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | 107 | 108 |
109 |
110 |
111 | 112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | 130 | 131 |
132 |
133 |
134 | 135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | 153 | 154 |
155 |
156 |
157 | 158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | 175 | ); 176 | 177 | export default Atat; 178 | -------------------------------------------------------------------------------- /src/js/DownloadPage/CheckForDonation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { loadDownloadStatus } from '../api/actions'; 4 | import { BUMPED, RENDERING } from './constants'; 5 | 6 | const PENDING = 0; 7 | const CONFIRMED = 1; 8 | const NOT_FOUND = 2; 9 | 10 | class CheckForDonation extends Component { 11 | constructor() { 12 | super(); 13 | 14 | this.state = { 15 | status: PENDING, 16 | startTime: Date.now(), 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | this._fetchDownloadStatus(); 22 | } 23 | 24 | _fetchDownloadStatus = async () => { 25 | const { openingKey } = this.props; 26 | const response = await loadDownloadStatus(openingKey); 27 | 28 | const isBumped = BUMPED === response.status; 29 | const isRenderingViaBump = RENDERING === response.status && response.bumped_on; 30 | 31 | if (isBumped || isRenderingViaBump) { 32 | this.setState({ 33 | status: CONFIRMED, 34 | }); 35 | return; 36 | } 37 | 38 | const now = Date.now(); 39 | const elapsed = now - this.state.startTime; 40 | const oneMinute = 60000; 41 | 42 | if (elapsed > oneMinute) { 43 | this.setState({ 44 | status: NOT_FOUND, 45 | }); 46 | return; 47 | } 48 | 49 | setTimeout(this._fetchDownloadStatus, 3000); 50 | } 51 | 52 | render() { 53 | const { status } = this.state; 54 | const donationPending = ( 55 |
56 |
57 | Checking for donation... 58 |
59 | ); 60 | 61 | const donationConfirmed = ( 62 |
63 |
64 | Payment confimed! 65 |
66 | ); 67 | 68 | const donationNotFound = ( 69 |
70 |
X
71 | 72 | Payment not found! 73 |
74 | Please, check if your payment was made successfully or try to pay again. 75 |
76 |
77 | ); 78 | 79 | return ( 80 |
81 | {status === PENDING && donationPending} 82 | {status === CONFIRMED && donationConfirmed} 83 | {status === NOT_FOUND && donationNotFound} 84 |
85 | ); 86 | } 87 | } 88 | 89 | export default CheckForDonation; 90 | -------------------------------------------------------------------------------- /src/js/DownloadPage/ContactButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ContactButton = ({ 4 | customText = 'If you have any questions, please check our FAQ or contact us through the link:', 5 | endText = null, 6 | }) => { 7 | const link = ( 8 | 14 | FAQ and Contact 15 | 16 | ); 17 | 18 | if (customText) { 19 | return ( 20 |

21 | {customText}  22 | {link} 23 | {endText && ` ${endText}`} 24 |

25 | ); 26 | } 27 | 28 | return link; 29 | }; 30 | 31 | export default ContactButton; 32 | -------------------------------------------------------------------------------- /src/js/DownloadPage/DownloadPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import swal from 'sweetalert2'; 3 | 4 | import { 5 | NOT_QUEUED, 6 | QUEUED, 7 | BUMPED, 8 | RENDERING, 9 | RENDERED, 10 | } from './constants'; 11 | 12 | import NotQueuedPageNew from './newFlow/NotQueuedPageNew'; 13 | import VideoRequestSentNew from './newFlow/VideoRequestSentNew'; 14 | import RequestDownloadPageNew from './newFlow/RequestDownloadPageNew'; 15 | import VideoQueuedPageNew from './newFlow/VideoQueuedPageNew'; 16 | import RenderingPage from './RenderingPage'; 17 | import RenderedPage from './RenderedPage'; 18 | import AddEmailForm from './AddEmailForm'; 19 | import { requestIntroDownload } from '../api/actions'; 20 | import { trackOpenedDownloadModal } from '../api/tracking'; 21 | 22 | import { registerPaymentEventsHandler, unregisterPaymentEventsHandler } from './paymentEventsHandler'; 23 | import UrlHandler from '../extras/UrlHandler'; 24 | 25 | const INITIAL_PAGE = 0; 26 | const REQUEST_PAGE = 1; 27 | const FINAL_PAGE = 2; 28 | const ADD_EMAIL_PAGE = 3; 29 | 30 | class DownloadPage extends Component { 31 | constructor(props) { 32 | super(props); 33 | const { status, openingKey, subpage } = props; 34 | 35 | const isRendered = status.status === RENDERED; 36 | 37 | const subpageState = isRendered ? {} : this.parseSubpage(subpage); 38 | 39 | this.state = { 40 | status, 41 | openingKey, 42 | ...subpageState, 43 | }; 44 | } 45 | 46 | componentDidMount() { 47 | window.addEventListener('hashchange', this.urlChangeHandler); 48 | registerPaymentEventsHandler(this.paymentSuccessCallback); 49 | trackOpenedDownloadModal(); 50 | } 51 | 52 | componentWillUnmount() { 53 | window.removeEventListener('hashchange', this.urlChangeHandler); 54 | unregisterPaymentEventsHandler(); 55 | } 56 | 57 | parseSubpage = (subpage) => { 58 | let page = INITIAL_PAGE; 59 | let donate = false; 60 | 61 | if (subpage === 'pay') { 62 | donate = true; 63 | page = REQUEST_PAGE; 64 | } 65 | 66 | if (subpage === 'request') { 67 | donate = false; 68 | page = REQUEST_PAGE; 69 | } 70 | 71 | if (subpage === 'add_email') { 72 | page = ADD_EMAIL_PAGE; 73 | } 74 | 75 | if (subpage === 'paid') { 76 | donate = true; 77 | page = FINAL_PAGE; 78 | } 79 | 80 | return { 81 | page, 82 | donate, 83 | }; 84 | } 85 | 86 | paymentSuccessCallback = async (payment) => { 87 | const { email } = payment; 88 | 89 | const requestStatus = await requestIntroDownload(this.state.openingKey, email); 90 | this.setState({ 91 | page: FINAL_PAGE, 92 | donate: true, 93 | requestStatus, 94 | requestEmail: email, 95 | paymentData: payment, 96 | }); 97 | 98 | UrlHandler.goToDownloadPage(this.state.openingKey, 'paid'); 99 | } 100 | 101 | urlChangeHandler = () => { 102 | const { key, subpage } = UrlHandler.getParams(); 103 | 104 | if (key !== this.state.openingKey) { 105 | window.location.reload(); 106 | return; 107 | } 108 | 109 | if (!subpage) { 110 | return; 111 | } 112 | 113 | const subpageState = this.parseSubpage(subpage); 114 | this.setState(subpageState); 115 | } 116 | 117 | yesDonateHandle = () => { 118 | const { page, openingKey } = this.state; 119 | 120 | if (page === INITIAL_PAGE) { 121 | swal({ 122 | title: 'pay', 123 | html: '

Fill the payment form above to make your payment first.

', 124 | }); 125 | return; 126 | } 127 | 128 | this.setState({ page: INITIAL_PAGE }); 129 | UrlHandler.goToDownloadPage(openingKey); 130 | }; 131 | 132 | noDonateHandle = () => { 133 | const { openingKey } = this.state; 134 | UrlHandler.goToDownloadPage(openingKey, 'request'); 135 | }; 136 | 137 | finishRequestHandle = (requestStatus, requestEmail) => { 138 | const { donate, openingKey } = this.state; 139 | UrlHandler.goToDownloadPage(openingKey, donate ? 'paid' : ''); 140 | this.setState({ 141 | page: FINAL_PAGE, 142 | requestStatus, 143 | requestEmail, 144 | }); 145 | } 146 | 147 | addEmailNextPage = (requestStatus, requestEmail) => { 148 | this.setState({ 149 | status: requestStatus, 150 | requestStatus, 151 | requestEmail, 152 | }); 153 | } 154 | 155 | renderInitialPage() { 156 | const { status, openingKey, requestEmail } = this.state; 157 | const statusType = status.status; 158 | 159 | switch (statusType) { 160 | default: 161 | case NOT_QUEUED: 162 | return ( 163 | 167 | ); 168 | 169 | case QUEUED: 170 | return ( 171 | 178 | ); 179 | 180 | case RENDERING: 181 | case BUMPED: 182 | return ( 183 | 188 | ); 189 | 190 | case RENDERED: 191 | return ( 192 | 195 | ); 196 | } 197 | } 198 | 199 | renderPageContent = () => { 200 | const { 201 | page, 202 | openingKey, 203 | donate, 204 | status, 205 | requestStatus, 206 | requestEmail, 207 | paymentData, 208 | } = this.state; 209 | 210 | switch (page) { 211 | default: 212 | case INITIAL_PAGE: 213 | return this.renderInitialPage(); 214 | 215 | case REQUEST_PAGE: 216 | return ( 217 | 224 | ); 225 | 226 | case FINAL_PAGE: 227 | return ( 228 | 235 | ); 236 | 237 | case ADD_EMAIL_PAGE: 238 | return ( 239 | 243 | ); 244 | } 245 | } 246 | 247 | render() { 248 | const { status } = this.state.status; 249 | const canDonateToReceiveFaster = status === QUEUED; 250 | const title = canDonateToReceiveFaster ? 'payment and download' : 'download'; 251 | return ( 252 |
253 |

{title}

254 | {this.renderPageContent()} 255 |
256 | ); 257 | } 258 | } 259 | 260 | export default DownloadPage; 261 | -------------------------------------------------------------------------------- /src/js/DownloadPage/DownloadVideoButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DownloadVideoButton = ({ url }) => ( 4 |
5 | DOWNLOAD 6 |
7 | ); 8 | 9 | export default DownloadVideoButton; 10 | -------------------------------------------------------------------------------- /src/js/DownloadPage/EmailRequestField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import swal from 'sweetalert2'; 4 | import axios from 'axios'; 5 | import { backOff } from 'exponential-backoff'; 6 | import { requestIntroDownload } from '../api/actions'; 7 | import UserIdentifier from '../extras/UserIdentifier'; 8 | import { trackSubmitWithoutDonation } from '../api/tracking'; 9 | 10 | const newsletterApiURL = process.env.NEWSLETTER_API_URL; 11 | 12 | class EmailRequestField extends Component { 13 | handleSubmit = async (e) => { 14 | e.preventDefault(); 15 | const { openingKey, finishRequestHandle } = this.props; 16 | const emailField = document.querySelector('#emailRequestField #email'); 17 | const email = emailField.value; 18 | const subscribeCheckbox = document.querySelector('#emailRequestField #subscribe-newsletter'); 19 | 20 | if (subscribeCheckbox.checked) { 21 | backOff(() => axios.request({ 22 | url: newsletterApiURL, 23 | method: 'POST', 24 | data: { 25 | email, 26 | language: navigator.language, 27 | source: 'star-wars-intro-creator', 28 | }, 29 | })); 30 | } 31 | 32 | UserIdentifier.addEmail(email); 33 | 34 | let requestDownloadStatus; 35 | 36 | await swal({ 37 | title: 'download request', 38 | text: `Requestion download for intro "${openingKey}"...`, 39 | allowOutsideClick: false, 40 | allowEscapeKey: false, 41 | onOpen: async () => { 42 | swal.showLoading(); 43 | requestDownloadStatus = await requestIntroDownload(openingKey, email); 44 | if (requestDownloadStatus) { 45 | swal.hideLoading(); 46 | swal.clickConfirm(); 47 | 48 | if (requestDownloadStatus) { 49 | finishRequestHandle(requestDownloadStatus, email); 50 | } 51 | } 52 | }, 53 | }); 54 | } 55 | 56 | render() { 57 | const { buttonlabel = 'ADD EMAIL TO REQUEST' } = this.props; 58 | return ( 59 |
60 |
61 | 68 |
69 | 70 |
73 | 74 |
75 |
76 | ); 77 | } 78 | } 79 | 80 | EmailRequestField.propTypes = { 81 | buttonlabel: PropTypes.string, 82 | openingKey: PropTypes.string, 83 | finishRequestHandle: PropTypes.func, 84 | }; 85 | 86 | export default EmailRequestField; 87 | -------------------------------------------------------------------------------- /src/js/DownloadPage/ImageUploadButton.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import axios from 'axios' 3 | import getURLFromFile from '../util/getURLFromFile' 4 | import ImageAdjustmentModal from '../ImageAdjustmentModal/ImageAdjustmentModal'; 5 | 6 | const ImageUploadButton = ({ onChange }) => { 7 | const fileInputRef = useRef(); 8 | const [imageToCrop, setImageToCrop] = useState(null); 9 | 10 | return ( 11 | <> 12 | 18 | { 24 | const file = event.target.files[0] 25 | if (!file) { 26 | event.target.value = null; 27 | return 28 | } 29 | 30 | const newImage = await getURLFromFile(file) 31 | setImageToCrop(newImage) 32 | fileInputRef.current.value = null 33 | }} 34 | /> 35 | setImageToCrop(null)} 39 | onChange={async (adjustedImage) => { 40 | const response = await axios.get('https://chatwoot-bot.vercel.app/api/get-presigned-url'); 41 | const { uploadURL } = response.data; 42 | adjustedImage.name = 'image.png' 43 | await axios.put(uploadURL, adjustedImage, { 44 | headers: { 45 | 'Content-Type': 'image/png', 46 | 'x-amz-acl': 'public-read', 47 | }, 48 | }); 49 | const imageURL = uploadURL.replace(/\?.*/g, '') 50 | 51 | onChange(imageURL); 52 | }} 53 | /> 54 | 55 | ); 56 | }; 57 | 58 | ImageUploadButton.propTypes = {}; 59 | 60 | export default ImageUploadButton; 61 | -------------------------------------------------------------------------------- /src/js/DownloadPage/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loader = ({ 4 | text, 5 | style, 6 | size = 5, 7 | }) => ( 8 |
9 |
17 |

{text}

18 |
19 | ); 20 | 21 | export default Loader; 22 | -------------------------------------------------------------------------------- /src/js/DownloadPage/PaymentModule.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Fragment, useCallback, useState, useRef, useEffect, useMemo, 3 | } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { Box, CircularProgress } from '@material-ui/core'; 6 | import { useElementSize } from 'usehooks-ts'; 7 | import { trackAddToCart } from '../api/tracking'; 8 | import { paymentPageUrl } from '../config'; 9 | import Loader from './Loader'; 10 | import VideoOptions from './newFlow/VideoOptions'; 11 | import ImageUploadButton from './ImageUploadButton'; 12 | 13 | const PaymentModule = ({ openingKey }) => { 14 | const iframeRef = useRef(null); 15 | const [isCustomImage, setIsCustomImage] = useState(false); 16 | const [utmParamsString, setUtmParamsString] = useState(''); 17 | const [isLoadingCustomImagePreview, setIsLoadingCustomImagePreview] = useState(true); 18 | const [imagePreviewRef, imagePreviewSize] = useElementSize(); 19 | 20 | const transformScale = useMemo(() => ( 21 | imagePreviewSize?.width / 1920 22 | ), [imagePreviewSize?.width]); 23 | 24 | const [customImage, setCustomImage] = useState('https://kassellabs.us-east-1.linodeobjects.com/static-assets/star-wars/DeathStar-Background.png'); 25 | 26 | const updatePaymentAmount = useCallback((amount) => { 27 | iframeRef.current.contentWindow.postMessage({ action: 'setAmount', payload: amount }, '*'); 28 | 29 | trackAddToCart(amount); 30 | 31 | setIsCustomImage(amount >= 40); 32 | }, [iframeRef.current]); 33 | 34 | useEffect(() => { 35 | if (!customImage || !isCustomImage) { 36 | iframeRef.current.contentWindow.postMessage({ action: 'setCode', payload: openingKey }, '*'); 37 | return; 38 | } 39 | 40 | const code = JSON.stringify({ code: openingKey, image: customImage }); 41 | iframeRef.current.contentWindow.postMessage({ action: 'setCode', payload: code }, '*'); 42 | }, [openingKey, customImage, isCustomImage]); 43 | 44 | useEffect(() => { 45 | if (!isCustomImage) { 46 | return () => {}; 47 | } 48 | 49 | const onMessage = (event) => { 50 | const isAnimationType = event.data?.type === 'animation'; 51 | const isFinishedAction = event.data?.action === 'finished'; 52 | 53 | if (isAnimationType && isFinishedAction) { 54 | setIsLoadingCustomImagePreview(false); 55 | } 56 | }; 57 | window.addEventListener('message', onMessage); 58 | setIsLoadingCustomImagePreview(true); 59 | 60 | return () => { 61 | window.removeEventListener('message', onMessage); 62 | }; 63 | }, [customImage, isCustomImage]); 64 | 65 | useEffect(() => { 66 | const savedParamsString = localStorage.getItem('saved-utm-params'); 67 | if (!savedParamsString) { 68 | return; 69 | } 70 | 71 | try { 72 | const savedUtmParams = JSON.parse(savedParamsString); 73 | const params = new URLSearchParams(savedUtmParams); 74 | const paramsString = params.toString(); 75 | if (paramsString) { 76 | setUtmParamsString(`&${paramsString}`); 77 | } 78 | } catch (error) { 79 | // Supress any errors here 80 | } 81 | }, []); 82 | 83 | return ( 84 | <> 85 | 86 | { 87 | isCustomImage && ( 88 |
92 |

93 | Upload your custom image at the button below: 94 |

95 | { 97 | setCustomImage(newCustomImage); 98 | }} 99 | /> 100 |

101 | Preview your custom image: 102 |

103 |
107 |