├── .env.local ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── example1.png ├── example2.png ├── example3.png ├── example4.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── images │ │ ├── background │ │ │ └── website.jpg │ │ └── netflix.svg │ └── scss │ │ ├── helpers │ │ ├── _breakpoints.scss │ │ ├── _default.scss │ │ ├── _mixins.scss │ │ ├── _normalize.scss │ │ ├── _settings.scss │ │ └── _variables.scss │ │ ├── transitions │ │ ├── fade │ │ │ ├── fade-height.scss │ │ │ ├── fade-in-down.scss │ │ │ ├── fade-in-left.scss │ │ │ ├── fade-in-right.scss │ │ │ ├── fade-in-up.scss │ │ │ ├── fade.scss │ │ │ └── index.scss │ │ └── zoom │ │ │ ├── index.scss │ │ │ └── zoom.scss │ │ └── ui │ │ ├── button.scss │ │ ├── checkbox.scss │ │ ├── dropdown.scss │ │ ├── form.scss │ │ ├── hamburger.scss │ │ ├── input.scss │ │ ├── link.scss │ │ └── tile.scss ├── components │ ├── Header │ │ ├── AuthorizedHeader.vue │ │ ├── Header.scss │ │ ├── Header.vue │ │ └── UnauthorizedHeader.vue │ ├── MovieDetails │ │ ├── MovieDetails.scss │ │ └── MovieDetails.vue │ ├── MovieLabels │ │ ├── MovieLabels.scss │ │ └── MovieLabels.vue │ ├── MovieList │ │ ├── MovieList.scss │ │ └── MovieList.vue │ ├── MovieListItem │ │ ├── MovieListItem.scss │ │ └── MovieListItem.vue │ ├── MovieSlider │ │ ├── MovieSlider.scss │ │ └── MovieSlider.vue │ ├── MovieSliderItem │ │ ├── MovieSliderItem.scss │ │ └── MovieSliderItem.vue │ ├── Pagination │ │ ├── Pagination.scss │ │ └── Pagination.vue │ ├── ProfileDropdown │ │ ├── ProfileDropdown.scss │ │ └── ProfileDropdown.vue │ ├── Slider │ │ ├── Slider.scss │ │ └── Slider.vue │ └── Spinner │ │ ├── Spinner.scss │ │ └── Spinner.vue ├── directives │ └── clickOutside.js ├── helpers │ ├── axiosInterceptors.js │ ├── constants.js │ ├── debounce.js │ ├── fontawesome.js │ └── getImageUrl.js ├── index.scss ├── main.js ├── pages │ ├── Home │ │ ├── Home.scss │ │ └── Home.vue │ ├── Movies │ │ └── Movies.vue │ ├── MyList │ │ └── MyList.vue │ ├── Popular │ │ └── Popular.vue │ ├── RecoverPassword │ │ ├── RecoverPassword.vue │ │ ├── RecoverPasswordForm.vue │ │ └── RecoverPasswordSuccess.vue │ ├── Search │ │ └── Search.vue │ ├── SignIn │ │ ├── SignIn.scss │ │ └── SignIn.vue │ ├── SignUp │ │ └── SignUp.vue │ ├── StartNow │ │ ├── StartNow.scss │ │ └── StartNow.vue │ └── TVShows │ │ └── TVShows.vue ├── router │ └── index.js ├── services │ └── SliderService.js └── store │ ├── index.js │ ├── myList │ └── index.js │ ├── shared │ └── index.js │ └── user │ └── index.js └── vue.config.js /.env.local: -------------------------------------------------------------------------------- 1 | VUE_APP_FIREBASE_API_KEY= 2 | VUE_APP_FIREBASE_AUTH_DOMAIN= 3 | VUE_APP_FIREBASE_PROJECT_ID= 4 | VUE_APP_FIREBASE_STORAGE_BUCKET= 5 | VUE_APP_FIREBASE_MESSAGING_SENDER_ID= 6 | VUE_APP_FIREBASE_APP_ID= 7 | 8 | VUE_APP_TMDB_API_KEY= 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yaroslav Afanassiev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Netflix Clone 2 | A simple [Netflix](https://netflix.com) clone based on Vue powered by [Firebase](https://firebase.google.com). 3 |
4 | Last Commit 5 | Dependencies status 6 | 7 | License Badge 8 | 9 |
10 | 11 | ## Live Demo 12 | Link: https://approxipix.github.io/vue-netflix-clone 13 | 14 | ![example](https://github.com/Approxipix/vue-netflix-clone/blob/master/example1.png?raw=true) 15 | ![example](https://github.com/Approxipix/vue-netflix-clone/blob/master/example2.png?raw=true) 16 | ![example](https://github.com/Approxipix/vue-netflix-clone/blob/master/example3.png?raw=true) 17 | ![example](https://github.com/Approxipix/vue-netflix-clone/blob/master/example4.png?raw=true) 18 | 19 | 20 | ## Tech stack 21 | * [Vue](https://github.com/vuejs/vue) 22 | * [Vuex](https://github.com/vuejs/vuex) 23 | * [Vue Router](https://github.com/vuejs/vue-router) 24 | * [Firebase](https://firebase.google.com) 25 | * [The Movie Database (TMDb)](https://www.themoviedb.org) 26 | 27 | ## Features 28 | - [x] Authentication 29 | - [x] Sign up 30 | - [x] Sign in 31 | - [x] Sign in with Google 32 | - [x] Sign in with Facebook 33 | - [x] Sign in as demo user 34 | - [x] Recover password with email verification 35 | - [x] Logout 36 | - [x] Movies 37 | - [x] Search movies 38 | - [x] List of movies by category 39 | - [x] List of movies with pagination 40 | - [x] Detailed information about the movie 41 | - [x] Fully responsive movie slider 42 | - [x] Add movie to "my list" 43 | - [x] Responsive 44 | 45 | ## Configuration 46 | To use this project with Firebase authentication, some configuration steps are required. 47 | 1) Create a free Firebase account at [Firebase](https://firebase.google.com) 48 | 2) Create a project from your [Firebase account console](https://console.firebase.google.com) 49 | 3) Configure the authentication providers for your Firebase project from your [Firebase account console](https://console.firebase.google.com) 50 | 4) Configuration required to connect to Firebase is defined in the `.env.local` file in the root of this repository 51 | ```js 52 | VUE_APP_FIREBASE_API_KEY='' 53 | VUE_APP_FIREBASE_AUTH_DOMAIN='' 54 | VUE_APP_FIREBASE_PROJECT_ID='' 55 | VUE_APP_FIREBASE_STORAGE_BUCKET='' 56 | VUE_APP_FIREBASE_MESSAGING_SENDER_ID='' 57 | VUE_APP_FIREBASE_APP_ID='' 58 | ``` 59 | 5) In order for this application to work you will have to obtain an API key from [TMDB](https://www.themoviedb.org/settings/api). Once you get the key, you must insert it in a file `.env.local` 60 | ```js 61 | VUE_APP_TMDB_API_KEY='' 62 | ``` 63 | 64 | ## Installation 65 | Clone project: 66 | ```shell 67 | https://github.com/Approxipix/vue-netflix-clone.git 68 | ``` 69 | 70 | Then change into that folder: 71 | ```shell 72 | cd vue-netflix-clone 73 | ``` 74 | 75 | Install project dependencies: 76 | ```shell 77 | npm install 78 | ``` 79 | 80 | Build for production: 81 | ```shell 82 | npm run build 83 | ``` 84 | 85 | Start up a local server: 86 | ```shell 87 | npm run serve 88 | ``` 89 | 90 | Open [http://localhost:8080](http://localhost:8080) to view it in the browser. 91 | 92 | ## License License Badge 93 | This project is licensed under the MIT License. See the [LICENSE](https://github.com/approxipix/vue-netflix-clone/blob/master/LICENSE) file for more information. 94 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Approxipixie/vue-netflix-clone/c48b8877806af9f4bc175d06f6a7b47a63c73b58/example1.png -------------------------------------------------------------------------------- /example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Approxipixie/vue-netflix-clone/c48b8877806af9f4bc175d06f6a7b47a63c73b58/example2.png -------------------------------------------------------------------------------- /example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Approxipixie/vue-netflix-clone/c48b8877806af9f4bc175d06f6a7b47a63c73b58/example3.png -------------------------------------------------------------------------------- /example4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Approxipixie/vue-netflix-clone/c48b8877806af9f4bc175d06f6a7b47a63c73b58/example4.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-netflix-clone", 3 | "version": "0.1.0", 4 | "description": "A simple Netflix Clone based on Vue, Firebase, TMDb", 5 | "author": "Yaroslav Afanasiev ", 6 | "homepage": "https://approxipix.github.io/vue-netflix-clone", 7 | "license": "MIT", 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-svg-core": "^1.2.18", 15 | "@fortawesome/free-brands-svg-icons": "^5.8.2", 16 | "@fortawesome/free-solid-svg-icons": "^5.8.2", 17 | "@fortawesome/vue-fontawesome": "^0.1.6", 18 | "axios": "^0.21.2", 19 | "core-js": "^2.6.5", 20 | "firebase": "^6.0.4", 21 | "vue": "^2.6.10", 22 | "vue-router": "^3.0.3", 23 | "vuex": "^3.0.1" 24 | }, 25 | "devDependencies": { 26 | "@vue/cli-plugin-babel": "^3.8.0", 27 | "@vue/cli-plugin-eslint": "^3.8.0", 28 | "@vue/cli-service": "^5.0.8", 29 | "@vue/eslint-config-prettier": "^4.0.1", 30 | "babel-eslint": "^10.0.1", 31 | "eslint": "^5.16.0", 32 | "eslint-plugin-vue": "^5.0.0", 33 | "sass": "^1.18.0", 34 | "sass-loader": "^7.1.0", 35 | "vue-svg-loader": "^0.16.0", 36 | "vue-template-compiler": "^2.6.10" 37 | }, 38 | "eslintConfig": { 39 | "root": true, 40 | "env": { 41 | "node": true 42 | }, 43 | "extends": [ 44 | "plugin:vue/essential" 45 | ], 46 | "parserOptions": { 47 | "parser": "babel-eslint" 48 | } 49 | }, 50 | "postcss": { 51 | "plugins": { 52 | "autoprefixer": {} 53 | } 54 | }, 55 | "browserslist": [ 56 | "> 1%", 57 | "last 2 versions" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Approxipixie/vue-netflix-clone/c48b8877806af9f4bc175d06f6a7b47a63c73b58/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-netflix-clone 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /src/assets/images/background/website.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Approxipixie/vue-netflix-clone/c48b8877806af9f4bc175d06f6a7b47a63c73b58/src/assets/images/background/website.jpg -------------------------------------------------------------------------------- /src/assets/images/netflix.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/assets/scss/helpers/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | 3 | $breakpoints: sm, md, lg, xl; 4 | $screens: (sm: $screen-sm, md: $screen-md, lg: $screen-lg, xl: $screen-xl); 5 | 6 | /* Small devices */ 7 | @mixin sm { 8 | @media (min-width: map-get($screens, sm) + 'px') { 9 | @content; 10 | } 11 | } 12 | 13 | /* Medium devices */ 14 | @mixin md { 15 | @media (min-width: map-get($screens, md) + 'px') { 16 | @content; 17 | } 18 | } 19 | 20 | /* Large devices */ 21 | @mixin lg { 22 | @media (min-width: map-get($screens, lg) + 'px') { 23 | @content; 24 | } 25 | } 26 | 27 | /* Extra large devices */ 28 | @mixin xl { 29 | @media (min-width: map-get($screens, xl) + 'px') { 30 | @content; 31 | } 32 | } 33 | 34 | /* Custom device */ 35 | @mixin rwd($screen) { 36 | @media (min-width: $screen + 'px') { 37 | @content; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/scss/helpers/_default.scss: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-font-smoothing: antialiased; 3 | } 4 | 5 | body { 6 | font-family: Helvetica, Arial ,sans-serif; 7 | font-size: 14px; 8 | color: $white; 9 | background: $gray900; 10 | @include md { 11 | font-size: 16px;; 12 | } 13 | } 14 | 15 | button { 16 | background-color: transparent; 17 | border: none; 18 | cursor: pointer; 19 | padding: 0; 20 | } 21 | 22 | figure { 23 | margin: 0; 24 | } 25 | 26 | * { 27 | box-sizing: border-box; 28 | position: relative; 29 | } 30 | 31 | a, 32 | button, 33 | input, 34 | textarea { 35 | transition-duration: .3s; 36 | 37 | &:active, 38 | &:focus { 39 | outline: none; 40 | } 41 | } 42 | 43 | input, 44 | textarea { 45 | appearance: none; 46 | } 47 | 48 | a { 49 | text-decoration: none; 50 | color: $gray400; 51 | } 52 | 53 | h1, h2, h3, h4, h5, h6 { 54 | margin: 0; 55 | } 56 | 57 | p { 58 | color: $gray600; 59 | } 60 | 61 | img { 62 | max-width: 100%; 63 | height: auto; 64 | } 65 | 66 | ul { 67 | margin: 0; 68 | padding: 0; 69 | list-style: none; 70 | } 71 | 72 | .content-area { 73 | overflow: hidden; 74 | } 75 | 76 | .no-scroll { 77 | height: 100%; 78 | overflow: hidden; 79 | position: static; 80 | width: 100%; 81 | } 82 | 83 | .no-transition { 84 | transition: none!important; 85 | transition-duration: 0s!important; 86 | } 87 | 88 | .bg { 89 | background-image: url("./assets/images/background/website.jpg"); 90 | background-position: center center; 91 | } 92 | 93 | .flex-jc { 94 | display: flex; 95 | justify-content: space-between; 96 | align-items: center; 97 | } 98 | 99 | .page__content { 100 | display: flex; 101 | flex-direction: column; 102 | padding: 6rem 1rem 0; 103 | @include sm { 104 | padding: 3rem 2rem 0; 105 | } 106 | } 107 | 108 | .page__title { 109 | font-size: 1.25rem; 110 | @include sm { 111 | font-size: 1.5rem; 112 | } 113 | @include md { 114 | font-size: 2rem; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/assets/scss/helpers/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin icon { 2 | font: normal normal normal 16px/1 icons; 3 | text-rendering: auto; 4 | -moz-osx-font-smoothing: grayscale; 5 | -webkit-font-smoothing: antialiased; 6 | } 7 | 8 | @mixin ellipsis { 9 | display: block; 10 | overflow: hidden; 11 | text-overflow: ellipsis; 12 | white-space: nowrap; 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/scss/helpers/_normalize.scss: -------------------------------------------------------------------------------- 1 | /* normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * 1. Remove the gray background on active links in IE 10. 89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 90 | */ 91 | 92 | a { 93 | background-color: transparent; /* 1 */ 94 | -webkit-text-decoration-skip: objects; /* 2 */ 95 | } 96 | 97 | /** 98 | * Remove the outline on focused links when they are also active or hovered 99 | * in all browsers (opinionated). 100 | */ 101 | 102 | a:active, 103 | a:hover { 104 | outline-width: 0; 105 | } 106 | 107 | /* Text-level semantics 108 | ========================================================================== */ 109 | 110 | /** 111 | * 1. Remove the bottom border in Firefox 39-. 112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: none; /* 1 */ 117 | text-decoration: underline; /* 2 */ 118 | text-decoration: underline dotted; /* 2 */ 119 | } 120 | 121 | /** 122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: inherit; 128 | } 129 | 130 | /** 131 | * Add the correct font weight in Chrome, Edge, and Safari. 132 | */ 133 | 134 | b, 135 | strong { 136 | font-weight: bolder; 137 | } 138 | 139 | /** 140 | * Add the correct font style in Android 4.3-. 141 | */ 142 | 143 | dfn { 144 | font-style: italic; 145 | } 146 | 147 | /** 148 | * Correct the font size and margin on `h1` elements within `section` and 149 | * `article` contexts in Chrome, Firefox, and Safari. 150 | */ 151 | 152 | h1 { 153 | font-size: 2em; 154 | margin: 0.67em 0; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Remove the border on images inside links in IE 10-. 200 | */ 201 | 202 | img { 203 | border-style: none; 204 | } 205 | 206 | /** 207 | * Hide the overflow in IE. 208 | */ 209 | 210 | svg:not(:root) { 211 | overflow: hidden; 212 | } 213 | 214 | /* Grouping content 215 | ========================================================================== */ 216 | 217 | /** 218 | * 1. Correct the inheritance and scaling of font size in all browsers. 219 | * 2. Correct the odd `em` font sizing in all browsers. 220 | */ 221 | 222 | code, 223 | kbd, 224 | pre, 225 | samp { 226 | font-family: monospace, monospace; /* 1 */ 227 | font-size: 1em; /* 2 */ 228 | } 229 | 230 | /** 231 | * Add the correct margin in IE 8. 232 | */ 233 | 234 | figure { 235 | margin: 1em 40px; 236 | } 237 | 238 | /** 239 | * 1. Add the correct box sizing in Firefox. 240 | * 2. Show the overflow in Edge and IE. 241 | */ 242 | 243 | hr { 244 | box-sizing: content-box; /* 1 */ 245 | height: 0; /* 1 */ 246 | overflow: visible; /* 2 */ 247 | } 248 | 249 | /* Forms 250 | ========================================================================== */ 251 | 252 | /** 253 | * 1. Change font properties to `inherit` in all browsers (opinionated). 254 | * 2. Remove the margin in Firefox and Safari. 255 | */ 256 | 257 | button, 258 | input, 259 | select, 260 | textarea { 261 | font: inherit; /* 1 */ 262 | margin: 0; /* 2 */ 263 | } 264 | 265 | /** 266 | * Restore the font weight unset by the previous rule. 267 | */ 268 | 269 | optgroup { 270 | font-weight: bold; 271 | } 272 | 273 | /** 274 | * Show the overflow in IE. 275 | * 1. Show the overflow in Edge. 276 | */ 277 | 278 | button, 279 | input { /* 1 */ 280 | overflow: visible; 281 | } 282 | 283 | /** 284 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 285 | * 1. Remove the inheritance of text transform in Firefox. 286 | */ 287 | 288 | button, 289 | select { /* 1 */ 290 | text-transform: none; 291 | } 292 | 293 | /** 294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 295 | * controls in Android 4. 296 | * 2. Correct the inability to style clickable types in iOS and Safari. 297 | */ 298 | 299 | button, 300 | html [type="button"], /* 1 */ 301 | [type="reset"], 302 | [type="submit"] { 303 | -webkit-appearance: button; /* 2 */ 304 | } 305 | 306 | /** 307 | * Remove the inner border and padding in Firefox. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | [type="button"]::-moz-focus-inner, 312 | [type="reset"]::-moz-focus-inner, 313 | [type="submit"]::-moz-focus-inner { 314 | border-style: none; 315 | padding: 0; 316 | } 317 | 318 | /** 319 | * Restore the focus styles unset by the previous rule. 320 | */ 321 | 322 | button:-moz-focusring, 323 | [type="button"]:-moz-focusring, 324 | [type="reset"]:-moz-focusring, 325 | [type="submit"]:-moz-focusring { 326 | outline: 1px dotted ButtonText; 327 | } 328 | 329 | /** 330 | * Change the border, margin, and padding in all browsers (opinionated). 331 | */ 332 | 333 | fieldset { 334 | border: 1px solid #c0c0c0; 335 | margin: 0 2px; 336 | padding: 0.35em 0.625em 0.75em; 337 | } 338 | 339 | /** 340 | * 1. Correct the text wrapping in Edge and IE. 341 | * 2. Correct the color inheritance from `fieldset` elements in IE. 342 | * 3. Remove the padding so developers are not caught out when they zero out 343 | * `fieldset` elements in all browsers. 344 | */ 345 | 346 | legend { 347 | box-sizing: border-box; /* 1 */ 348 | color: inherit; /* 2 */ 349 | display: table; /* 1 */ 350 | max-width: 100%; /* 1 */ 351 | padding: 0; /* 3 */ 352 | white-space: normal; /* 1 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 404 | */ 405 | 406 | ::-webkit-input-placeholder { 407 | color: inherit; 408 | opacity: 0.54; 409 | } 410 | 411 | /** 412 | * 1. Correct the inability to style clickable types in iOS and Safari. 413 | * 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; /* 1 */ 418 | font: inherit; /* 2 */ 419 | } 420 | -------------------------------------------------------------------------------- /src/assets/scss/helpers/_settings.scss: -------------------------------------------------------------------------------- 1 | // Screen sizes 2 | $screen-sm: 576; 3 | $screen-md: 768; 4 | $screen-lg: 992; 5 | $screen-xl: 1200; 6 | 7 | // Max content (container) width 8 | $content-xs: 100%; 9 | $content-sm: $content-xs; 10 | $content-md: $content-sm; 11 | $content-lg: 1000px; 12 | $content-xl: $content-lg; 13 | 14 | -------------------------------------------------------------------------------- /src/assets/scss/helpers/_variables.scss: -------------------------------------------------------------------------------- 1 | $white: #ffffff; 2 | $black: #000000; 3 | $gray200: #cccccc; 4 | $gray300: #a3a2a2; 5 | $gray400: #b3b3b3; 6 | $gray500: #8c8c8c; 7 | $gray600: #737373; 8 | $gray700: #454545; 9 | $gray800: #333333; 10 | $gray900: #1a1a1a; 11 | $yellow: #f1c40f; 12 | $orange: #e87c03; 13 | $red: #e50914; 14 | $blue: #3498db; 15 | $purple: #9b59b6; 16 | $green: #2ecc71; 17 | 18 | $primary-btn: #dc0913; 19 | $primary-btn-hover: #f40612; 20 | 21 | $z-index-dropdown: 50; 22 | $z-index-header: 150; 23 | $z-index-modal: 200; 24 | $z-index-nav: 250; 25 | -------------------------------------------------------------------------------- /src/assets/scss/transitions/fade/fade-height.scss: -------------------------------------------------------------------------------- 1 | 2 | .fade-height-enter-active, 3 | .fade-height-leave-active { 4 | transition: all 0.2s; 5 | max-height: 250px; 6 | } 7 | 8 | .fade-height-enter, 9 | .fade-height-leave-to 10 | { 11 | opacity: 0; 12 | max-height: 0px; 13 | } -------------------------------------------------------------------------------- /src/assets/scss/transitions/fade/fade-in-down.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeInDown { 2 | from { 3 | transform: translate3d(0, -40px, 0); 4 | } 5 | 6 | to { 7 | transform: translate3d(0, 0, 0); 8 | opacity: 1 9 | } 10 | } 11 | 12 | .fade-in-down-leave-to { 13 | opacity: 0; 14 | transition: opacity .3s; 15 | } 16 | 17 | .fade-in-down-enter { 18 | opacity: 0; 19 | transform: translate3d(0, -40px, 0); 20 | } 21 | 22 | .fade-in-down-enter-to { 23 | opacity: 0; 24 | animation-duration: .7s; 25 | animation-fill-mode: both; 26 | animation-name: fadeInDown; 27 | } -------------------------------------------------------------------------------- /src/assets/scss/transitions/fade/fade-in-left.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeInLeft { 2 | from { 3 | transform: translate3d(-40px, 0, 0); 4 | } 5 | 6 | to { 7 | transform: translate3d(0, 0, 0); 8 | opacity: 1 9 | } 10 | } 11 | 12 | .fade-in-left-leave-to { 13 | opacity: 0; 14 | transition: opacity .3s; 15 | } 16 | 17 | .fade-in-left-enter { 18 | opacity: 0; 19 | transform: translate3d(-40px, 0, 0); 20 | } 21 | 22 | .fade-in-left-enter-to { 23 | opacity: 0; 24 | animation-duration: .7s; 25 | animation-fill-mode: both; 26 | animation-name: fadeInLeft; 27 | } -------------------------------------------------------------------------------- /src/assets/scss/transitions/fade/fade-in-right.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeInRight { 2 | from { 3 | transform: translate3d(40px, 0, 0); 4 | } 5 | 6 | to { 7 | transform: translate3d(0, 0, 0); 8 | opacity: 1 9 | } 10 | } 11 | 12 | .fade-in-right-leave-to { 13 | opacity: 0; 14 | transition: opacity .3s; 15 | } 16 | 17 | .fade-in-right-enter { 18 | opacity: 0; 19 | transform: translate3d(40px, 0, 0); 20 | } 21 | 22 | .fade-in-right-enter-to { 23 | opacity: 0; 24 | animation-duration: .7s; 25 | animation-fill-mode: both; 26 | animation-name: fadeInRight; 27 | } -------------------------------------------------------------------------------- /src/assets/scss/transitions/fade/fade-in-up.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeInUp { 2 | from { 3 | transform: translate3d(0, 40px, 0); 4 | } 5 | 6 | to { 7 | transform: translate3d(0, 0, 0); 8 | opacity: 1 9 | } 10 | } 11 | 12 | .fade-in-up-leave-to { 13 | opacity: 0; 14 | transition: opacity .3s; 15 | } 16 | 17 | .fade-in-up-enter { 18 | opacity: 0; 19 | transform: translate3d(0, 40px, 0); 20 | } 21 | 22 | .fade-in-up-enter-to { 23 | opacity: 0; 24 | animation-duration: .7s; 25 | animation-fill-mode: both; 26 | animation-name: fadeInUp; 27 | } -------------------------------------------------------------------------------- /src/assets/scss/transitions/fade/fade.scss: -------------------------------------------------------------------------------- 1 | .fade-enter-active, 2 | .fade-leave-active { 3 | transition-duration: 0.3s; 4 | transition-property: opacity; 5 | transition-timing-function: ease; 6 | } 7 | 8 | .fade-enter, 9 | .fade-leave-active { 10 | opacity: 0 11 | } -------------------------------------------------------------------------------- /src/assets/scss/transitions/fade/index.scss: -------------------------------------------------------------------------------- 1 | @import "./fade-height.scss"; 2 | @import "./fade-in-down.scss"; 3 | @import "./fade-in-left.scss"; 4 | @import "./fade-in-right.scss"; 5 | @import "./fade-in-up.scss"; 6 | @import "./fade.scss"; -------------------------------------------------------------------------------- /src/assets/scss/transitions/zoom/index.scss: -------------------------------------------------------------------------------- 1 | @import "./zoom.scss"; -------------------------------------------------------------------------------- /src/assets/scss/transitions/zoom/zoom.scss: -------------------------------------------------------------------------------- 1 | .zoom-enter-active, 2 | .zoom-leave-active { 3 | transition-duration: 0.3s; 4 | transition-property: all; 5 | transition-timing-function: ease; 6 | } 7 | 8 | .zoom-enter, 9 | .zoom-leave-to { 10 | opacity: 0; 11 | transform: scale(0); 12 | } -------------------------------------------------------------------------------- /src/assets/scss/ui/button.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | position: relative; 3 | display: inline-block; 4 | width: auto; 5 | padding: .3rem .9rem; 6 | font-size: 1rem; 7 | color: $white; 8 | text-align: center; 9 | white-space: nowrap; 10 | border-radius: .18rem; 11 | outline: none; 12 | cursor: pointer; 13 | transition: all .2s ease-out; 14 | &:not(:last-child) { 15 | margin-right: .5rem; 16 | } 17 | 18 | @include sm { 19 | padding: .4rem 1.1rem; 20 | } 21 | 22 | &--primary { 23 | border: 1px solid $primary-btn; 24 | background-color: $primary-btn; 25 | box-shadow: 0 1px 0 rgba(0,0,0,.45); 26 | &:hover { 27 | background: $primary-btn-hover; 28 | } 29 | } 30 | 31 | &--secondary { 32 | border: 1px solid $primary-btn; 33 | background-color: transparent; 34 | box-shadow: 0 1px 0 rgba(0, 0, 0, .45); 35 | &:hover { 36 | border-color: $primary-btn-hover; 37 | } 38 | } 39 | 40 | &--close { 41 | position: absolute; 42 | top: 32px; 43 | right: 32px; 44 | width: 24px; 45 | height: 24px; 46 | &:before, 47 | &:after { 48 | content: ''; 49 | display: block; 50 | width: 24px; 51 | height: 2px; 52 | background-color: $white; 53 | } 54 | &:before { 55 | position: relative; 56 | transform: translateY(50%) rotate(45deg); 57 | } 58 | &:after { 59 | transform: translateY(-50%) rotate(-45deg); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/assets/scss/ui/checkbox.scss: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | margin-right: .5rem; 3 | user-select: none; 4 | cursor: pointer; 5 | -webkit-tap-highlight-color:transparent; 6 | 7 | span { 8 | display: inline-block; 9 | vertical-align: middle; 10 | transform: translate3d(0, 0, 0); 11 | &:first-child { 12 | position: relative; 13 | width: 1.125rem; 14 | height: 1.125rem; 15 | border-radius: .2rem; 16 | transform: scale(1); 17 | vertical-align: middle; 18 | border: 1px solid $gray500; 19 | transition: all .2s ease; 20 | svg { 21 | position: absolute; 22 | top: 3px; 23 | left: 2px; 24 | fill: none; 25 | stroke: $gray900; 26 | stroke-width: 2; 27 | stroke-linecap: round; 28 | stroke-linejoin: round; 29 | stroke-dasharray: 1rem; 30 | stroke-dashoffset: 1rem; 31 | transition: all .3s ease; 32 | transition-delay: 0.1s; 33 | transform: translate3d(0, 0, 0); 34 | } 35 | } 36 | &:last-child { 37 | padding-left: 8px; 38 | } 39 | } 40 | 41 | &:hover span:first-child { 42 | border-color: $gray200; 43 | } 44 | 45 | &__wrapper { 46 | display: flex; 47 | font-size: .875rem; 48 | line-height: 1.43; 49 | letter-spacing: .3px; 50 | } 51 | 52 | &__input { 53 | display: none; 54 | } 55 | 56 | &__text { 57 | color: $gray400; 58 | font-size: .81rem; 59 | font-weight: 500; 60 | transition: color .3s ease; 61 | &:hover { 62 | color: $gray200; 63 | } 64 | } 65 | } 66 | 67 | .checkbox__input:checked + .checkbox span:first-child { 68 | background: $gray200; 69 | border-color: $gray200; 70 | animation: wave .4s ease; 71 | } 72 | .checkbox__input:checked + .checkbox span:first-child svg { 73 | stroke-dashoffset: 0; 74 | } 75 | .checkbox__input:checked + .checkbox span:first-child:before { 76 | transform: scale(3.5); 77 | opacity: 0; 78 | transition: all .6s ease; 79 | } 80 | 81 | @keyframes wave { 82 | 50% { 83 | transform: scale(0.9); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/assets/scss/ui/dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | position: absolute; 3 | top: calc(100% + 1rem); 4 | right: 50%; 5 | padding: .5rem; 6 | background-color: rgba(0, 0, 0, .85); 7 | border: 1px solid $gray700; 8 | border-top: 2px solid $white; 9 | color: $white; 10 | transition: all .3s ease-in-out; 11 | visibility: hidden; 12 | opacity: 0; 13 | transform: translateX(50%); 14 | 15 | &:before { 16 | content: ''; 17 | left: 0; 18 | bottom: 100%; 19 | position: absolute; 20 | height: 1.5rem; 21 | width: 100%; 22 | } 23 | 24 | &:after { 25 | content: ''; 26 | position: absolute; 27 | bottom: 100%; 28 | right: 50%; 29 | width: 0; 30 | height: 0; 31 | border-left: .55rem solid transparent; 32 | border-right: .55rem solid transparent; 33 | border-bottom: .55rem solid $white; 34 | transform: translateX(50%); 35 | } 36 | 37 | &:hover { 38 | visibility: visible; 39 | opacity: 1; 40 | } 41 | 42 | &__list { 43 | position: relative; 44 | z-index: 200; 45 | } 46 | 47 | &__btn { 48 | display: inline-block; 49 | padding: .25rem .5rem; 50 | color: $white; 51 | &:not(:last-child) { 52 | margin-right: 0; 53 | } 54 | &:hover { 55 | &:after { 56 | width: 85%; 57 | } 58 | } 59 | &:after { 60 | content: ''; 61 | position: absolute; 62 | bottom: 0; 63 | left: 50%; 64 | width: 0; 65 | height: 2px; 66 | background-color: $red; 67 | transform: translateX(-50%); 68 | transition: width .2s ease-in-out; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/assets/scss/ui/form.scss: -------------------------------------------------------------------------------- 1 | .form { 2 | 3 | &__field { 4 | margin-bottom: 1rem; 5 | } 6 | 7 | &__btns { 8 | margin-top: 2.5rem; 9 | margin-bottom: .5rem; 10 | .btn { 11 | width: 100%; 12 | padding: .9rem; 13 | font-weight: bold; 14 | &:not(:last-child) { 15 | margin-bottom: .5rem; 16 | } 17 | } 18 | } 19 | 20 | &__error { 21 | &-list { 22 | margin-top: .275rem; 23 | padding: 0 .175rem; 24 | } 25 | &-item { 26 | padding: .175rem 0; 27 | color: $orange; 28 | font-size: .815rem; 29 | } 30 | &-message { 31 | margin-bottom: 1rem; 32 | padding: .625rem 1.25rem; 33 | font-size: .875rem; 34 | background: $orange; 35 | border-radius: .25rem; 36 | a { 37 | color: $white; 38 | text-decoration: underline; 39 | } 40 | } 41 | } 42 | 43 | &__required { 44 | &-list { 45 | display: grid; 46 | grid-template-columns: repeat(2, 1fr); 47 | grid-gap: .5rem; 48 | margin: .5rem 0 .75rem 0; 49 | } 50 | &-item { 51 | position: relative; 52 | padding-left: .7rem; 53 | transition: all .2s ease-in-out; 54 | &:before { 55 | content: ''; 56 | display: block; 57 | position: absolute; 58 | left: 0; 59 | top: 35%; 60 | width: .4rem; 61 | height: .4rem; 62 | border-radius: 50%; 63 | background-color: $red; 64 | transition: all .2s ease-in-out; 65 | } 66 | &--done { 67 | color: $gray600; 68 | &:before { 69 | background-color: $gray600; 70 | } 71 | & .SignUp__required-text:after { 72 | left: 0; 73 | right: 0; 74 | } 75 | } 76 | } 77 | &-text { 78 | position: relative; 79 | font-size: .6rem; 80 | @include sm { 81 | font-size: .725rem; 82 | } 83 | &:after { 84 | content: ''; 85 | position: absolute; 86 | height: 1px; 87 | left: 50%; 88 | right: 50%; 89 | top: 55%; 90 | background-color: $gray600; 91 | transition: all .2s ease-in-out; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/assets/scss/ui/hamburger.scss: -------------------------------------------------------------------------------- 1 | .hamburger { 2 | width: 2.5rem; 3 | 4 | span { 5 | display: block; 6 | width: 100%; 7 | box-shadow: 0 2px 10px 0 rgba(0, 0, 0, .3); 8 | border-radius: .25rem; 9 | height: .25rem; 10 | background: $white; 11 | transition: all .3s; 12 | position: relative; 13 | } 14 | 15 | span + span { 16 | margin-top: .4375rem; 17 | } 18 | 19 | span:nth-child(1) { 20 | animation: ease .7s top-2 forwards; 21 | } 22 | span:nth-child(2) { 23 | animation: ease .7s scaled-2 forwards; 24 | } 25 | span:nth-child(3) { 26 | animation: ease .7s bottom-2 forwards; 27 | } 28 | 29 | &--active { 30 | span:nth-child(1) { 31 | animation: ease .7s top forwards; 32 | } 33 | span:nth-child(2) { 34 | animation: ease .7s scaled forwards; 35 | } 36 | span:nth-child(3) { 37 | animation: ease .7s bottom forwards; 38 | } 39 | } 40 | } 41 | 42 | @keyframes top { 43 | 0% { 44 | top: 0; 45 | transform: rotate(0); 46 | } 47 | 50% { 48 | top: .6875rem; 49 | transform: rotate(0); 50 | } 51 | 100% { 52 | top: .6875rem; 53 | transform: rotate(45deg); 54 | } 55 | } 56 | 57 | @keyframes top-2 { 58 | 0% { 59 | top: .6875rem; 60 | transform: rotate(45deg); 61 | } 62 | 50% { 63 | top: .6875rem; 64 | transform: rotate(0deg); 65 | } 66 | 100% { 67 | top: 0; 68 | transform: rotate(0deg); 69 | } 70 | } 71 | 72 | @keyframes bottom { 73 | 0% { 74 | bottom: 0; 75 | transform: rotate(0); 76 | } 77 | 50% { 78 | bottom: .6875rem; 79 | transform: rotate(0); 80 | } 81 | 100% { 82 | bottom: .6875rem; 83 | transform: rotate(135deg); 84 | } 85 | } 86 | 87 | @keyframes bottom-2 { 88 | 0% { 89 | bottom: .6875rem; 90 | transform: rotate(135deg); 91 | } 92 | 50% { 93 | bottom: .6875rem; 94 | transform: rotate(0); 95 | } 96 | 100% { 97 | bottom: 0; 98 | transform: rotate(0); 99 | } 100 | } 101 | 102 | @keyframes scaled { 103 | 50% { 104 | transform: scale(0); 105 | } 106 | 100% { 107 | transform: scale(0); 108 | } 109 | } 110 | 111 | @keyframes scaled-2 { 112 | 0% { 113 | transform: scale(0); 114 | } 115 | 50% { 116 | transform: scale(0); 117 | } 118 | 100% { 119 | transform: scale(1); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/assets/scss/ui/input.scss: -------------------------------------------------------------------------------- 1 | .input { 2 | position: relative; 3 | width: 100%; 4 | border-radius: .25rem; 5 | background: $gray800; 6 | border: 0; 7 | color: $white; 8 | font-size: 1rem; 9 | padding: 1rem 1.25rem 0; 10 | height: 3rem; 11 | outline: none; 12 | 13 | &::placeholder { 14 | color: transparent; 15 | } 16 | 17 | &--filled, 18 | &:focus { 19 | background: $gray700; 20 | + .input__placeholder { 21 | top: .7rem; 22 | font-size: .68rem; 23 | } 24 | } 25 | 26 | &__wrapper { 27 | position: relative; 28 | border-radius: .25rem; 29 | } 30 | 31 | &__placeholder { 32 | position: absolute; 33 | font-size: 1rem; 34 | top: 50%; 35 | left: 1.25rem; 36 | color: $gray500; 37 | transition: font .1s ease,top .1s ease,transform .1s ease; 38 | transform: translateY(-50%); 39 | } 40 | 41 | &--error { 42 | &:after { 43 | content: ''; 44 | position: absolute; 45 | bottom: 0; 46 | left: 0; 47 | width: 100%; 48 | height: .25rem; 49 | border-bottom: 2px solid $orange; 50 | border-bottom-right-radius: .25rem; 51 | border-bottom-left-radius: .25rem; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/assets/scss/ui/link.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | font-size: 1rem; 3 | color: $gray400; 4 | 5 | &:hover { 6 | text-decoration: underline; 7 | } 8 | 9 | &--white { 10 | color: $white; 11 | } 12 | 13 | &--s { 14 | font-size: .81rem; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/scss/ui/tile.scss: -------------------------------------------------------------------------------- 1 | .tile { 2 | position: relative; 3 | min-height: 100vh; 4 | padding: 1rem 1.5rem; 5 | 6 | &:before { 7 | content: ''; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | background: rgba(0, 0, 0, .4); 14 | } 15 | 16 | &__container { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | width: 100%; 21 | height: 100%; 22 | padding: 4rem 1.6rem 2rem; 23 | background-color: $black; 24 | @include sm { 25 | padding: 5rem 1.6rem 2rem; 26 | } 27 | @include md { 28 | top: 50%; 29 | left: 50%; 30 | width: 28rem; 31 | height: auto; 32 | padding: 3.75rem 4.25rem 2.5rem; 33 | transform: translate(-50%, -50%); 34 | background-color: rgba(0, 0, 0, .75); 35 | border-radius: .25rem; 36 | } 37 | } 38 | 39 | &__title { 40 | color: $white; 41 | font-size: 1.75rem; 42 | font-weight: bold; 43 | margin-bottom: 1.6rem; 44 | @include sm { 45 | font-size: 2rem; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Header/AuthorizedHeader.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 129 | 130 | 133 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | @import "../../assets/scss/helpers/variables"; 3 | 4 | .Header { 5 | position: fixed; 6 | top: 0; 7 | z-index: $z-index-header; 8 | display: grid; 9 | grid-gap: 1rem; 10 | grid-template-columns: auto 1fr auto; 11 | grid-template-areas: 12 | "logo nav actions" 13 | "search search search"; 14 | align-items: center; 15 | width: 100%; 16 | padding: 1rem 1rem; 17 | transition: background-color .2s ease-in-out; 18 | @include sm { 19 | grid-template-columns: auto 1fr auto auto; 20 | grid-template-areas: 21 | "logo nav search actions"; 22 | padding: 1rem 2rem; 23 | } 24 | 25 | .hamburger { 26 | z-index: $z-index-nav; 27 | display: block; 28 | margin-left: .5rem; 29 | transform: scale(.8); 30 | @include md { 31 | display: none; 32 | } 33 | } 34 | 35 | &--bg { 36 | background-color: $gray900; 37 | } 38 | 39 | &--un { 40 | position: absolute; 41 | top: 0; 42 | z-index: $z-index-header; 43 | display: flex; 44 | justify-content: space-between; 45 | align-items: center; 46 | width: 100%; 47 | padding: 1rem 1.5rem; 48 | @include lg { 49 | padding: 2rem 3rem; 50 | } 51 | } 52 | 53 | &__logo { 54 | grid-area: logo; 55 | width: 5.6rem; 56 | height: 1.7rem; 57 | fill: $red; 58 | @include sm { 59 | margin-right: 1rem; 60 | } 61 | &--un { 62 | @include sm { 63 | width: 10.5rem; 64 | height: 2.9rem; 65 | } 66 | } 67 | } 68 | 69 | &__nav { 70 | position: fixed; 71 | grid-area: nav; 72 | top: 0; 73 | right: 0; 74 | bottom: 0; 75 | z-index: $z-index-modal; 76 | width: 0; 77 | display: flex; 78 | justify-content: center; 79 | background-color: $gray900; 80 | transition: width .6s ease-in-out; 81 | overflow: hidden; 82 | @include md { 83 | z-index: unset; 84 | position: relative; 85 | display: block; 86 | background-color: transparent; 87 | overflow: visible; 88 | } 89 | &--opened { 90 | width: 100%; 91 | .Header__nav-list { 92 | transition: opacity .6s ease-in-out; 93 | opacity: 1; 94 | } 95 | } 96 | &-list { 97 | display: flex; 98 | flex-direction: column; 99 | align-items: center; 100 | width: 100%; 101 | padding: 5rem 1.5rem 2rem; 102 | text-align: center; 103 | opacity: 0; 104 | transition: opacity .3s ease-in-out; 105 | white-space: nowrap; 106 | @include md { 107 | flex-direction: row; 108 | padding: 0; 109 | text-align: left; 110 | opacity: 1; 111 | transition: none; 112 | } 113 | } 114 | &-item { 115 | margin-bottom: 1rem; 116 | @include md { 117 | margin-bottom: 0; 118 | &:not(:last-child) { 119 | margin-right: 1.25rem; 120 | } 121 | } 122 | &:hover { 123 | .dropdown { 124 | @include md { 125 | visibility: visible; 126 | opacity: 1; 127 | } 128 | } 129 | } 130 | } 131 | &-link { 132 | font-size: 1.25rem; 133 | color: $white; 134 | &:hover { 135 | color: $gray300; 136 | } 137 | @include md { 138 | font-size: 1rem; 139 | } 140 | } 141 | } 142 | 143 | &__actions { 144 | grid-area: actions; 145 | display: flex; 146 | align-items: center; 147 | justify-content: flex-end; 148 | } 149 | 150 | &__search { 151 | grid-area: search; 152 | padding: .35rem .5rem; 153 | background-color: $black; 154 | border: 1px solid $white; 155 | @include sm { 156 | background-color: transparent; 157 | border: none; 158 | } 159 | &--active, 160 | &:focus-within { 161 | background-color: $black; 162 | border: 1px solid $white; 163 | .Header__search-icon { 164 | margin-right: .75rem; 165 | } 166 | .Header__search-input { 167 | @include sm { 168 | width: 13rem; 169 | } 170 | @include md { 171 | width: 16rem; 172 | } 173 | } 174 | } 175 | &-icon { 176 | margin-right: .75rem; 177 | font-size: 1.1rem; 178 | cursor: pointer; 179 | @include sm { 180 | margin-right: 0; 181 | } 182 | } 183 | &-input { 184 | width: 100%; 185 | color: $white; 186 | font-size: .875rem; 187 | line-height: 1rem; 188 | background-color: transparent; 189 | border: 0; 190 | @include sm { 191 | width: 0; 192 | } 193 | &--active, 194 | &:focus { 195 | @include sm { 196 | width: 13rem; 197 | } 198 | @include md { 199 | width: 16rem; 200 | } 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/components/Header/Header.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 36 | -------------------------------------------------------------------------------- /src/components/Header/UnauthorizedHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /src/components/MovieDetails/MovieDetails.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | @import "../../assets/scss/helpers/variables"; 3 | 4 | .MovieDetails { 5 | width: 100%; 6 | min-height: 100vh; 7 | align-items: center; 8 | color: $white; 9 | display: flex; 10 | background-position: center top; 11 | background-repeat: no-repeat; 12 | background-size: cover; 13 | @include sm { 14 | min-height: 80vh; 15 | } 16 | @include xl { 17 | background-position: 25rem top; 18 | } 19 | &:before { 20 | content: ''; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | background: rgba(0, 0, 0, .75); 27 | @include xl { 28 | width: 60%; 29 | background: linear-gradient(90deg, $black 50%, transparent); 30 | } 31 | } 32 | 33 | &__fade { 34 | &--top, 35 | &--bottom { 36 | position: absolute; 37 | width: 100%; 38 | height: 30%; 39 | pointer-events: none; 40 | } 41 | &--top { 42 | top: 0; 43 | background-image: linear-gradient(0deg, transparent, rgba(37, 37, 37, .3), $black); 44 | } 45 | &--bottom { 46 | bottom: 0; 47 | background-image: linear-gradient(180deg, transparent, rgba(37, 37, 37, .3), $black); 48 | } 49 | } 50 | 51 | &__wrapper { 52 | width: 100%; 53 | padding: 1rem; 54 | @include sm { 55 | padding: 2rem; 56 | } 57 | @include md { 58 | padding: 3rem; 59 | } 60 | } 61 | 62 | &__details { 63 | width: 100%; 64 | @include md { 65 | width: 50%; 66 | } 67 | @include lg { 68 | width: 40%; 69 | } 70 | @include xl { 71 | width: 30%; 72 | } 73 | } 74 | 75 | &__title { 76 | margin: 0 0 1.5rem 0; 77 | font-size: 2rem; 78 | line-height: 2rem; 79 | @include sm { 80 | font-size: 2.75rem; 81 | } 82 | @include md { 83 | font-size: 3.5rem; 84 | line-height: 3rem; 85 | } 86 | } 87 | 88 | &__description { 89 | font-size: 1rem; 90 | color: $gray500; 91 | @include sm { 92 | font-size: 1.2rem; 93 | } 94 | &--white { 95 | color: $white; 96 | } 97 | } 98 | 99 | &__btn { 100 | display: flex; 101 | align-items: center; 102 | padding: .5rem 1.5rem .5rem 1rem !important; 103 | font-weight: bold; 104 | text-transform: uppercase; 105 | border: 1px solid $white; 106 | &:hover { 107 | color: $black; 108 | background-color: $white; 109 | } 110 | &-icon { 111 | margin-right: .25rem; 112 | padding: 2px; 113 | width: 20px; 114 | height: 20px; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/MovieDetails/MovieDetails.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 77 | 78 | 81 | -------------------------------------------------------------------------------- /src/components/MovieLabels/MovieLabels.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/variables"; 2 | 3 | .MovieLabels { 4 | &__rate { 5 | margin-right: .5rem; 6 | &--green { 7 | color: $green; 8 | } 9 | &--yellow { 10 | color: $yellow; 11 | } 12 | &--red { 13 | color: $red; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/MovieLabels/MovieLabels.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/components/MovieList/MovieList.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | @import "../../assets/scss/helpers/variables"; 3 | 4 | .MovieList { 5 | display: grid; 6 | grid-template-columns: repeat(1, 1fr); 7 | grid-gap: 1rem; 8 | margin-bottom: 1rem; 9 | 10 | @include sm { 11 | grid-template-columns: repeat(2, 1fr); 12 | } 13 | @include md { 14 | grid-template-columns: repeat(3, 1fr); 15 | } 16 | @include lg { 17 | grid-template-columns: repeat(4, 1fr); 18 | } 19 | @include xl { 20 | grid-template-columns: repeat(5, 1fr); 21 | } 22 | 23 | &__wrapper { 24 | position: relative; 25 | min-height: 75vh; 26 | .Spinner__overflow { 27 | background-color: transparent; 28 | } 29 | } 30 | 31 | &__details { 32 | width: 100vw; 33 | @include md { 34 | width: 80%; 35 | height: 80%; 36 | } 37 | .MovieDetails { 38 | min-height: 100vh; 39 | @include md { 40 | min-height: 80vh; 41 | } 42 | @include xl { 43 | background-position: 20rem top; 44 | } 45 | } 46 | &-backdrop { 47 | position: fixed; 48 | top: 0; 49 | bottom: 0; 50 | left: 0; 51 | right: 0; 52 | z-index: $z-index-modal; 53 | background-color: rgba(0, 0, 0, .85); 54 | display: flex; 55 | justify-content: center; 56 | align-items: center; 57 | } 58 | } 59 | 60 | &__empty { 61 | display: flex; 62 | font-size: 1.25rem; 63 | color: $gray600; 64 | @include md { 65 | justify-content: center; 66 | align-items: center; 67 | min-height: 200px; 68 | font-size: 1.5rem; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/MovieList/MovieList.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 124 | 125 | 128 | -------------------------------------------------------------------------------- /src/components/MovieListItem/MovieListItem.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | @import "../../assets/scss/helpers/variables"; 3 | 4 | .MovieListItem { 5 | height: 24rem; 6 | background-position: center top; 7 | background-repeat: no-repeat; 8 | background-size: cover; 9 | transition: all .3s ease-in-out; 10 | cursor: pointer; 11 | @include sm { 12 | height: 28rem; 13 | } 14 | 15 | &:hover { 16 | transform: scale(1.05); 17 | .MovieListItem__details { 18 | opacity: 1; 19 | } 20 | } 21 | 22 | &__details { 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: flex-end; 26 | height: 100%; 27 | width: 100%; 28 | padding: 1rem; 29 | opacity: 1; 30 | transition: all .3s ease-in-out; 31 | background-color: rgba(0, 0, 0, .7); 32 | @include sm { 33 | opacity: 0; 34 | } 35 | } 36 | 37 | &__title { 38 | margin: 0 0 .75rem 0; 39 | font-size: 1.5rem; 40 | } 41 | 42 | &__description { 43 | display: -webkit-box; 44 | -webkit-line-clamp: 3; 45 | -webkit-box-orient: vertical; 46 | overflow: hidden; 47 | color: $white; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/MovieListItem/MovieListItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 42 | 43 | 46 | -------------------------------------------------------------------------------- /src/components/MovieSlider/MovieSlider.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | @import "../../assets/scss/helpers/variables"; 3 | 4 | .MovieSlider { 5 | &__wrapper { 6 | padding: 0 1rem; 7 | @include sm { 8 | padding: 0 2rem; 9 | margin-bottom: 1.5rem; 10 | } 11 | .Slider { 12 | min-height: 13rem; 13 | } 14 | } 15 | 16 | &__details { 17 | position: fixed; 18 | top: 0; 19 | left: 0; 20 | width: 100%; 21 | min-height: 20rem; 22 | z-index: $z-index-modal; 23 | 24 | @include sm { 25 | position: absolute; 26 | top: calc(100% - 1rem); 27 | z-index: $z-index-dropdown; 28 | .MovieDetails { 29 | min-height: 30rem; 30 | } 31 | .btn--close { 32 | display: none; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/MovieSlider/MovieSlider.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 90 | 91 | 94 | -------------------------------------------------------------------------------- /src/components/MovieSliderItem/MovieSliderItem.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | @import "../../assets/scss/helpers/variables"; 3 | 4 | $slideBorderWidth: '3px'; 5 | $slideScale: '1.2'; 6 | 7 | .MovieSliderItem { 8 | margin: 0 .25rem; 9 | height: 10rem; 10 | background-position: top center; 11 | background-repeat: no-repeat; 12 | background-size: cover; 13 | border: #{$slideBorderWidth} solid transparent; 14 | 15 | &:after { 16 | content: none; 17 | position: absolute; 18 | top: calc(100% + #{$slideBorderWidth}); 19 | width: 0; 20 | height: 0; 21 | left: 50%; 22 | border-left: 1rem solid transparent; 23 | border-right: 1rem solid transparent; 24 | border-top: .75rem solid #fff; 25 | transform: translateX(-50%); 26 | } 27 | 28 | &__details { 29 | height: 100%; 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: flex-end; 33 | padding: .5rem 1rem; 34 | opacity: 1; 35 | transition: all .3s ease-in-out; 36 | background-image: linear-gradient(180deg, rgba(0, 0, 0, .7) 0%, transparent); 37 | @include sm { 38 | opacity: 0; 39 | visibility: hidden; 40 | } 41 | &:before { 42 | content: ''; 43 | position: absolute; 44 | bottom: -3px; 45 | left: 0; 46 | width: 100%; 47 | height: 40%; 48 | background-image: linear-gradient(0deg, rgba(0, 0, 0, .75) 0%, transparent); 49 | } 50 | .MovieLabels { 51 | font-size: .75rem; 52 | } 53 | } 54 | 55 | &__title { 56 | margin-bottom: .25rem; 57 | } 58 | 59 | &__btn { 60 | position: absolute; 61 | top: .5rem; 62 | right: 1rem; 63 | font-size: 1.25rem; 64 | color: $white; 65 | } 66 | } 67 | 68 | .MovieSlider { 69 | padding-bottom: 1rem; 70 | @include sm { 71 | &:hover { 72 | > * { 73 | transform: scale(1) translate3d(-2rem, 0, 0) !important; 74 | opacity: .3; 75 | } 76 | } 77 | } 78 | 79 | .Slider__slide { 80 | padding: .5rem 0; 81 | transition: all .3s ease-in-out !important; 82 | @include sm { 83 | padding: 1rem 0; 84 | &:hover { 85 | transform: scale(#{$slideScale}) !important; 86 | opacity: 1; 87 | .MovieSliderItem__details { 88 | visibility: visible; 89 | opacity: 1; 90 | } 91 | ~ .Slider__slide { 92 | transform: translate3d(2rem, 0 , 0) !important; 93 | } 94 | } 95 | &--active { 96 | &:hover { 97 | padding-left: 1.6rem; 98 | } 99 | .MovieSliderItem { 100 | margin-left: 0; 101 | } 102 | } 103 | &--last { 104 | &:hover { 105 | padding-right: 1.6rem; 106 | } 107 | .MovieSliderItem { 108 | margin-right: 0; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | .Slider--has-selected { 116 | z-index: 55; 117 | 118 | .MovieSlider { 119 | @include sm { 120 | > * { 121 | transform: scale(1) translate3d(-2rem, 0, 0) !important; 122 | } 123 | } 124 | 125 | .Slider__slide--selected { 126 | .MovieSliderItem { 127 | @include sm { 128 | border-color: $white; 129 | &:after { 130 | content: ''; 131 | } 132 | } 133 | .MovieSliderItem__details { 134 | visibility: visible; 135 | opacity: 1; 136 | &:before { 137 | bottom: 0; 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | .MovieSlider:not(:hover) { 145 | .Slider__slide { 146 | &--selected { 147 | transform: scale(#{$slideScale}) !important; 148 | ~ .Slider__slide:not(:hover) { 149 | transform: translate3d(2rem, 0, 0) !important; 150 | } 151 | } 152 | &--active.Slider__slide--selected { 153 | padding-left: 1.6rem; 154 | } 155 | &--last.Slider__slide--selected { 156 | padding-right: 1.6rem; 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/components/MovieSliderItem/MovieSliderItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 49 | 50 | 53 | -------------------------------------------------------------------------------- /src/components/Pagination/Pagination.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | @import './src/assets/scss/helpers/variables'; 3 | 4 | .Pagination { 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | @include sm { 9 | padding: 1rem; 10 | } 11 | 12 | &__list { 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | overflow: hidden; 17 | @include sm { 18 | border-radius: 2rem; 19 | background: $gray700; 20 | } 21 | } 22 | 23 | &__btn { 24 | padding: .5rem; 25 | color: $gray500; 26 | transition: all .2s ease-in-out; 27 | @include sm { 28 | padding: .5rem 1rem; 29 | } 30 | &--active { 31 | background: $red; 32 | color: $white; 33 | cursor: default; 34 | } 35 | &:not(:disabled):hover { 36 | color: $red; 37 | background-color: $white; 38 | } 39 | &:disabled { 40 | cursor: default; 41 | } 42 | } 43 | 44 | &__dots { 45 | display: none; 46 | margin: 0 .5rem; 47 | color: $gray500; 48 | @include sm { 49 | display: inline-flex; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Pagination/Pagination.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 82 | 83 | 86 | -------------------------------------------------------------------------------- /src/components/ProfileDropdown/ProfileDropdown.scss: -------------------------------------------------------------------------------- 1 | @import './src/assets/scss/helpers/variables'; 2 | 3 | .ProfileDropdown { 4 | display: flex; 5 | align-items: center; 6 | 7 | &:hover { 8 | .dropdown { 9 | visibility: visible; 10 | opacity: 1; 11 | } 12 | } 13 | 14 | &__avatar { 15 | width: 1.5rem; 16 | height: 1.5rem; 17 | margin-right: .25rem; 18 | border-radius: .25rem; 19 | background-color: $yellow; 20 | } 21 | &__arrow { 22 | padding: .25rem; 23 | } 24 | 25 | .dropdown { 26 | right: 0; 27 | padding: 0; 28 | transform: none; 29 | &:after { 30 | right: 1.5rem; 31 | transform: none; 32 | } 33 | &__btn { 34 | width: 100%; 35 | padding: .75rem 1rem; 36 | &:after { 37 | bottom: .5rem; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ProfileDropdown/ProfileDropdown.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /src/components/Slider/Slider.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | 3 | .Slider { 4 | height: 100%; 5 | 6 | &__list { 7 | display: block; 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | } 12 | 13 | &__track { 14 | display: flex; 15 | flex-direction: row; 16 | flex-wrap: nowrap; 17 | } 18 | 19 | &__actions { 20 | position: absolute; 21 | bottom: 0; 22 | display: flex; 23 | justify-content: space-between; 24 | width: 100%; 25 | padding: 1rem 0; 26 | } 27 | 28 | &__slides { 29 | display: flex; 30 | flex-direction: row; 31 | flex-grow: 1; 32 | flex-shrink: 0; 33 | flex-wrap: nowrap; 34 | align-items: center; 35 | justify-content: flex-start; 36 | } 37 | 38 | &__slide { 39 | display: block; 40 | flex-grow: 1; 41 | flex-shrink: 0; 42 | } 43 | 44 | &__dots { 45 | display: flex; 46 | align-items: center; 47 | white-space: nowrap; 48 | } 49 | 50 | &__dot button { 51 | display: block; 52 | width: 1rem; 53 | height: .25rem; 54 | margin: 0 .25rem; 55 | font-size: 0; 56 | line-height: 0; 57 | border: none; 58 | border-radius: .25rem; 59 | background-color: #555555; 60 | transition-duration: 0.3s; 61 | cursor: pointer; 62 | @include sm { 63 | width: 2rem; 64 | } 65 | } 66 | 67 | .Spinner__overflow { 68 | background-color: transparent; 69 | } 70 | 71 | } 72 | 73 | .Slider--rtl .Slider__track, 74 | .Slider--rtl .Slider__slides, 75 | .Slider--rtl .Slider__actions, 76 | .Slider--rtl .Slider__dots { 77 | flex-direction: row-reverse; 78 | } 79 | 80 | .Slider:focus, .Slider:active, .Slider *:focus, .Slider *:active { 81 | outline: none; 82 | } 83 | 84 | .Slider__dot--current button, .Slider__dot:hover button { 85 | background-color: #eee; 86 | } 87 | 88 | .Slider--disabled .Slider__slides { 89 | display: block; 90 | } 91 | 92 | .Slider--fade { 93 | .Slider__slide { 94 | opacity: 0; 95 | position: relative; 96 | z-index: 0; 97 | } 98 | .Slider__slide--active { 99 | opacity: 1; 100 | z-index: 2; 101 | } 102 | .Slider__slide--expiring { 103 | opacity: 1; 104 | transition-duration: 0s; 105 | z-index: 1; 106 | } 107 | } 108 | 109 | .Slider__nav-button[disabled] { 110 | cursor: default; 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/components/Slider/Slider.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 300 | 301 | 304 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.scss: -------------------------------------------------------------------------------- 1 | .Spinner { 2 | animation: rotate 2s linear infinite; 3 | width: 50px; 4 | height: 50px; 5 | 6 | .path { 7 | stroke: hsl(210, 70, 75); 8 | stroke-linecap: round; 9 | animation: dash 1.5s ease-in-out infinite; 10 | } 11 | 12 | &__overflow { 13 | position: absolute; 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | right: 0; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | background-color: rgba(0, 0, 0, .5); 22 | } 23 | } 24 | 25 | @keyframes rotate { 26 | 100% { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | @keyframes dash { 32 | 0% { 33 | stroke-dasharray: 1, 150; 34 | stroke-dashoffset: 0; 35 | } 36 | 50% { 37 | stroke-dasharray: 90, 150; 38 | stroke-dashoffset: -35; 39 | } 40 | 100% { 41 | stroke-dasharray: 90, 150; 42 | stroke-dashoffset: -124; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/directives/clickOutside.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind: (element, binding, vnode) => { 3 | element.clickOutsideEvent = event => { 4 | if (!(element === event.target || element.contains(event.target))) { 5 | vnode.context[binding.expression](event); 6 | } 7 | }; 8 | document.body.addEventListener('click', element.clickOutsideEvent) 9 | }, 10 | unbind: element => { 11 | document.body.removeEventListener('click', element.clickOutsideEvent) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/axiosInterceptors.js: -------------------------------------------------------------------------------- 1 | import axios from "axios/index"; 2 | 3 | axios.interceptors.request.use(function (config) { 4 | if (config.method === 'get') { 5 | if (typeof config.params !== 'undefined') { 6 | config.params = Object.assign(config.params, { 7 | api_key: process.env.VUE_APP_TMDB_API_KEY, 8 | }) 9 | } else { 10 | config.params = { 11 | api_key: process.env.VUE_APP_TMDB_API_KEY, 12 | } 13 | } 14 | } 15 | return config; 16 | }, function (error) { 17 | return Promise.reject(error); 18 | }); 19 | -------------------------------------------------------------------------------- /src/helpers/constants.js: -------------------------------------------------------------------------------- 1 | export const routes = { 2 | startNow: '/', 3 | signIn: '/sign-in', 4 | signUp: '/sign-up', 5 | recoverPassword: '/recover-password', 6 | recoverPasswordSuccess: '/recover-password/success', 7 | recoverPasswordCode: '/recover-password/:code', 8 | 9 | home: '/home', 10 | tvShows: '/tv-shows', 11 | movies: '/movies', 12 | popular: '/popular', 13 | myList: '/my-list', 14 | search: '/search', 15 | }; 16 | 17 | export const actions = { 18 | signUp: 'signUp', 19 | signIn: 'signIn', 20 | autoSignIn: 'autoSignIn', 21 | signInGoogle: 'signInGoogle', 22 | signInFacebook: 'signInFacebook', 23 | signInAnonymously: 'signInAnonymously', 24 | recoverPassword: 'recoverPassword', 25 | recoverPasswordWithEmail: 'recoverPasswordWithEmail', 26 | logout: 'logout', 27 | setUser: 'setUser', 28 | setLoading: 'setLoading', 29 | setGenres: 'setGenres', 30 | setConfiguration: 'setConfiguration', 31 | setError: 'setError', 32 | clearError: 'clearError', 33 | setMyList: 'setMyList', 34 | addMovieToMyList: 'addMovieToMyList', 35 | removeMovieFromMyList: 'removeMovieFromMyList', 36 | }; 37 | -------------------------------------------------------------------------------- /src/helpers/debounce.js: -------------------------------------------------------------------------------- 1 | function convertTime(time) { 2 | const [amt, t = 'ms'] = String(time).split(/(ms|s)/i); 3 | const types = { 4 | ms: 1, 5 | s: 1000, 6 | }; 7 | 8 | return Number(amt) * types[t]; 9 | } 10 | 11 | function debounce(fn, wait) { 12 | let timeout = null; 13 | const timer = typeof wait === 'number' ? wait : convertTime(wait); 14 | 15 | const debounced = function (...args) { 16 | const later = () => { 17 | timeout = null; 18 | fn.apply(this, args); 19 | }; 20 | 21 | clearTimeout(timeout); 22 | timeout = setTimeout(later, timer); 23 | 24 | if (!timeout) fn.apply(this, args); 25 | }; 26 | 27 | debounced.cancel = () => { 28 | clearTimeout(timeout); 29 | timeout = null; 30 | }; 31 | 32 | return debounced; 33 | } 34 | 35 | export default debounce; -------------------------------------------------------------------------------- /src/helpers/fontawesome.js: -------------------------------------------------------------------------------- 1 | import { library} from '@fortawesome/fontawesome-svg-core'; 2 | import { 3 | faSortDown, 4 | faPlus, 5 | faMinus, 6 | faPlay, 7 | faChevronDown, 8 | faChevronRight, 9 | faChevronLeft, 10 | faPlayCircle, 11 | faPlusCircle, 12 | faInfoCircle, 13 | faSearch, 14 | } from '@fortawesome/free-solid-svg-icons'; 15 | import { 16 | faFacebook, 17 | faGoogle, 18 | } from '@fortawesome/free-brands-svg-icons'; 19 | 20 | library.add( 21 | faFacebook, 22 | faGoogle, 23 | faSortDown, 24 | faPlus, 25 | faMinus, 26 | faPlay, 27 | faChevronDown, 28 | faChevronRight, 29 | faChevronLeft, 30 | faPlayCircle, 31 | faPlusCircle, 32 | faInfoCircle, 33 | faSearch, 34 | ); 35 | -------------------------------------------------------------------------------- /src/helpers/getImageUrl.js: -------------------------------------------------------------------------------- 1 | import { store } from "../store/index.js"; 2 | 3 | function getImageUrl(url, size = 3, type) { 4 | const config = store.getters.configuration; 5 | if (!config) return null; 6 | if (type === 'backdrop') { 7 | return config.images.base_url + config.images.backdrop_sizes[size] + url; 8 | } else { 9 | return config.images.base_url + config.images.poster_sizes[size] + url; 10 | } 11 | } 12 | 13 | export default getImageUrl; 14 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | //helpers 2 | @import "assets/scss/helpers/breakpoints"; 3 | @import "assets/scss/helpers/variables"; 4 | @import "assets/scss/helpers/settings"; 5 | @import "assets/scss/helpers/default"; 6 | @import "assets/scss/helpers/mixins"; 7 | @import "assets/scss/helpers/normalize"; 8 | 9 | //ui 10 | @import "assets/scss/ui/input"; 11 | @import "assets/scss/ui/button"; 12 | @import "assets/scss/ui/checkbox"; 13 | @import "assets/scss/ui/link"; 14 | @import "assets/scss/ui/tile"; 15 | @import "assets/scss/ui/form"; 16 | @import "assets/scss/ui/hamburger"; 17 | @import "assets/scss/ui/dropdown"; 18 | 19 | //transitions 20 | @import "assets/scss/transitions/fade/index.scss"; 21 | @import "assets/scss/transitions/zoom/index.scss"; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router/index"; 4 | import { store } from "./store/index"; 5 | import * as firebase from 'firebase'; 6 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; 7 | import { actions } from "./helpers/constants"; 8 | import './index.scss'; 9 | import './helpers/fontawesome'; 10 | import './helpers/axiosInterceptors'; 11 | 12 | Vue.component('font-awesome-icon', FontAwesomeIcon); 13 | Vue.config.productionTip = false; 14 | 15 | let app = ''; 16 | 17 | firebase.initializeApp({ 18 | apiKey: process.env.VUE_APP_FIREBASE_API_KEY, 19 | authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN, 20 | projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID, 21 | storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_BUCKET, 22 | messagingSenderId: process.env.VUE_APP_FIREBASE_MESSAGING_SENDER_ID, 23 | appId: process.env.VUE_APP_FIREBASE_APP_ID, 24 | }); 25 | 26 | firebase.auth().onAuthStateChanged(user => { 27 | if (!app) { 28 | app = new Vue({ 29 | router, 30 | store, 31 | render: h => h(App), 32 | created() { 33 | if (user) { 34 | this.$store.dispatch(actions.autoSignIn, user); 35 | } 36 | } 37 | }).$mount('#app'); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/pages/Home/Home.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/helpers/breakpoints"; 2 | 3 | .Home { 4 | &__main-slider { 5 | height: 100vh; 6 | @include sm { 7 | height: 80vh; 8 | } 9 | .MovieDetails { 10 | padding-top: 3rem; 11 | } 12 | } 13 | 14 | &__slider-list { 15 | padding: 2rem 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/Home/Home.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 69 | 70 | 73 | -------------------------------------------------------------------------------- /src/pages/Movies/Movies.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 57 | -------------------------------------------------------------------------------- /src/pages/MyList/MyList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /src/pages/Popular/Popular.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /src/pages/RecoverPassword/RecoverPassword.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 87 | -------------------------------------------------------------------------------- /src/pages/RecoverPassword/RecoverPasswordForm.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 160 | -------------------------------------------------------------------------------- /src/pages/RecoverPassword/RecoverPasswordSuccess.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/pages/Search/Search.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /src/pages/SignIn/SignIn.scss: -------------------------------------------------------------------------------- 1 | @import './src/assets/scss/helpers/variables'; 2 | 3 | .SignIn { 4 | .SignIn { 5 | &__social { 6 | &-list { 7 | margin-top: 5rem; 8 | margin-bottom: 1rem; 9 | } 10 | &-btn { 11 | display: flex; 12 | color: #737373; 13 | font-size: .9rem; 14 | background-color: transparent; 15 | padding: .25rem 0; 16 | } 17 | &-icon { 18 | font-size: 1rem; 19 | margin-right: .5rem; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/SignIn/SignIn.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 185 | 186 | 189 | -------------------------------------------------------------------------------- /src/pages/SignUp/SignUp.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 170 | -------------------------------------------------------------------------------- /src/pages/StartNow/StartNow.scss: -------------------------------------------------------------------------------- 1 | @import './src/assets/scss/helpers/breakpoints'; 2 | @import './src/assets/scss/helpers/variables'; 3 | 4 | .StartNow { 5 | position: relative; 6 | min-height: 100vh; 7 | padding: 1rem 1.5rem; 8 | 9 | &:before, 10 | &:after { 11 | content: ''; 12 | position: absolute; 13 | left: 0; 14 | right: 0; 15 | z-index: 10; 16 | } 17 | 18 | &:after { 19 | bottom: 0; 20 | height: 50%; 21 | background-image: linear-gradient(to top,rgba(0,0,0,1) 30%, rgba(0,0,0,.8) 60%, rgba(0,0,0,.6) 70%, rgba(0,0,0,0) 100%); 22 | @include md { 23 | content: none; 24 | } 25 | } 26 | 27 | &:before { 28 | top: 0; 29 | height: 10%; 30 | background-image: linear-gradient(to bottom,rgba(0,0,0,.6) 50%,rgba(0,0,0,0) 95%); 31 | @include md { 32 | height: 100%; 33 | background: rgba(0, 0, 0, .3) linear-gradient(0deg, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, .5) 100%), 34 | radial-gradient(50% 120% at 0 0, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, .7) 100%); 35 | } 36 | } 37 | } 38 | 39 | .StartNow { 40 | .StartNow { 41 | &__container { 42 | position: absolute; 43 | bottom: 10%; 44 | left: .5rem; 45 | right: .5rem; 46 | text-align: center; 47 | z-index: 15; 48 | @include md { 49 | top: 30%; 50 | left: 1.5rem; 51 | right: 1.5rem; 52 | bottom: auto; 53 | } 54 | } 55 | 56 | &__title { 57 | position: relative; 58 | margin-bottom: 1rem; 59 | font-size: 2rem; 60 | font-weight: bold; 61 | color: $white; 62 | @include sm { 63 | margin-bottom: 1.5rem; 64 | font-size: 3.75rem; 65 | } 66 | @include lg { 67 | margin-bottom: 2.2rem; 68 | font-size: 5rem; 69 | } 70 | } 71 | 72 | &__subtitle { 73 | margin-bottom: 1.5rem; 74 | font-weight: normal; 75 | font-size: 1.2rem; 76 | color: $white; 77 | text-transform: uppercase; 78 | @include sm { 79 | margin-bottom: 2.5rem; 80 | font-size: 1.5rem; 81 | } 82 | @include lg { 83 | margin-bottom: 2.6rem; 84 | font-size: 1.8rem; 85 | } 86 | } 87 | 88 | &__btn { 89 | padding: 1rem 1.5rem; 90 | font-size: 1.2rem; 91 | text-transform: uppercase; 92 | @include sm { 93 | padding: 1.3rem 3.5rem; 94 | font-size: 1.5rem; 95 | } 96 | @include lg { 97 | padding: 1.5rem 3.75rem; 98 | font-size: 1.8rem; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/pages/StartNow/StartNow.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/pages/TVShows/TVShows.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 57 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import * as firebase from 'firebase'; 2 | import Vue from 'vue'; 3 | import Router from 'vue-router'; 4 | import StartNow from '@/pages/StartNow/StartNow'; 5 | import SignIn from '@/pages/SignIn/SignIn'; 6 | import SignUp from '@/pages/SignUp/SignUp'; 7 | import RecoverPassword from '@/pages/RecoverPassword/RecoverPassword'; 8 | import RecoverPasswordSuccess from '@/pages/RecoverPassword/RecoverPasswordSuccess'; 9 | import RecoverPasswordForm from '@/pages/RecoverPassword/RecoverPasswordForm'; 10 | import Home from '@/pages/Home/Home'; 11 | import Movies from '@/pages/Movies/Movies'; 12 | import TVShows from '@/pages/TVShows/TVShows'; 13 | import Popular from '@/pages/Popular/Popular'; 14 | import MyList from '@/pages/MyList/MyList'; 15 | import Search from '@/pages/Search/Search'; 16 | import { routes } from '../helpers/constants'; 17 | 18 | Vue.use(Router); 19 | 20 | const router = new Router({ 21 | mode: "history", 22 | routes: [ 23 | { 24 | path: '*', 25 | redirect: '/', 26 | }, 27 | { 28 | path: routes.startNow, 29 | name: 'StartNow', 30 | component: StartNow, 31 | }, 32 | { 33 | path: routes.signIn, 34 | name: 'SignIn', 35 | component: SignIn, 36 | }, 37 | { 38 | path: routes.signUp, 39 | name: 'SignUp', 40 | component: SignUp, 41 | }, 42 | { 43 | path: routes.recoverPassword, 44 | name: 'RecoverPassword', 45 | component: RecoverPassword, 46 | }, 47 | { 48 | path: routes.recoverPasswordSuccess, 49 | name: 'RecoverPasswordSuccess', 50 | component: RecoverPasswordSuccess, 51 | }, 52 | { 53 | path: routes.recoverPasswordCode, 54 | name: 'RecoverPasswordForm', 55 | component: RecoverPasswordForm, 56 | }, 57 | { 58 | path: routes.home, 59 | name: 'Home', 60 | component: Home, 61 | meta: { 62 | requiresAuth: true, 63 | }, 64 | }, 65 | { 66 | path: `${routes.movies}/:id` , 67 | name: 'Movies', 68 | component: Movies, 69 | meta: { 70 | requiresAuth: true, 71 | }, 72 | }, 73 | { 74 | path: `${routes.tvShows}/:id`, 75 | name: 'TVShows', 76 | component: TVShows, 77 | meta: { 78 | requiresAuth: true, 79 | }, 80 | }, 81 | { 82 | path: routes.popular, 83 | name: 'Popular', 84 | component: Popular, 85 | meta: { 86 | requiresAuth: true, 87 | }, 88 | }, 89 | { 90 | path: routes.myList, 91 | name: 'My List', 92 | component: MyList, 93 | meta: { 94 | requiresAuth: true, 95 | }, 96 | }, 97 | { 98 | path: `${routes.search}/:search`, 99 | name: 'Search', 100 | component: Search, 101 | meta: { 102 | requiresAuth: true, 103 | }, 104 | }, 105 | ], 106 | }); 107 | 108 | router.beforeEach((to, from, next) => { 109 | const currentUser = firebase.auth().currentUser; 110 | const requiresAuth = to.matched.some(record => record.meta.requiresAuth); 111 | 112 | if (requiresAuth && !currentUser) next(routes.signIn); 113 | else if (!requiresAuth && currentUser) next(routes.home); 114 | else next(); 115 | }); 116 | 117 | export default router; 118 | -------------------------------------------------------------------------------- /src/services/SliderService.js: -------------------------------------------------------------------------------- 1 | // Component props 2 | export const props = { 3 | props: { 4 | arrows: { 5 | type: Boolean, 6 | default: true, 7 | }, 8 | 9 | // Set the carousel to be the navigation of other carousels 10 | asNavFor: { 11 | type: Array, 12 | default: function() { 13 | return []; 14 | }, 15 | }, 16 | 17 | // Enable autoplay 18 | autoplay: { 19 | type: Boolean, 20 | default: false, 21 | }, 22 | 23 | // Autoplay interval in milliseconds 24 | autoplaySpeed: { 25 | type: Number, 26 | default: 3000, 27 | }, 28 | 29 | // Enable centered view when slidesToShow > 1 30 | centerMode: { 31 | type: Boolean, 32 | default: false, 33 | }, 34 | 35 | // Slides padding in center mode 36 | centerPadding: { 37 | type: String, 38 | default: '15%', 39 | }, 40 | 41 | // Enable dot indicators/pagination 42 | dots: { 43 | type: Boolean, 44 | default: true, 45 | }, 46 | 47 | // Enable fade effect 48 | fade: { 49 | type: Boolean, 50 | default: false, 51 | }, 52 | 53 | // Infinite loop sliding 54 | infinite: { 55 | type: Boolean, 56 | default: true, 57 | }, 58 | 59 | // Index of slide to start on 60 | initialSlide: { 61 | type: Number, 62 | default: 0, 63 | }, 64 | 65 | // Enable mobile first calculation for responsive settings 66 | mobileFirst: { 67 | type: Boolean, 68 | default: true, 69 | }, 70 | 71 | // Enable prev/next navigation buttons 72 | navButtons: { 73 | type: Boolean, 74 | default: true, 75 | }, 76 | 77 | // Depreciated 78 | nextArrow: { 79 | type: String, 80 | default: null, 81 | }, 82 | 83 | // All settings as one object 84 | options: { 85 | type: Object, 86 | default: () => null, 87 | }, 88 | 89 | // Pause autoplay when a dot is hovered 90 | pauseOnDotsHover: { 91 | type: Boolean, 92 | default: false, 93 | }, 94 | 95 | // Pause autoplay when a slide is hovered 96 | pauseOnHover: { 97 | type: Boolean, 98 | default: true, 99 | }, 100 | 101 | // Depreciated 102 | prevArrow: { 103 | type: String, 104 | default: null, 105 | }, 106 | 107 | // Object containing breakpoints and settings objects 108 | responsive: { 109 | type: Array, 110 | default: () => null, 111 | }, 112 | 113 | // Enable right-to-left mode 114 | rtl: { 115 | type: Boolean, 116 | default: false, 117 | }, 118 | 119 | // Number of slides to scroll 120 | slidesToScroll: { 121 | type: Number, 122 | default: 1, 123 | }, 124 | 125 | // Number of slides to show 126 | slidesToShow: { 127 | type: Number, 128 | default: 1, 129 | }, 130 | 131 | // Slide animation speed in milliseconds 132 | speed: { 133 | type: Number, 134 | default: 300, 135 | }, 136 | 137 | // Transition timing function 138 | // Available: ease, linear, ease-in, ease-out, ease-in-out 139 | timing: { 140 | type: String, 141 | default: 'ease', 142 | }, 143 | 144 | // Disable Agile carousel 145 | disabled: { 146 | type: Boolean, 147 | default: false, 148 | } 149 | }, 150 | 151 | data() { 152 | return { 153 | initialSettings: { 154 | asNavFor: this.asNavFor, 155 | autoplay: this.autoplay, 156 | autoplaySpeed: this.autoplaySpeed, 157 | centerMode: this.centerMode, 158 | centerPadding: this.centerPadding, 159 | dots: this.dots, 160 | fade: this.fade, 161 | infinite: this.infinite, 162 | initialSlide: this.initialSlide, 163 | mobileFirst: this.mobileFirst, 164 | navButtons: this.navButtons, 165 | pauseOnDotsHover: this.pauseOnDotsHover, 166 | pauseOnHover: this.pauseOnHover, 167 | responsive: this.responsive, 168 | rtl: this.rtl, 169 | slidesToScroll: this.slidesToScroll, 170 | slidesToShow: this.slidesToShow, 171 | speed: this.speed, 172 | timing: this.timing, 173 | disabled: this.disabled, 174 | } 175 | } 176 | }, 177 | }; 178 | 179 | // Handlers methods for mouse/touch events 180 | export const handlers = { 181 | methods: { 182 | handleMouseDown(e) { 183 | if (!e.touches) e.preventDefault(); 184 | this.mouseDown = true; 185 | this.dragStartX = ('ontouchstart' in window) ? e.touches[0].clientX : e.clientX; 186 | this.dragStartY = ('ontouchstart' in window) ? e.touches[0].clientY : e.clientY; 187 | }, 188 | 189 | handleMouseMove(e) { 190 | let positionX = ('ontouchstart' in window) ? e.touches[0].clientX : e.clientX; 191 | let positionY = ('ontouchstart' in window) ? e.touches[0].clientY : e.clientY; 192 | let dragDistanceX = Math.abs(positionX - this.dragStartX); 193 | let dragDistanceY = Math.abs(positionY - this.dragStartY); 194 | if (dragDistanceX > 3 * dragDistanceY) { 195 | this.disableScroll(); 196 | this.dragDistance = positionX - this.dragStartX; 197 | } 198 | }, 199 | 200 | handleMouseUp() { 201 | this.mouseDown = false; 202 | this.enableScroll(); 203 | }, 204 | 205 | handleMouseOver(element) { 206 | const { autoplay, pauseOnDotsHover, pauseOnHover } = this.settings; 207 | if (!autoplay) return; 208 | if ((element === 'dot' && pauseOnDotsHover) || (element === 'track' && pauseOnHover)) { 209 | this.pauseAutoPlay = true; 210 | } 211 | }, 212 | 213 | handleMouseOut(element) { 214 | const { autoplay, pauseOnDotsHover, pauseOnHover } = this.settings; 215 | if (!autoplay) return; 216 | if ((element === 'dot' && pauseOnDotsHover) || (element === 'track' && pauseOnHover)) { 217 | this.pauseAutoPlay = false; 218 | } 219 | } 220 | } 221 | }; 222 | 223 | // Helpers methods 224 | export const helpers = { 225 | methods: { 226 | htmlCollectionToArray(collection) { 227 | return Array.prototype.slice.call(collection, 0); 228 | }, 229 | 230 | compare(a, b) { 231 | if (a.breakpoint < b.breakpoint) return (this.initialSettings.mobileFirst) ? -1 : 1; 232 | if (a.breakpoint > b.breakpoint) return (this.initialSettings.mobileFirst) ? 1 : -1; 233 | return 0; 234 | }, 235 | 236 | getWidth() { 237 | this.widthWindow = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; 238 | this.widthContainer = this.$refs.list.clientWidth; 239 | } 240 | } 241 | }; 242 | 243 | // Carousel preparation methods 244 | export const preparations = { 245 | methods: { 246 | prepareSettings() { 247 | const { responsive, mobileFirst } = this.initialSettings; 248 | if (!responsive) { 249 | this.toggleFade(); 250 | this.toggleAutoPlay(); 251 | return false; 252 | } 253 | 254 | let newSettings = Object.assign({}, this.initialSettings); 255 | delete newSettings.responsive; 256 | 257 | responsive.forEach(option => { 258 | if (mobileFirst ? option.breakpoint < this.widthWindow : option.breakpoint > this.widthWindow) { 259 | for (let key in option.settings) { 260 | newSettings[key] = option.settings[key]; 261 | } 262 | } 263 | }); 264 | 265 | this.settings = Object.assign({}, newSettings); 266 | }, 267 | 268 | prepareSlides() { 269 | this.slides = this.htmlCollectionToArray(this.$refs.slides.children); 270 | 271 | if (this.clonedSlides) { 272 | this.slidesClonedBefore = this.htmlCollectionToArray(this.$refs.slidesClonedBefore.children); 273 | this.slidesClonedAfter = this.htmlCollectionToArray(this.$refs.slidesClonedAfter.children); 274 | } 275 | 276 | for (let slide of this.allSlides) { 277 | slide.classList.add('Slider__slide'); 278 | } 279 | }, 280 | 281 | prepareSlidesClasses() { 282 | if (this.currentSlide === null) return false; 283 | for (let i = 0; i < this.slidesCount; i++) { 284 | this.slides[i].classList.remove('Slider__slide--active'); 285 | this.slides[i].classList.remove('Slider__slide--current'); 286 | this.slides[i].classList.remove('Slider__slide--last'); 287 | } 288 | 289 | this.slides[this.currentSlide].classList.add('Slider__slide--active'); 290 | this.slides[this.currentSlide + this.options.slidesToShow - 1].classList.add('Slider__slide--last'); 291 | 292 | let start = (this.clonedSlides) ? this.slidesCount + this.currentSlide : this.currentSlide; 293 | 294 | if (this.centerMode) { 295 | start -= (Math.floor(this.settings.slidesToShow / 2) - +(this.settings.slidesToShow % 2 === 0)); 296 | } 297 | 298 | for (let i = Math.max(start, 0); i < Math.min(start + this.settings.slidesToShow, this.slidesCount); i++) { 299 | this.allSlides[i].classList.add('Slider__slide--current'); 300 | } 301 | }, 302 | 303 | prepareCarousel() { 304 | this.widthSlide = !this.settings.disabled ? this.widthContainer / this.settings.slidesToShow : 'auto'; 305 | 306 | for (let i = 0; i < this.allSlidesCount; i++) { 307 | this.allSlides[i].style.width = this.widthSlide + 'px'; 308 | } 309 | 310 | if (this.settings.disabled) { 311 | this.translateX = 0; 312 | } else { 313 | if (this.currentSlide === null && this.slidesCount) { 314 | this.currentSlide = this.settings.initialSlide; 315 | } 316 | 317 | this.goTo(this.currentSlide, false, false); 318 | } 319 | } 320 | } 321 | }; 322 | 323 | // Component watchers 324 | export const watchers = { 325 | watch: { 326 | widthWindow(newValue, oldValue) { 327 | if (oldValue) { 328 | this.prepareCarousel(); 329 | this.toggleFade(); 330 | } 331 | }, 332 | 333 | currentSlide() { 334 | this.prepareSlidesClasses(); 335 | this.autoplayStart = (this.settings.autoplay) ? +new Date() : null; 336 | this.$emit('afterChange', { currentSlide: this.currentSlide }); 337 | }, 338 | 339 | currentBreakpoint() { 340 | this.prepareSettings(); 341 | this.$emit('breakpoint', { breakpoint: this.currentBreakpoint }); 342 | }, 343 | 344 | dragDistance() { 345 | if (this.mouseDown) { 346 | const { rtl } = this.settings; 347 | const dragDistance = this.dragDistance * (rtl ? -1 : 1); 348 | 349 | if (dragDistance > this.swipeDistance && this.canGoToPrev) { 350 | this.goToPrev(); 351 | this.handleMouseUp(); 352 | } 353 | 354 | if (dragDistance < -1 * this.swipeDistance && this.canGoToNext) { 355 | this.goToNext(); 356 | this.handleMouseUp(); 357 | } 358 | } 359 | }, 360 | 361 | 'settings.fade'() { 362 | this.toggleFade(); 363 | }, 364 | 365 | 'settings.autoplay'() { 366 | this.toggleAutoPlay(); 367 | }, 368 | 369 | pauseAutoPlay(nevValue) { 370 | if (nevValue) { 371 | this.remaining = this.settings.autoplaySpeed - (+new Date() - this.autoplayStart); 372 | this.disableAutoPlay(); 373 | this.clearAutoPlayPause(); 374 | } else { 375 | this.autoplayTimeout = setTimeout(() => { 376 | this.clearAutoPlayPause(); 377 | this.goToNext(); 378 | this.toggleAutoPlay(); 379 | }, this.remaining) 380 | } 381 | } 382 | }, 383 | }; 384 | 385 | 386 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import user from "./user"; 4 | import shared from "./shared"; 5 | import myList from "./myList"; 6 | 7 | Vue.use(Vuex); 8 | 9 | export const store = new Vuex.Store({ 10 | modules: { 11 | user: user, 12 | myList: myList, 13 | shared: shared, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/store/myList/index.js: -------------------------------------------------------------------------------- 1 | import { actions } from '../../helpers/constants'; 2 | 3 | export default { 4 | state: { 5 | myList: [], 6 | }, 7 | mutations: { 8 | setMyList(state, payload) { 9 | state.myList = payload; 10 | }, 11 | }, 12 | actions: { 13 | addMovieToMyList({ commit, state }, payload) { 14 | commit(actions.setMyList, [payload.movie, ...state.myList]); 15 | }, 16 | removeMovieFromMyList({ commit, state }, payload) { 17 | commit(actions.setMyList, state.myList.filter(({ id }) => id !== payload.movie.id)); 18 | }, 19 | }, 20 | getters: { 21 | myList(state) { 22 | return state.myList; 23 | } 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/store/shared/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { actions } from '../../helpers/constants'; 3 | 4 | export default { 5 | state: { 6 | loading: false, 7 | configuration: null, 8 | genres: null, 9 | error: null, 10 | }, 11 | mutations: { 12 | setLoading(state, payload) { 13 | state.loading = payload; 14 | }, 15 | setError(state, payload) { 16 | state.error = payload; 17 | }, 18 | clearError(state) { 19 | state.error = null; 20 | }, 21 | setConfiguration(state, payload) { 22 | state.configuration = payload; 23 | }, 24 | setGenres(state, payload) { 25 | state.genres = payload; 26 | }, 27 | }, 28 | actions: { 29 | clearError({ commit }) { 30 | commit(actions.clearError); 31 | }, 32 | 33 | setError({ commit }, payload) { 34 | commit(actions.setError, payload); 35 | }, 36 | 37 | setConfiguration({ commit }) { 38 | axios.get('https://api.themoviedb.org/3/configuration') 39 | .then(response => { 40 | commit(actions.setConfiguration, response.data); 41 | }) 42 | .catch(error => { 43 | commit(actions.setError, error); 44 | }); 45 | }, 46 | 47 | async setGenres ({ commit }) { 48 | const genres = { 49 | tv: [], 50 | movies: [] 51 | }; 52 | 53 | await axios.get('https://api.themoviedb.org/3/genre/movie/list') 54 | .then(response => { 55 | genres.movies = response.data.genres; 56 | }) 57 | .catch(error => { 58 | commit(actions.setError, error); 59 | }); 60 | 61 | await axios.get('https://api.themoviedb.org/3/genre/tv/list') 62 | .then(response => { 63 | genres.tv = response.data.genres; 64 | }) 65 | .catch(error => { 66 | commit(actions.setError, error); 67 | }); 68 | 69 | commit(actions.setGenres, genres); 70 | } 71 | }, 72 | getters: { 73 | configuration(state) { 74 | return state.configuration; 75 | }, 76 | genres(state) { 77 | return state.genres; 78 | }, 79 | loading(state) { 80 | return state.loading; 81 | }, 82 | error(state) { 83 | return state.error; 84 | }, 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /src/store/user/index.js: -------------------------------------------------------------------------------- 1 | import * as firebase from "firebase"; 2 | import router from '../../router/index'; 3 | import { routes, actions } from '../../helpers/constants'; 4 | 5 | export default { 6 | state: { 7 | user: null, 8 | }, 9 | mutations: { 10 | setUser(state, payload) { 11 | state.user = payload; 12 | }, 13 | }, 14 | actions: { 15 | signUp({ commit }, payload) { 16 | commit(actions.setLoading, true); 17 | commit(actions.clearError); 18 | firebase 19 | .auth() 20 | .createUserWithEmailAndPassword(payload.email, payload.password) 21 | .then(user => { 22 | commit(actions.setLoading, false); 23 | const newUser = { 24 | id: user.uid, 25 | name: user.displayName, 26 | email: user.email, 27 | photoUrl: user.photoURL, 28 | }; 29 | commit(actions.setUser, newUser); 30 | this.dispatch(actions.setConfiguration); 31 | }) 32 | .catch(error => { 33 | commit(actions.setLoading, false); 34 | commit(actions.setError, error); 35 | }); 36 | }, 37 | 38 | signIn({ commit }, payload) { 39 | commit(actions.setLoading, true); 40 | commit(actions.clearError); 41 | firebase 42 | .auth() 43 | .signInWithEmailAndPassword(payload.email, payload.password) 44 | .then(user => { 45 | commit(actions.setLoading, false); 46 | this.dispatch(actions.setConfiguration); 47 | const newUser = { 48 | id: user.uid, 49 | name: user.displayName, 50 | email: user.email, 51 | photoUrl: user.photoURL, 52 | }; 53 | commit(actions.setUser, newUser); 54 | }) 55 | .catch(error => { 56 | commit(actions.setLoading, false); 57 | commit(actions.setError, error); 58 | }); 59 | }, 60 | 61 | autoSignIn({ commit }, payload) { 62 | this.dispatch(actions.setConfiguration); 63 | commit(actions.setUser, { 64 | id: payload.uid, 65 | name: payload.displayName, 66 | email: payload.email, 67 | photoUrl: payload.photoURL, 68 | }); 69 | }, 70 | 71 | signInGoogle({ commit }) { 72 | commit(actions.setLoading, true); 73 | commit(actions.clearError); 74 | firebase 75 | .auth() 76 | .signInWithPopup(new firebase.auth.GoogleAuthProvider()) 77 | .then(user => { 78 | commit(actions.setLoading, false); 79 | this.dispatch(actions.setConfiguration); 80 | const newUser = { 81 | id: user.uid, 82 | name: user.displayName, 83 | email: user.email, 84 | photoUrl: user.photoURL, 85 | }; 86 | commit(actions.setUser, newUser); 87 | }) 88 | .catch(error => { 89 | commit(actions.setLoading, false); 90 | commit(actions.setError, error); 91 | }); 92 | }, 93 | 94 | signInFacebook({ commit }) { 95 | commit(actions.setLoading, true); 96 | commit(actions.clearError); 97 | firebase 98 | .auth() 99 | .signInWithPopup(new firebase.auth.FacebookAuthProvider()) 100 | .then(user => { 101 | commit(actions.setLoading, false); 102 | this.dispatch(actions.setConfiguration); 103 | const newUser = { 104 | id: user.uid, 105 | name: user.displayName, 106 | email: user.email, 107 | photoUrl: user.photoURL, 108 | }; 109 | commit(actions.setUser, newUser); 110 | }) 111 | .catch(error => { 112 | commit(actions.setLoading, false); 113 | commit(actions.setError, error); 114 | }); 115 | }, 116 | 117 | signInAnonymously({ commit }) { 118 | commit(actions.setLoading, true); 119 | commit(actions.clearError); 120 | firebase 121 | .auth() 122 | .signInAnonymously() 123 | .then(user => { 124 | commit(actions.setLoading, false); 125 | this.dispatch(actions.setConfiguration); 126 | const newUser = { 127 | id: user.uid, 128 | name: user.displayName, 129 | email: user.email, 130 | photoUrl: user.photoURL, 131 | }; 132 | commit(actions.setUser, newUser); 133 | }) 134 | .catch(error => { 135 | commit(actions.setLoading, false); 136 | commit(actions.setError, error); 137 | }); 138 | }, 139 | 140 | recoverPasswordWithEmail({ commit }, payload) { 141 | commit(actions.setLoading, true); 142 | commit(actions.clearError); 143 | firebase 144 | .auth() 145 | .sendPasswordResetEmail(payload.email) 146 | .then(() => { 147 | commit(actions.setLoading, false); 148 | router.push(routes.recoverPasswordSuccess) 149 | }) 150 | .catch(error => { 151 | commit(actions.setLoading, false); 152 | commit(actions.setError, error); 153 | }); 154 | }, 155 | 156 | recoverPassword({ commit }, payload) { 157 | commit(actions.setLoading, true); 158 | commit(actions.clearError); 159 | firebase 160 | .auth() 161 | .confirmPasswordReset(payload.code, payload.newPassword) 162 | .then(user => { 163 | commit(actions.setLoading, false); 164 | router.push(routes.home); 165 | this.dispatch(actions.setConfiguration); 166 | const newUser = { 167 | id: user.uid, 168 | name: user.displayName, 169 | email: user.email, 170 | photoUrl: user.photoURL, 171 | }; 172 | commit(actions.setUser, newUser); 173 | }) 174 | .catch(error => { 175 | commit(actions.setLoading, false); 176 | commit(actions.setError, error); 177 | }); 178 | }, 179 | 180 | logout({ commit }) { 181 | firebase 182 | .auth() 183 | .signOut() 184 | .then(() => { 185 | commit(actions.setUser, null); 186 | router.push(routes.startNow); 187 | }); 188 | }, 189 | }, 190 | getters: { 191 | user(state) { 192 | return state.user; 193 | }, 194 | }, 195 | }; 196 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runtimeCompiler: true, 3 | productionSourceMap: false, 4 | chainWebpack: config => { 5 | const svgRule = config.module.rule('svg'); 6 | 7 | svgRule.uses.clear(); 8 | 9 | svgRule 10 | .use('vue-svg-loader') 11 | .loader('vue-svg-loader') 12 | .options({ 13 | svgo: { 14 | plugins: [{ removeViewBox: false }] 15 | } 16 | }); 17 | }, 18 | }; 19 | --------------------------------------------------------------------------------