Compare commits
	
		
			351 Commits
		
	
	
		
			feat/apple
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4a128044bf | |||
| dec15194e4 | |||
| 2fed03a882 | |||
| 74d5868a5c | |||
| 609ebc3940 | |||
| 5d2e667ceb | |||
| 5786f55e78 | |||
| ba888fb303 | |||
| 5f942848aa | |||
| 3a58e77da0 | |||
| 8d03ea5eec | |||
| 2f4c6e2543 | |||
| 7829ad7298 | |||
| 6642b2531e | |||
| abf005fd8d | |||
| c6cff7a0c4 | |||
| 5f1de791c0 | |||
| 91c75198de | |||
| 3b98faeddd | |||
| cbf400c118 | |||
| c49f0816c8 | |||
| 9cf2bb9bd8 | |||
| 6ceb7861de | |||
| 97ed8a491e | |||
| cf0bd9aa84 | |||
| a62de038a4 | |||
| eeac27370b | |||
| 88edc03b8b | |||
| 4a5dddac75 | |||
| fee26fb9e1 | |||
| 9ee9eff8a3 | |||
| fda353f746 | |||
| 762eb6fe79 | |||
| 335155eb8f | |||
| 577a64a32f | |||
| b7ac8bce83 | |||
| 3594b18872 | |||
| 29dfe55974 | |||
| 3111513458 | |||
| 67686095a5 | |||
| e7a0e08938 | |||
| 41067aae84 | |||
| f7fe582200 | |||
| 09a25e0f37 | |||
| d061ca06e2 | |||
| 2b8d9868b9 | |||
| fe86bbae40 | |||
| 81bead113f | |||
| 0015588f9c | |||
| 132dd2803e | |||
| f8196b162e | |||
| fde8fd9259 | |||
| dc69b4086c | |||
| 2a893f5871 | |||
| 96c412ca49 | |||
| d279298dec | |||
| d13d883db9 | |||
| b7e7fe9c55 | |||
| d12dfc3c8e | |||
| 890d0c428d | |||
| 105fb02378 | |||
| 7478016384 | |||
| 8216502eeb | |||
| 5eadb0b47a | |||
| a4a669e774 | |||
| 8308a7231a | |||
| d585af2193 | |||
| fe162eb081 | |||
| ae3b228cf5 | |||
| 023b2cd86e | |||
| 1f51cead5c | |||
| be29242cd3 | |||
| 742caad102 | |||
| eae632d0da | |||
| 7f68d2bf79 | |||
| df0432a99f | |||
| 92a9ccd470 | |||
| f5ff2ba44c | |||
| 6c80fdff86 | |||
| f425055a53 | |||
| 1ddaf25150 | |||
| 5cacbec11a | |||
| 728e3a5406 | |||
| 8a75fd7c22 | |||
| baf16f2a55 | |||
| a9c06a6aaf | |||
| 9bb5211b4e | |||
| 8c28f7d5f3 | |||
| d658d90d18 | |||
| 19366f29a9 | |||
| 3628b2bfaa | |||
| 0085daec61 | |||
| e45dffcfbe | |||
| 7e24829300 | |||
| b3266af6bc | |||
| 79893c4652 | |||
| 23a1fe5f7f | |||
| ca873f14c7 | |||
| 80ce96d6b2 | |||
| 333314fa69 | |||
| 7c969b55dc | |||
| eb253609d5 | |||
| 06a48e738d | |||
| 15b6206b05 | |||
| 2c3de34b22 | |||
| 1bfdb8629f | |||
| afa3c21c99 | |||
| 1161a25c97 | |||
| 7dd2d3ee82 | |||
| 1d2e88749c | |||
| d39e02cb56 | |||
| 21ff5f22a7 | |||
| fca123e26d | |||
| b5c56a62de | |||
| dc98f9ced2 | |||
| 394cd71e44 | |||
| b7db3fec62 | |||
| 80a65f1940 | |||
| a6dbb2ba59 | |||
| 4dd51dc4cd | |||
| caa8dffc87 | |||
| 25dd8bea9e | |||
| da99616086 | |||
| 28950a974c | |||
| 8f454b54d8 | |||
| 982d8c353c | |||
| 03bbb5781a | |||
| 0433f8c910 | |||
| f21d879af0 | |||
| c180bdf98a | |||
| bf44668a12 | |||
| acc9bda292 | |||
| bb834e7c2e | |||
| 2d58cca30d | |||
| a5a4bd2641 | |||
| 38813229c9 | |||
| d58504cde3 | |||
| 04c9e019d3 | |||
| d24a318de8 | |||
| 3b0039b51b | |||
| 5dd3509466 | |||
| dbde8bc00b | |||
| 7449650b64 | |||
| a0810fbee1 | |||
| a614974a35 | |||
| b24b091a3e | |||
| 3aefb4c4ac | |||
| 2e2ca59334 | |||
| 6463d5ef4c | |||
| 4432d8e604 | |||
| 7ded50ea84 | |||
| d49285f1e2 | |||
| b9f39e690d | |||
| fc2b139653 | |||
| 3ceb2d7a6f | |||
| 95ad74a1b5 | |||
| ca4d87b315 | |||
| 86efb04eb8 | |||
| 67de2a91fe | |||
| df388b929a | |||
| 9083b0a5d0 | |||
| dbc225a41c | |||
| 18a0acfe19 | |||
| 4488e53ff2 | |||
| 7bced50952 | |||
| 824a2143ef | |||
| 5c1b9a00f4 | |||
| aaef8a6107 | |||
| 9f3745b71c | |||
| 5431b5be40 | |||
| acfa3e9d54 | |||
| 3c72bdf3c2 | |||
| dc2359ff6a | |||
| 0b6398cc4c | |||
| d3a3160cf8 | |||
| b021882013 | |||
| d1cbbfffd8 | |||
| 5104df0af0 | |||
| 5e330861ca | |||
| 4d27fdb25a | |||
| 2ab1609bd9 | |||
| aa7e6a2a53 | |||
| 2937e7b974 | |||
| 2371907f54 | |||
| 6615827b29 | |||
| 97c23fa895 | |||
| 39930428a9 | |||
| 83b14e0744 | |||
| f180b7f39b | |||
| a2fbfcb13c | |||
| d640f7f882 | |||
| d43c12b103 | |||
| 38c3792675 | |||
| ac2785abd5 | |||
| 1ff6a0e831 | |||
| 7a3b709404 | |||
|  | d63cb4ac52 | ||
| b6ee1cf906 | |||
| 60201b1b67 | |||
| a8b8603649 | |||
| e193528fe9 | |||
| 73afb34964 | |||
| 65bbc453e6 | |||
|  | 188477ab64 | ||
|  | a31bfb6b39 | ||
| 681ed69ef0 | |||
| b771428b4d | |||
| fc0103ee5d | |||
| 55067b81b8 | |||
| dfe2b5df09 | |||
| dc0c435163 | |||
| 9d1ac56b9a | |||
| fc2c3664d9 | |||
| 0bd45ed777 | |||
| 3912766982 | |||
| 3becce2a6c | |||
| 20b8692c91 | |||
| 14ac780aa5 | |||
| d836870612 | |||
| bc6f706e4a | |||
| 6ac6a9b039 | |||
| 85be80d712 | |||
| 105be1e411 | |||
| 010830243e | |||
| 923dc46dc7 | |||
| f2ef5366f5 | |||
| 20380a4587 | |||
| 069ef2c458 | |||
| 2f430b2d8f | |||
| f7a579a438 | |||
| b9ddd998bc | |||
| ae59d02df2 | |||
| ec205bab0c | |||
| ed49d825b8 | |||
| a9db8be46a | |||
| 1caa3c7fae | |||
| 2ea4bffd49 | |||
| 5ae52f59fc | |||
| a7e6d25d3f | |||
| 83751a4e3e | |||
| 0e9daab187 | |||
| 4390491873 | |||
| d620a4cc2e | |||
| 32669e5bef | |||
| 6edad3991f | |||
| 50acf0bedc | |||
| d4369ec7a4 | |||
| c16543099e | |||
| f2a65d755c | |||
| 1fd48edd42 | |||
| 68e45303c6 | |||
| 532993e9dd | |||
| d19d72ce0c | |||
| d1820a08cf | |||
| bc73665b12 | |||
| 9edb19569a | |||
| 7802a89d15 | |||
| 915260f41b | |||
| 0d57e9a03b | |||
| 582207d453 | |||
| b1b08bfa04 | |||
| 14e883672d | |||
| 7a405140db | |||
| 35497f5bd2 | |||
| 91b19785d6 | |||
| a301d21cc2 | |||
| a2a4b9a553 | |||
| 45f45559fd | |||
| 458256132a | |||
| 0f2c166e1c | |||
| 1c7a688cb8 | |||
| 6269f178e9 | |||
| 3e7527ee19 | |||
| 2236316863 | |||
| cc2fded193 | |||
| f32e0a8ab0 | |||
| ec6e6d2ba0 | |||
| ca85635b03 | |||
| 32257dc64e | |||
| 6bba319735 | |||
| dcce972fdc | |||
| 32e25fb983 | |||
| e7882869e6 | |||
| d0a251f69a | |||
| 9bc7f29162 | |||
| 3ff963f007 | |||
| bcfce66ec0 | |||
| 33e3ee3489 | |||
| e3502a7690 | |||
| 8d09ba4d07 | |||
| ba670d06aa | |||
| a11ad2f651 | |||
| 755bd116d5 | |||
| 9e33784781 | |||
| 470bcdd72e | |||
| d56a7d4dfe | |||
| b46e586c92 | |||
| 563eb3f1ef | |||
| 98644513ad | |||
| 3033db02b8 | |||
| 70a6ed189b | |||
| d7e4d2095c | |||
| 1c0799a30a | |||
| 2b3955060f | |||
| 4ac4d642e7 | |||
| 3d12cd2735 | |||
| 4a44924f56 | |||
| 3910b5d7b2 | |||
| 4a32fe5255 | |||
| 8b9b2be891 | |||
| 96321831d1 | |||
| 39cd5ce04a | |||
| 4a46bbd2be | |||
| f45dcc560c | |||
| 1a014bea15 | |||
| 6d6f1ffd06 | |||
| 4528b240e1 | |||
| c454d9c9e0 | |||
| f8c284cd71 | |||
| 46daff2ddb | |||
| 9bb98ce569 | |||
| 001c243f95 | |||
| 0fdaf5bd4e | |||
| 931918c60b | |||
| a9d3246b97 | |||
| cde119592d | |||
| 031127fb1f | |||
| fa50dd3455 | |||
| 49c418c3f1 | |||
| 8e7aa77ee3 | |||
| 4b0fcca5d2 | |||
| 585fa5afcf | |||
| 38cec8c31a | |||
| 431cb7c034 | |||
| 91a92a30ad | |||
| 9d819e9a14 | |||
| ca910089c5 | |||
| 6270206812 | |||
| 1d1a78608e | |||
| 2e8795a317 | |||
| f39560e041 | |||
| 6f74a5bff4 | |||
| c339045a0e | |||
| 9e38b67857 | |||
| a6f72c8f6b | |||
| c8f9cb7e22 | |||
| 7bb624b942 | |||
| b11d2f752b | |||
| 1a82b751ea | |||
|  | 45bc0389ac | ||
| 67d3af0ed0 | 
							
								
								
									
										107
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,107 @@ | |||||||
|  | --- | ||||||
|  | kind: pipeline | ||||||
|  | type: docker | ||||||
|  | name: seasoned build | ||||||
|  |  | ||||||
|  | platform: | ||||||
|  |   os: linux | ||||||
|  |   arch: amd64 | ||||||
|  |  | ||||||
|  | volumes: | ||||||
|  |   - name: cache | ||||||
|  |     host: | ||||||
|  |       path: /tmp/cache | ||||||
|  |  | ||||||
|  | steps: | ||||||
|  |   - name: Load cached frontend packages | ||||||
|  |     image: sinlead/drone-cache:1.0.0 | ||||||
|  |     settings: | ||||||
|  |       action: load | ||||||
|  |       key: yarn.lock | ||||||
|  |       mount: node_modules | ||||||
|  |       prefix: yarn-modules-seasoned | ||||||
|  |     volumes: | ||||||
|  |       - name: cache | ||||||
|  |         path: /cache | ||||||
|  |  | ||||||
|  |   - name: Frontend install | ||||||
|  |     image: node:18.2.0 | ||||||
|  |     commands: | ||||||
|  |       - node -v | ||||||
|  |       - yarn --version | ||||||
|  |       - yarn | ||||||
|  |  | ||||||
|  |   - name: Cache frontend packages | ||||||
|  |     image: sinlead/drone-cache:1.0.0 | ||||||
|  |     settings: | ||||||
|  |       action: save | ||||||
|  |       key: yarn.lock | ||||||
|  |       mount: node_modules | ||||||
|  |       prefix: yarn-modules-seasoned | ||||||
|  |     volumes: | ||||||
|  |       - name: cache | ||||||
|  |         path: /cache | ||||||
|  |  | ||||||
|  |   - name: Frontend build | ||||||
|  |     image: node:18.2.0 | ||||||
|  |     commands: | ||||||
|  |       - yarn build | ||||||
|  |     environment: | ||||||
|  |       ELASTIC: | ||||||
|  |         from_secret: ELASTIC | ||||||
|  |       ELASTIC_INDEX: | ||||||
|  |         from_secret: ELASTIC_INDEX | ||||||
|  |       SEASONED_API: | ||||||
|  |         from_secret: SEASONED_API | ||||||
|  |       SEASONED_DOMAIN: | ||||||
|  |         from_secret: SEASONED_DOMAIN | ||||||
|  |  | ||||||
|  |   - name: Lint project using eslint | ||||||
|  |     image: node:18.2.0 | ||||||
|  |     commands: | ||||||
|  |       - yarn lint | ||||||
|  |     failure: ignore | ||||||
|  |  | ||||||
|  |   - name: Build and publish docker image | ||||||
|  |     image: plugins/docker | ||||||
|  |     settings: | ||||||
|  |       registry: ghcr.io | ||||||
|  |       repo: ghcr.io/kevinmidboe/seasoned | ||||||
|  |       dockerfile: Dockerfile | ||||||
|  |       username: | ||||||
|  |         from_secret: GITHUB_USERNAME | ||||||
|  |       password: | ||||||
|  |         from_secret: GITHUB_PASSWORD | ||||||
|  |       tags: latest | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - push | ||||||
|  |       branch: | ||||||
|  |         - master | ||||||
|  |  | ||||||
|  |   - name: deploy | ||||||
|  |     image: appleboy/drone-ssh | ||||||
|  |     pull: true | ||||||
|  |     secrets: | ||||||
|  |       - ssh_key | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - push | ||||||
|  |       branch: | ||||||
|  |         - master | ||||||
|  |         - drone-test | ||||||
|  |       status: success | ||||||
|  |     settings: | ||||||
|  |       host: 10.0.0.54 | ||||||
|  |       username: root | ||||||
|  |       key: | ||||||
|  |         from_secret: ssh_key | ||||||
|  |       command_timeout: 600s | ||||||
|  |       script: | ||||||
|  |         - /home/kevin/deploy/seasoned.sh | ||||||
|  |  | ||||||
|  | trigger: | ||||||
|  |   event: | ||||||
|  |     include: | ||||||
|  |       - push | ||||||
|  |       # - pull_request | ||||||
							
								
								
									
										4
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | SEASONED_API= | ||||||
|  | ELASTIC= | ||||||
|  | ELASTIC_INDEX=shows,movies | ||||||
|  | SEASONED_DOMAIN= | ||||||
							
								
								
									
										31
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | |||||||
|  | { | ||||||
|  |   "root": true, | ||||||
|  |   "parser": "vue-eslint-parser", | ||||||
|  |   "parserOptions": { | ||||||
|  |     "parser": "@typescript-eslint/parser", | ||||||
|  |     "sourceType": "module" | ||||||
|  |   }, | ||||||
|  |   "plugins": [ | ||||||
|  |     "@typescript-eslint" | ||||||
|  |   ], | ||||||
|  |   "extends": [ | ||||||
|  |     "@vue/eslint-config-airbnb", | ||||||
|  |     "plugin:vue/recommended", | ||||||
|  |     "plugin:@typescript-eslint/recommended", | ||||||
|  |     "plugin:prettier/recommended", | ||||||
|  |   ], | ||||||
|  |   "rules": { | ||||||
|  |     "vue/no-v-model-argument": "off", | ||||||
|  |     "no-underscore-dangle": "off", | ||||||
|  |     "vue/multi-word-component-names": "off", | ||||||
|  |     "no-shadow": "off", | ||||||
|  |     "@typescript-eslint/no-shadow": ["error"], | ||||||
|  |   }, | ||||||
|  |   "settings": { | ||||||
|  |     "import/resolver": { | ||||||
|  |       webpack: { | ||||||
|  |         config: "./webpack.config.js" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,5 +1,6 @@ | |||||||
| # config file - copy config.json.example | # config file - copy config.json.example | ||||||
| src/config.json | src/config.json | ||||||
|  | .env | ||||||
|  |  | ||||||
| # Build directory | # Build directory | ||||||
| dist/ | dist/ | ||||||
|   | |||||||
| @@ -1,8 +0,0 @@ | |||||||
| <IfModule mod_rewrite.c> |  | ||||||
|   RewriteEngine On |  | ||||||
|   RewriteBase / |  | ||||||
|   RewriteRule ^index\.html$ - [L] |  | ||||||
|   RewriteCond %{REQUEST_FILENAME} !-f |  | ||||||
|   RewriteCond %{REQUEST_FILENAME} !-d |  | ||||||
|   RewriteRule . /index.html [L] |  | ||||||
| </IfModule> |  | ||||||
							
								
								
									
										10
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | { | ||||||
|  |   "tabWidth": 2, | ||||||
|  |   "useTabs": false, | ||||||
|  |   "semi": true, | ||||||
|  |   "singleQuote": false, | ||||||
|  |   "bracketSpacing": true, | ||||||
|  |   "arrowParens": "avoid", | ||||||
|  |   "vueIndentScriptAndStyle": true, | ||||||
|  |   "trailingComma": "none" | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | FROM nginx:1.23.1 | ||||||
|  |  | ||||||
|  | COPY public /usr/share/nginx/html | ||||||
|  | COPY nginx.conf /etc/nginx/conf.d/default.conf.template | ||||||
|  | COPY docker-entrypoint.sh /docker-entrypoint.d/05-docker-entrypoint.sh | ||||||
|  |  | ||||||
|  | RUN chmod +x /docker-entrypoint.d/05-docker-entrypoint.sh | ||||||
|  |  | ||||||
|  | EXPOSE 5000 | ||||||
|  |  | ||||||
|  | LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasoned | ||||||
							
								
								
									
										75
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,45 +1,72 @@ | |||||||
| # The Movie Database App | # Seasoned Request | ||||||
|  |  | ||||||
| A Vue.js project. | Seasoned request is frontend vue application for searching, requesting and viewing account watch activity. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Demo |  | ||||||
|  |  | ||||||
| [TMDB Vue App](https://tmdb-vue-app.herokuapp.com/) |  | ||||||
|  |  | ||||||
| ## Config setup | ## Config setup | ||||||
| Set seasonedShows api endpoint and/or elastic.    |  | ||||||
|  - SeasonedShows [can be found here](https://github.com/kevinmidboe/seasonedshows) and is the matching backend to fetch tmdb search results, tmdb lists, request new content, check plex status and lets owner search and add torrents to download. |  | ||||||
|  - Elastic is optional and can be used for a instant search feature for all movies and shows registered in tmdb. |  | ||||||
|  |  | ||||||
| ```json | ```bash | ||||||
| { | # make copy of example environment file | ||||||
|   "SEASONED_URL": "http://localhost:31459/api", | cp .env.example .env | ||||||
|   "ELASTIC_URL": "http://localhost:9200" |  | ||||||
| } |  | ||||||
| ``` | ``` | ||||||
| *Set ELASTIC_URL to undefined or false to disable* |  | ||||||
|  |  | ||||||
| ## Build Setup | ```bash | ||||||
|  | # .env sane default values | ||||||
|  | SEASONED_API= | ||||||
|  | ELASTIC= | ||||||
|  | ELASTIC_INDEX=shows,movies | ||||||
|  | SEASONED_DOMAIN= | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ``` bash | - Leave SEASONED_API empty to request `/api` from same origin and proxy passed by nginx, set if hosting [seasonedShows backend api](https://github.com/KevinMidboe/seasonedShows) locally. | ||||||
|  | - Elastic is optional and can be used for a instant search feature for all movies and shows registered in tmdb, leave empty to disable. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # .env example values | ||||||
|  | SEASONED_API=http://localhost:31459 | ||||||
|  | ELASTIC=http://localhost:9200 | ||||||
|  | ELASTIC_INDEX=shows,movies | ||||||
|  | SEASONED_DOMAIN=request.movie | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Build Steps | ||||||
|  |  | ||||||
|  | ```bash | ||||||
| # install dependencies | # install dependencies | ||||||
| npm install | yarn | ||||||
|  |  | ||||||
| # serve with hot reload at localhost:8080 | # build vue project using webpack | ||||||
| npm run dev | yarn build | ||||||
|  |  | ||||||
| # build for production with minification | # test or host built files using docker, might require sudo: | ||||||
| npm run build | docker build -t seasoned . | ||||||
|  | docker run -d -p 5000:5000 --name seasoned-request --env-file .env seasoned | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). | ## Development Steps | ||||||
| This app uses [history mode](https://router.vuejs.org/en/essentials/history-mode.html) |  | ||||||
|  | ```bash | ||||||
|  | # serve project with hot reloading at localhost:8080 | ||||||
|  | yarn dev | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | To proxy requests to `/api` either update `SEASONED_API` in `.env` or run set environment variable, e.g.: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # export and run | ||||||
|  | export SEASONED_API=http://localhost:31459 | ||||||
|  | yarn dev | ||||||
|  |  | ||||||
|  | # or run with environment variable inline | ||||||
|  | SEASONED_API=http://localhost:31459 yarn dev | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Documentation | ## Documentation | ||||||
|  |  | ||||||
| All api functions are documented in `/docs` and [found here](docs/api.md).   | All api functions are documented in `/docs` and [found here](docs/api.md).   | ||||||
| [html version also available](http://htmlpreview.github.io/?https://github.com/KevinMidboe/seasoned/blob/release/v2/docs/api/index.html) | [html version also available](http://htmlpreview.github.io/?https://github.com/KevinMidboe/seasoned/blob/release/v2/docs/api/index.html) | ||||||
|  |  | ||||||
| ## License | ## License | ||||||
|  |  | ||||||
| [MIT](https://github.com/dmtrbrl/tmdb-app/blob/master/LICENSE) | [MIT](https://github.com/dmtrbrl/tmdb-app/blob/master/LICENSE) | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								docker-entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | #!/bin/sh | ||||||
|  | set -eu | ||||||
|  |  | ||||||
|  | export SEASONED_API=${SEASONED_API:-http://localhost:31459} | ||||||
|  | export SEASONED_DOMAIN=${SEASONED_DOMAIN:-localhost} | ||||||
|  |  | ||||||
|  | envsubst '$SEASONED_API,$SEASONED_DOMAIN' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf | ||||||
|  |  | ||||||
|  | exec "$@" | ||||||
| @@ -1,350 +0,0 @@ | |||||||
| /*! |  | ||||||
|  * AnchorJS - v4.0.0 - 2017-06-02 |  | ||||||
|  * https://github.com/bryanbraun/anchorjs |  | ||||||
|  * Copyright (c) 2017 Bryan Braun; Licensed MIT |  | ||||||
|  */ |  | ||||||
| /* eslint-env amd, node */ |  | ||||||
|  |  | ||||||
| // https://github.com/umdjs/umd/blob/master/templates/returnExports.js |  | ||||||
| (function(root, factory) { |  | ||||||
|   'use strict'; |  | ||||||
|   if (typeof define === 'function' && define.amd) { |  | ||||||
|     // AMD. Register as an anonymous module. |  | ||||||
|     define([], factory); |  | ||||||
|   } else if (typeof module === 'object' && module.exports) { |  | ||||||
|     // Node. Does not work with strict CommonJS, but |  | ||||||
|     // only CommonJS-like environments that support module.exports, |  | ||||||
|     // like Node. |  | ||||||
|     module.exports = factory(); |  | ||||||
|   } else { |  | ||||||
|     // Browser globals (root is window) |  | ||||||
|     root.AnchorJS = factory(); |  | ||||||
|     root.anchors = new root.AnchorJS(); |  | ||||||
|   } |  | ||||||
| })(this, function() { |  | ||||||
|   'use strict'; |  | ||||||
|   function AnchorJS(options) { |  | ||||||
|     this.options = options || {}; |  | ||||||
|     this.elements = []; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Assigns options to the internal options object, and provides defaults. |  | ||||||
|      * @param {Object} opts - Options object |  | ||||||
|      */ |  | ||||||
|     function _applyRemainingDefaultOptions(opts) { |  | ||||||
|       opts.icon = opts.hasOwnProperty('icon') ? opts.icon : '\ue9cb'; // Accepts characters (and also URLs?), like  '#', '¶', '❡', or '§'. |  | ||||||
|       opts.visible = opts.hasOwnProperty('visible') ? opts.visible : 'hover'; // Also accepts 'always' & 'touch' |  | ||||||
|       opts.placement = opts.hasOwnProperty('placement') |  | ||||||
|         ? opts.placement |  | ||||||
|         : 'right'; // Also accepts 'left' |  | ||||||
|       opts.class = opts.hasOwnProperty('class') ? opts.class : ''; // Accepts any class name. |  | ||||||
|       // Using Math.floor here will ensure the value is Number-cast and an integer. |  | ||||||
|       opts.truncate = opts.hasOwnProperty('truncate') |  | ||||||
|         ? Math.floor(opts.truncate) |  | ||||||
|         : 64; // Accepts any value that can be typecast to a number. |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     _applyRemainingDefaultOptions(this.options); |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Checks to see if this device supports touch. Uses criteria pulled from Modernizr: |  | ||||||
|      * https://github.com/Modernizr/Modernizr/blob/da22eb27631fc4957f67607fe6042e85c0a84656/feature-detects/touchevents.js#L40 |  | ||||||
|      * @returns {Boolean} - true if the current device supports touch. |  | ||||||
|      */ |  | ||||||
|     this.isTouchDevice = function() { |  | ||||||
|       return !!( |  | ||||||
|         'ontouchstart' in window || |  | ||||||
|         (window.DocumentTouch && document instanceof DocumentTouch) |  | ||||||
|       ); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Add anchor links to page elements. |  | ||||||
|      * @param  {String|Array|Nodelist} selector - A CSS selector for targeting the elements you wish to add anchor links |  | ||||||
|      *                                            to. Also accepts an array or nodeList containing the relavant elements. |  | ||||||
|      * @returns {this}                           - The AnchorJS object |  | ||||||
|      */ |  | ||||||
|     this.add = function(selector) { |  | ||||||
|       var elements, |  | ||||||
|         elsWithIds, |  | ||||||
|         idList, |  | ||||||
|         elementID, |  | ||||||
|         i, |  | ||||||
|         index, |  | ||||||
|         count, |  | ||||||
|         tidyText, |  | ||||||
|         newTidyText, |  | ||||||
|         readableID, |  | ||||||
|         anchor, |  | ||||||
|         visibleOptionToUse, |  | ||||||
|         indexesToDrop = []; |  | ||||||
|  |  | ||||||
|       // We reapply options here because somebody may have overwritten the default options object when setting options. |  | ||||||
|       // For example, this overwrites all options but visible: |  | ||||||
|       // |  | ||||||
|       // anchors.options = { visible: 'always'; } |  | ||||||
|       _applyRemainingDefaultOptions(this.options); |  | ||||||
|  |  | ||||||
|       visibleOptionToUse = this.options.visible; |  | ||||||
|       if (visibleOptionToUse === 'touch') { |  | ||||||
|         visibleOptionToUse = this.isTouchDevice() ? 'always' : 'hover'; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Provide a sensible default selector, if none is given. |  | ||||||
|       if (!selector) { |  | ||||||
|         selector = 'h2, h3, h4, h5, h6'; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       elements = _getElements(selector); |  | ||||||
|  |  | ||||||
|       if (elements.length === 0) { |  | ||||||
|         return this; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       _addBaselineStyles(); |  | ||||||
|  |  | ||||||
|       // We produce a list of existing IDs so we don't generate a duplicate. |  | ||||||
|       elsWithIds = document.querySelectorAll('[id]'); |  | ||||||
|       idList = [].map.call(elsWithIds, function assign(el) { |  | ||||||
|         return el.id; |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       for (i = 0; i < elements.length; i++) { |  | ||||||
|         if (this.hasAnchorJSLink(elements[i])) { |  | ||||||
|           indexesToDrop.push(i); |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (elements[i].hasAttribute('id')) { |  | ||||||
|           elementID = elements[i].getAttribute('id'); |  | ||||||
|         } else if (elements[i].hasAttribute('data-anchor-id')) { |  | ||||||
|           elementID = elements[i].getAttribute('data-anchor-id'); |  | ||||||
|         } else { |  | ||||||
|           tidyText = this.urlify(elements[i].textContent); |  | ||||||
|  |  | ||||||
|           // Compare our generated ID to existing IDs (and increment it if needed) |  | ||||||
|           // before we add it to the page. |  | ||||||
|           newTidyText = tidyText; |  | ||||||
|           count = 0; |  | ||||||
|           do { |  | ||||||
|             if (index !== undefined) { |  | ||||||
|               newTidyText = tidyText + '-' + count; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             index = idList.indexOf(newTidyText); |  | ||||||
|             count += 1; |  | ||||||
|           } while (index !== -1); |  | ||||||
|           index = undefined; |  | ||||||
|           idList.push(newTidyText); |  | ||||||
|  |  | ||||||
|           elements[i].setAttribute('id', newTidyText); |  | ||||||
|           elementID = newTidyText; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         readableID = elementID.replace(/-/g, ' '); |  | ||||||
|  |  | ||||||
|         // The following code builds the following DOM structure in a more effiecient (albeit opaque) way. |  | ||||||
|         // '<a class="anchorjs-link ' + this.options.class + '" href="#' + elementID + '" aria-label="Anchor link for: ' + readableID + '" data-anchorjs-icon="' + this.options.icon + '"></a>'; |  | ||||||
|         anchor = document.createElement('a'); |  | ||||||
|         anchor.className = 'anchorjs-link ' + this.options.class; |  | ||||||
|         anchor.href = '#' + elementID; |  | ||||||
|         anchor.setAttribute('aria-label', 'Anchor link for: ' + readableID); |  | ||||||
|         anchor.setAttribute('data-anchorjs-icon', this.options.icon); |  | ||||||
|  |  | ||||||
|         if (visibleOptionToUse === 'always') { |  | ||||||
|           anchor.style.opacity = '1'; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.options.icon === '\ue9cb') { |  | ||||||
|           anchor.style.font = '1em/1 anchorjs-icons'; |  | ||||||
|  |  | ||||||
|           // We set lineHeight = 1 here because the `anchorjs-icons` font family could otherwise affect the |  | ||||||
|           // height of the heading. This isn't the case for icons with `placement: left`, so we restore |  | ||||||
|           // line-height: inherit in that case, ensuring they remain positioned correctly. For more info, |  | ||||||
|           // see https://github.com/bryanbraun/anchorjs/issues/39. |  | ||||||
|           if (this.options.placement === 'left') { |  | ||||||
|             anchor.style.lineHeight = 'inherit'; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.options.placement === 'left') { |  | ||||||
|           anchor.style.position = 'absolute'; |  | ||||||
|           anchor.style.marginLeft = '-1em'; |  | ||||||
|           anchor.style.paddingRight = '0.5em'; |  | ||||||
|           elements[i].insertBefore(anchor, elements[i].firstChild); |  | ||||||
|         } else { |  | ||||||
|           // if the option provided is `right` (or anything else). |  | ||||||
|           anchor.style.paddingLeft = '0.375em'; |  | ||||||
|           elements[i].appendChild(anchor); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       for (i = 0; i < indexesToDrop.length; i++) { |  | ||||||
|         elements.splice(indexesToDrop[i] - i, 1); |  | ||||||
|       } |  | ||||||
|       this.elements = this.elements.concat(elements); |  | ||||||
|  |  | ||||||
|       return this; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Removes all anchorjs-links from elements targed by the selector. |  | ||||||
|      * @param  {String|Array|Nodelist} selector - A CSS selector string targeting elements with anchor links, |  | ||||||
|      *                                            OR a nodeList / array containing the DOM elements. |  | ||||||
|      * @returns {this}                           - The AnchorJS object |  | ||||||
|      */ |  | ||||||
|     this.remove = function(selector) { |  | ||||||
|       var index, |  | ||||||
|         domAnchor, |  | ||||||
|         elements = _getElements(selector); |  | ||||||
|  |  | ||||||
|       for (var i = 0; i < elements.length; i++) { |  | ||||||
|         domAnchor = elements[i].querySelector('.anchorjs-link'); |  | ||||||
|         if (domAnchor) { |  | ||||||
|           // Drop the element from our main list, if it's in there. |  | ||||||
|           index = this.elements.indexOf(elements[i]); |  | ||||||
|           if (index !== -1) { |  | ||||||
|             this.elements.splice(index, 1); |  | ||||||
|           } |  | ||||||
|           // Remove the anchor from the DOM. |  | ||||||
|           elements[i].removeChild(domAnchor); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       return this; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Removes all anchorjs links. Mostly used for tests. |  | ||||||
|      */ |  | ||||||
|     this.removeAll = function() { |  | ||||||
|       this.remove(this.elements); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Urlify - Refine text so it makes a good ID. |  | ||||||
|      * |  | ||||||
|      * To do this, we remove apostrophes, replace nonsafe characters with hyphens, |  | ||||||
|      * remove extra hyphens, truncate, trim hyphens, and make lowercase. |  | ||||||
|      * |  | ||||||
|      * @param  {String} text - Any text. Usually pulled from the webpage element we are linking to. |  | ||||||
|      * @returns {String}      - hyphen-delimited text for use in IDs and URLs. |  | ||||||
|      */ |  | ||||||
|     this.urlify = function(text) { |  | ||||||
|       // Regex for finding the nonsafe URL characters (many need escaping): & +$,:;=?@"#{}|^~[`%!'<>]./()*\ |  | ||||||
|       var nonsafeChars = /[& +$,:;=?@"#{}|^~[`%!'<>\]\.\/\(\)\*\\]/g, |  | ||||||
|         urlText; |  | ||||||
|  |  | ||||||
|       // The reason we include this _applyRemainingDefaultOptions is so urlify can be called independently, |  | ||||||
|       // even after setting options. This can be useful for tests or other applications. |  | ||||||
|       if (!this.options.truncate) { |  | ||||||
|         _applyRemainingDefaultOptions(this.options); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Note: we trim hyphens after truncating because truncating can cause dangling hyphens. |  | ||||||
|       // Example string:                                  // " ⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." |  | ||||||
|       urlText = text |  | ||||||
|         .trim() // "⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." |  | ||||||
|         .replace(/\'/gi, '') // "⚡⚡ Dont forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." |  | ||||||
|         .replace(nonsafeChars, '-') // "⚡⚡-Dont-forget--URL-fragments-should-be-i18n-friendly--hyphenated--short--and-clean-" |  | ||||||
|         .replace(/-{2,}/g, '-') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-short-and-clean-" |  | ||||||
|         .substring(0, this.options.truncate) // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-" |  | ||||||
|         .replace(/^-+|-+$/gm, '') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated" |  | ||||||
|         .toLowerCase(); // "⚡⚡-dont-forget-url-fragments-should-be-i18n-friendly-hyphenated" |  | ||||||
|  |  | ||||||
|       return urlText; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Determines if this element already has an AnchorJS link on it. |  | ||||||
|      * Uses this technique: http://stackoverflow.com/a/5898748/1154642 |  | ||||||
|      * @param    {HTMLElemnt}  el - a DOM node |  | ||||||
|      * @returns   {Boolean}     true/false |  | ||||||
|      */ |  | ||||||
|     this.hasAnchorJSLink = function(el) { |  | ||||||
|       var hasLeftAnchor = |  | ||||||
|           el.firstChild && |  | ||||||
|           (' ' + el.firstChild.className + ' ').indexOf(' anchorjs-link ') > -1, |  | ||||||
|         hasRightAnchor = |  | ||||||
|           el.lastChild && |  | ||||||
|           (' ' + el.lastChild.className + ' ').indexOf(' anchorjs-link ') > -1; |  | ||||||
|  |  | ||||||
|       return hasLeftAnchor || hasRightAnchor || false; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Turns a selector, nodeList, or array of elements into an array of elements (so we can use array methods). |  | ||||||
|      * It also throws errors on any other inputs. Used to handle inputs to .add and .remove. |  | ||||||
|      * @param  {String|Array|Nodelist} input - A CSS selector string targeting elements with anchor links, |  | ||||||
|      *                                         OR a nodeList / array containing the DOM elements. |  | ||||||
|      * @returns {Array} - An array containing the elements we want. |  | ||||||
|      */ |  | ||||||
|     function _getElements(input) { |  | ||||||
|       var elements; |  | ||||||
|       if (typeof input === 'string' || input instanceof String) { |  | ||||||
|         // See https://davidwalsh.name/nodelist-array for the technique transforming nodeList -> Array. |  | ||||||
|         elements = [].slice.call(document.querySelectorAll(input)); |  | ||||||
|         // I checked the 'input instanceof NodeList' test in IE9 and modern browsers and it worked for me. |  | ||||||
|       } else if (Array.isArray(input) || input instanceof NodeList) { |  | ||||||
|         elements = [].slice.call(input); |  | ||||||
|       } else { |  | ||||||
|         throw new Error('The selector provided to AnchorJS was invalid.'); |  | ||||||
|       } |  | ||||||
|       return elements; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * _addBaselineStyles |  | ||||||
|      * Adds baseline styles to the page, used by all AnchorJS links irregardless of configuration. |  | ||||||
|      */ |  | ||||||
|     function _addBaselineStyles() { |  | ||||||
|       // We don't want to add global baseline styles if they've been added before. |  | ||||||
|       if (document.head.querySelector('style.anchorjs') !== null) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       var style = document.createElement('style'), |  | ||||||
|         linkRule = |  | ||||||
|           ' .anchorjs-link {' + |  | ||||||
|           '   opacity: 0;' + |  | ||||||
|           '   text-decoration: none;' + |  | ||||||
|           '   -webkit-font-smoothing: antialiased;' + |  | ||||||
|           '   -moz-osx-font-smoothing: grayscale;' + |  | ||||||
|           ' }', |  | ||||||
|         hoverRule = |  | ||||||
|           ' *:hover > .anchorjs-link,' + |  | ||||||
|           ' .anchorjs-link:focus  {' + |  | ||||||
|           '   opacity: 1;' + |  | ||||||
|           ' }', |  | ||||||
|         anchorjsLinkFontFace = |  | ||||||
|           ' @font-face {' + |  | ||||||
|           '   font-family: "anchorjs-icons";' + // Icon from icomoon; 10px wide & 10px tall; 2 empty below & 4 above |  | ||||||
|           '   src: url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype");' + |  | ||||||
|           ' }', |  | ||||||
|         pseudoElContent = |  | ||||||
|           ' [data-anchorjs-icon]::after {' + |  | ||||||
|           '   content: attr(data-anchorjs-icon);' + |  | ||||||
|           ' }', |  | ||||||
|         firstStyleEl; |  | ||||||
|  |  | ||||||
|       style.className = 'anchorjs'; |  | ||||||
|       style.appendChild(document.createTextNode('')); // Necessary for Webkit. |  | ||||||
|  |  | ||||||
|       // We place it in the head with the other style tags, if possible, so as to |  | ||||||
|       // not look out of place. We insert before the others so these styles can be |  | ||||||
|       // overridden if necessary. |  | ||||||
|       firstStyleEl = document.head.querySelector('[rel="stylesheet"], style'); |  | ||||||
|       if (firstStyleEl === undefined) { |  | ||||||
|         document.head.appendChild(style); |  | ||||||
|       } else { |  | ||||||
|         document.head.insertBefore(style, firstStyleEl); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       style.sheet.insertRule(linkRule, style.sheet.cssRules.length); |  | ||||||
|       style.sheet.insertRule(hoverRule, style.sheet.cssRules.length); |  | ||||||
|       style.sheet.insertRule(pseudoElContent, style.sheet.cssRules.length); |  | ||||||
|       style.sheet.insertRule(anchorjsLinkFontFace, style.sheet.cssRules.length); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return AnchorJS; |  | ||||||
| }); |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| .input { |  | ||||||
|   font-family: inherit; |  | ||||||
|   display: block; |  | ||||||
|   width: 100%; |  | ||||||
|   height: 2rem; |  | ||||||
|   padding: .5rem; |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
|   border: 1px solid #ccc; |  | ||||||
|   font-size: .875rem; |  | ||||||
|   border-radius: 3px; |  | ||||||
|   box-sizing: border-box; |  | ||||||
| } |  | ||||||
| @@ -1,544 +0,0 @@ | |||||||
| /*! Basscss | http://basscss.com | MIT License */ |  | ||||||
|  |  | ||||||
| .h1{ font-size: 2rem } |  | ||||||
| .h2{ font-size: 1.5rem } |  | ||||||
| .h3{ font-size: 1.25rem } |  | ||||||
| .h4{ font-size: 1rem } |  | ||||||
| .h5{ font-size: .875rem } |  | ||||||
| .h6{ font-size: .75rem } |  | ||||||
|  |  | ||||||
| .font-family-inherit{ font-family:inherit } |  | ||||||
| .font-size-inherit{ font-size:inherit } |  | ||||||
| .text-decoration-none{ text-decoration:none } |  | ||||||
|  |  | ||||||
| .bold{ font-weight: bold; font-weight: bold } |  | ||||||
| .regular{ font-weight:normal } |  | ||||||
| .italic{ font-style:italic } |  | ||||||
| .caps{ text-transform:uppercase; letter-spacing: .2em; } |  | ||||||
|  |  | ||||||
| .left-align{ text-align:left } |  | ||||||
| .center{ text-align:center } |  | ||||||
| .right-align{ text-align:right } |  | ||||||
| .justify{ text-align:justify } |  | ||||||
|  |  | ||||||
| .nowrap{ white-space:nowrap } |  | ||||||
| .break-word{ word-wrap:break-word } |  | ||||||
|  |  | ||||||
| .line-height-1{ line-height: 1 } |  | ||||||
| .line-height-2{ line-height: 1.125 } |  | ||||||
| .line-height-3{ line-height: 1.25 } |  | ||||||
| .line-height-4{ line-height: 1.5 } |  | ||||||
|  |  | ||||||
| .list-style-none{ list-style:none } |  | ||||||
| .underline{ text-decoration:underline } |  | ||||||
|  |  | ||||||
| .truncate{ |  | ||||||
|   max-width:100%; |  | ||||||
|   overflow:hidden; |  | ||||||
|   text-overflow:ellipsis; |  | ||||||
|   white-space:nowrap; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .list-reset{ |  | ||||||
|   list-style:none; |  | ||||||
|   padding-left:0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .inline{ display:inline } |  | ||||||
| .block{ display:block } |  | ||||||
| .inline-block{ display:inline-block } |  | ||||||
| .table{ display:table } |  | ||||||
| .table-cell{ display:table-cell } |  | ||||||
|  |  | ||||||
| .overflow-hidden{ overflow:hidden } |  | ||||||
| .overflow-scroll{ overflow:scroll } |  | ||||||
| .overflow-auto{ overflow:auto } |  | ||||||
|  |  | ||||||
| .clearfix:before, |  | ||||||
| .clearfix:after{ |  | ||||||
|   content:" "; |  | ||||||
|   display:table |  | ||||||
| } |  | ||||||
| .clearfix:after{ clear:both } |  | ||||||
|  |  | ||||||
| .left{ float:left } |  | ||||||
| .right{ float:right } |  | ||||||
|  |  | ||||||
| .fit{ max-width:100% } |  | ||||||
|  |  | ||||||
| .max-width-1{ max-width: 24rem } |  | ||||||
| .max-width-2{ max-width: 32rem } |  | ||||||
| .max-width-3{ max-width: 48rem } |  | ||||||
| .max-width-4{ max-width: 64rem } |  | ||||||
|  |  | ||||||
| .border-box{ box-sizing:border-box } |  | ||||||
|  |  | ||||||
| .align-baseline{ vertical-align:baseline } |  | ||||||
| .align-top{ vertical-align:top } |  | ||||||
| .align-middle{ vertical-align:middle } |  | ||||||
| .align-bottom{ vertical-align:bottom } |  | ||||||
|  |  | ||||||
| .m0{ margin:0 } |  | ||||||
| .mt0{ margin-top:0 } |  | ||||||
| .mr0{ margin-right:0 } |  | ||||||
| .mb0{ margin-bottom:0 } |  | ||||||
| .ml0{ margin-left:0 } |  | ||||||
| .mx0{ margin-left:0; margin-right:0 } |  | ||||||
| .my0{ margin-top:0; margin-bottom:0 } |  | ||||||
|  |  | ||||||
| .m1{ margin: .5rem } |  | ||||||
| .mt1{ margin-top: .5rem } |  | ||||||
| .mr1{ margin-right: .5rem } |  | ||||||
| .mb1{ margin-bottom: .5rem } |  | ||||||
| .ml1{ margin-left: .5rem } |  | ||||||
| .mx1{ margin-left: .5rem; margin-right: .5rem } |  | ||||||
| .my1{ margin-top: .5rem; margin-bottom: .5rem } |  | ||||||
|  |  | ||||||
| .m2{ margin: 1rem } |  | ||||||
| .mt2{ margin-top: 1rem } |  | ||||||
| .mr2{ margin-right: 1rem } |  | ||||||
| .mb2{ margin-bottom: 1rem } |  | ||||||
| .ml2{ margin-left: 1rem } |  | ||||||
| .mx2{ margin-left: 1rem; margin-right: 1rem } |  | ||||||
| .my2{ margin-top: 1rem; margin-bottom: 1rem } |  | ||||||
|  |  | ||||||
| .m3{ margin: 2rem } |  | ||||||
| .mt3{ margin-top: 2rem } |  | ||||||
| .mr3{ margin-right: 2rem } |  | ||||||
| .mb3{ margin-bottom: 2rem } |  | ||||||
| .ml3{ margin-left: 2rem } |  | ||||||
| .mx3{ margin-left: 2rem; margin-right: 2rem } |  | ||||||
| .my3{ margin-top: 2rem; margin-bottom: 2rem } |  | ||||||
|  |  | ||||||
| .m4{ margin: 4rem } |  | ||||||
| .mt4{ margin-top: 4rem } |  | ||||||
| .mr4{ margin-right: 4rem } |  | ||||||
| .mb4{ margin-bottom: 4rem } |  | ||||||
| .ml4{ margin-left: 4rem } |  | ||||||
| .mx4{ margin-left: 4rem; margin-right: 4rem } |  | ||||||
| .my4{ margin-top: 4rem; margin-bottom: 4rem } |  | ||||||
|  |  | ||||||
| .mxn1{ margin-left: -.5rem; margin-right: -.5rem; } |  | ||||||
| .mxn2{ margin-left: -1rem; margin-right: -1rem; } |  | ||||||
| .mxn3{ margin-left: -2rem; margin-right: -2rem; } |  | ||||||
| .mxn4{ margin-left: -4rem; margin-right: -4rem; } |  | ||||||
|  |  | ||||||
| .ml-auto{ margin-left:auto } |  | ||||||
| .mr-auto{ margin-right:auto } |  | ||||||
| .mx-auto{ margin-left:auto; margin-right:auto; } |  | ||||||
|  |  | ||||||
| .p0{ padding:0 } |  | ||||||
| .pt0{ padding-top:0 } |  | ||||||
| .pr0{ padding-right:0 } |  | ||||||
| .pb0{ padding-bottom:0 } |  | ||||||
| .pl0{ padding-left:0 } |  | ||||||
| .px0{ padding-left:0; padding-right:0 } |  | ||||||
| .py0{ padding-top:0;  padding-bottom:0 } |  | ||||||
|  |  | ||||||
| .p1{ padding: .5rem } |  | ||||||
| .pt1{ padding-top: .5rem } |  | ||||||
| .pr1{ padding-right: .5rem } |  | ||||||
| .pb1{ padding-bottom: .5rem } |  | ||||||
| .pl1{ padding-left: .5rem } |  | ||||||
| .py1{ padding-top: .5rem; padding-bottom: .5rem } |  | ||||||
| .px1{ padding-left: .5rem; padding-right: .5rem } |  | ||||||
|  |  | ||||||
| .p2{ padding: 1rem } |  | ||||||
| .pt2{ padding-top: 1rem } |  | ||||||
| .pr2{ padding-right: 1rem } |  | ||||||
| .pb2{ padding-bottom: 1rem } |  | ||||||
| .pl2{ padding-left: 1rem } |  | ||||||
| .py2{ padding-top: 1rem; padding-bottom: 1rem } |  | ||||||
| .px2{ padding-left: 1rem; padding-right: 1rem } |  | ||||||
|  |  | ||||||
| .p3{ padding: 2rem } |  | ||||||
| .pt3{ padding-top: 2rem } |  | ||||||
| .pr3{ padding-right: 2rem } |  | ||||||
| .pb3{ padding-bottom: 2rem } |  | ||||||
| .pl3{ padding-left: 2rem } |  | ||||||
| .py3{ padding-top: 2rem; padding-bottom: 2rem } |  | ||||||
| .px3{ padding-left: 2rem; padding-right: 2rem } |  | ||||||
|  |  | ||||||
| .p4{ padding: 4rem } |  | ||||||
| .pt4{ padding-top: 4rem } |  | ||||||
| .pr4{ padding-right: 4rem } |  | ||||||
| .pb4{ padding-bottom: 4rem } |  | ||||||
| .pl4{ padding-left: 4rem } |  | ||||||
| .py4{ padding-top: 4rem; padding-bottom: 4rem } |  | ||||||
| .px4{ padding-left: 4rem; padding-right: 4rem } |  | ||||||
|  |  | ||||||
| .col{ |  | ||||||
|   float:left; |  | ||||||
|   box-sizing:border-box; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-right{ |  | ||||||
|   float:right; |  | ||||||
|   box-sizing:border-box; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-1{ |  | ||||||
|   width:8.33333%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-2{ |  | ||||||
|   width:16.66667%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-3{ |  | ||||||
|   width:25%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-4{ |  | ||||||
|   width:33.33333%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-5{ |  | ||||||
|   width:41.66667%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-6{ |  | ||||||
|   width:50%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-7{ |  | ||||||
|   width:58.33333%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-8{ |  | ||||||
|   width:66.66667%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-9{ |  | ||||||
|   width:75%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-10{ |  | ||||||
|   width:83.33333%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-11{ |  | ||||||
|   width:91.66667%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .col-12{ |  | ||||||
|   width:100%; |  | ||||||
| } |  | ||||||
| @media (min-width: 40em){ |  | ||||||
|  |  | ||||||
|   .sm-col{ |  | ||||||
|     float:left; |  | ||||||
|     box-sizing:border-box; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-right{ |  | ||||||
|     float:right; |  | ||||||
|     box-sizing:border-box; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-1{ |  | ||||||
|     width:8.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-2{ |  | ||||||
|     width:16.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-3{ |  | ||||||
|     width:25%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-4{ |  | ||||||
|     width:33.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-5{ |  | ||||||
|     width:41.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-6{ |  | ||||||
|     width:50%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-7{ |  | ||||||
|     width:58.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-8{ |  | ||||||
|     width:66.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-9{ |  | ||||||
|     width:75%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-10{ |  | ||||||
|     width:83.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-11{ |  | ||||||
|     width:91.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .sm-col-12{ |  | ||||||
|     width:100%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @media (min-width: 52em){ |  | ||||||
|  |  | ||||||
|   .md-col{ |  | ||||||
|     float:left; |  | ||||||
|     box-sizing:border-box; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-right{ |  | ||||||
|     float:right; |  | ||||||
|     box-sizing:border-box; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-1{ |  | ||||||
|     width:8.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-2{ |  | ||||||
|     width:16.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-3{ |  | ||||||
|     width:25%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-4{ |  | ||||||
|     width:33.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-5{ |  | ||||||
|     width:41.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-6{ |  | ||||||
|     width:50%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-7{ |  | ||||||
|     width:58.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-8{ |  | ||||||
|     width:66.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-9{ |  | ||||||
|     width:75%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-10{ |  | ||||||
|     width:83.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-11{ |  | ||||||
|     width:91.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .md-col-12{ |  | ||||||
|     width:100%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @media (min-width: 64em){ |  | ||||||
|  |  | ||||||
|   .lg-col{ |  | ||||||
|     float:left; |  | ||||||
|     box-sizing:border-box; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-right{ |  | ||||||
|     float:right; |  | ||||||
|     box-sizing:border-box; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-1{ |  | ||||||
|     width:8.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-2{ |  | ||||||
|     width:16.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-3{ |  | ||||||
|     width:25%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-4{ |  | ||||||
|     width:33.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-5{ |  | ||||||
|     width:41.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-6{ |  | ||||||
|     width:50%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-7{ |  | ||||||
|     width:58.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-8{ |  | ||||||
|     width:66.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-9{ |  | ||||||
|     width:75%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-10{ |  | ||||||
|     width:83.33333%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-11{ |  | ||||||
|     width:91.66667%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .lg-col-12{ |  | ||||||
|     width:100%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| .flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } |  | ||||||
|  |  | ||||||
| @media (min-width: 40em){ |  | ||||||
|   .sm-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (min-width: 52em){ |  | ||||||
|   .md-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (min-width: 64em){ |  | ||||||
|   .lg-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .flex-column{ -webkit-box-orient:vertical; -webkit-box-direction:normal; -webkit-flex-direction:column; -ms-flex-direction:column; flex-direction:column } |  | ||||||
| .flex-wrap{ -webkit-flex-wrap:wrap; -ms-flex-wrap:wrap; flex-wrap:wrap } |  | ||||||
|  |  | ||||||
| .items-start{ -webkit-box-align:start; -webkit-align-items:flex-start; -ms-flex-align:start; -ms-grid-row-align:flex-start; align-items:flex-start } |  | ||||||
| .items-end{ -webkit-box-align:end; -webkit-align-items:flex-end; -ms-flex-align:end; -ms-grid-row-align:flex-end; align-items:flex-end } |  | ||||||
| .items-center{ -webkit-box-align:center; -webkit-align-items:center; -ms-flex-align:center; -ms-grid-row-align:center; align-items:center } |  | ||||||
| .items-baseline{ -webkit-box-align:baseline; -webkit-align-items:baseline; -ms-flex-align:baseline; -ms-grid-row-align:baseline; align-items:baseline } |  | ||||||
| .items-stretch{ -webkit-box-align:stretch; -webkit-align-items:stretch; -ms-flex-align:stretch; -ms-grid-row-align:stretch; align-items:stretch } |  | ||||||
|  |  | ||||||
| .self-start{ -webkit-align-self:flex-start; -ms-flex-item-align:start; align-self:flex-start } |  | ||||||
| .self-end{ -webkit-align-self:flex-end; -ms-flex-item-align:end; align-self:flex-end } |  | ||||||
| .self-center{ -webkit-align-self:center; -ms-flex-item-align:center; align-self:center } |  | ||||||
| .self-baseline{ -webkit-align-self:baseline; -ms-flex-item-align:baseline; align-self:baseline } |  | ||||||
| .self-stretch{ -webkit-align-self:stretch; -ms-flex-item-align:stretch; align-self:stretch } |  | ||||||
|  |  | ||||||
| .justify-start{ -webkit-box-pack:start; -webkit-justify-content:flex-start; -ms-flex-pack:start; justify-content:flex-start } |  | ||||||
| .justify-end{ -webkit-box-pack:end; -webkit-justify-content:flex-end; -ms-flex-pack:end; justify-content:flex-end } |  | ||||||
| .justify-center{ -webkit-box-pack:center; -webkit-justify-content:center; -ms-flex-pack:center; justify-content:center } |  | ||||||
| .justify-between{ -webkit-box-pack:justify; -webkit-justify-content:space-between; -ms-flex-pack:justify; justify-content:space-between } |  | ||||||
| .justify-around{ -webkit-justify-content:space-around; -ms-flex-pack:distribute; justify-content:space-around } |  | ||||||
|  |  | ||||||
| .content-start{ -webkit-align-content:flex-start; -ms-flex-line-pack:start; align-content:flex-start } |  | ||||||
| .content-end{ -webkit-align-content:flex-end; -ms-flex-line-pack:end; align-content:flex-end } |  | ||||||
| .content-center{ -webkit-align-content:center; -ms-flex-line-pack:center; align-content:center } |  | ||||||
| .content-between{ -webkit-align-content:space-between; -ms-flex-line-pack:justify; align-content:space-between } |  | ||||||
| .content-around{ -webkit-align-content:space-around; -ms-flex-line-pack:distribute; align-content:space-around } |  | ||||||
| .content-stretch{ -webkit-align-content:stretch; -ms-flex-line-pack:stretch; align-content:stretch } |  | ||||||
| .flex-auto{ |  | ||||||
|   -webkit-box-flex:1; |  | ||||||
|   -webkit-flex:1 1 auto; |  | ||||||
|       -ms-flex:1 1 auto; |  | ||||||
|           flex:1 1 auto; |  | ||||||
|   min-width:0; |  | ||||||
|   min-height:0; |  | ||||||
| } |  | ||||||
| .flex-none{ -webkit-box-flex:0; -webkit-flex:none; -ms-flex:none; flex:none } |  | ||||||
| .fs0{ flex-shrink: 0 } |  | ||||||
|  |  | ||||||
| .order-0{ -webkit-box-ordinal-group:1; -webkit-order:0; -ms-flex-order:0; order:0 } |  | ||||||
| .order-1{ -webkit-box-ordinal-group:2; -webkit-order:1; -ms-flex-order:1; order:1 } |  | ||||||
| .order-2{ -webkit-box-ordinal-group:3; -webkit-order:2; -ms-flex-order:2; order:2 } |  | ||||||
| .order-3{ -webkit-box-ordinal-group:4; -webkit-order:3; -ms-flex-order:3; order:3 } |  | ||||||
| .order-last{ -webkit-box-ordinal-group:100000; -webkit-order:99999; -ms-flex-order:99999; order:99999 } |  | ||||||
|  |  | ||||||
| .relative{ position:relative } |  | ||||||
| .absolute{ position:absolute } |  | ||||||
| .fixed{ position:fixed } |  | ||||||
|  |  | ||||||
| .top-0{ top:0 } |  | ||||||
| .right-0{ right:0 } |  | ||||||
| .bottom-0{ bottom:0 } |  | ||||||
| .left-0{ left:0 } |  | ||||||
|  |  | ||||||
| .z1{ z-index: 1 } |  | ||||||
| .z2{ z-index: 2 } |  | ||||||
| .z3{ z-index: 3 } |  | ||||||
| .z4{ z-index: 4 } |  | ||||||
|  |  | ||||||
| .border{ |  | ||||||
|   border-style:solid; |  | ||||||
|   border-width: 1px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .border-top{ |  | ||||||
|   border-top-style:solid; |  | ||||||
|   border-top-width: 1px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .border-right{ |  | ||||||
|   border-right-style:solid; |  | ||||||
|   border-right-width: 1px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .border-bottom{ |  | ||||||
|   border-bottom-style:solid; |  | ||||||
|   border-bottom-width: 1px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .border-left{ |  | ||||||
|   border-left-style:solid; |  | ||||||
|   border-left-width: 1px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .border-none{ border:0 } |  | ||||||
|  |  | ||||||
| .rounded{ border-radius: 3px } |  | ||||||
| .circle{ border-radius:50% } |  | ||||||
|  |  | ||||||
| .rounded-top{ border-radius: 3px 3px 0 0 } |  | ||||||
| .rounded-right{ border-radius: 0 3px 3px 0 } |  | ||||||
| .rounded-bottom{ border-radius: 0 0 3px 3px } |  | ||||||
| .rounded-left{ border-radius: 3px 0 0 3px } |  | ||||||
|  |  | ||||||
| .not-rounded{ border-radius:0 } |  | ||||||
|  |  | ||||||
| .hide{ |  | ||||||
|   position:absolute !important; |  | ||||||
|   height:1px; |  | ||||||
|   width:1px; |  | ||||||
|   overflow:hidden; |  | ||||||
|   clip:rect(1px, 1px, 1px, 1px); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (max-width: 40em){ |  | ||||||
|   .xs-hide{ display:none !important } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (min-width: 40em) and (max-width: 52em){ |  | ||||||
|   .sm-hide{ display:none !important } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (min-width: 52em) and (max-width: 64em){ |  | ||||||
|   .md-hide{ display:none !important } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (min-width: 64em){ |  | ||||||
|   .lg-hide{ display:none !important } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .display-none{ display:none !important } |  | ||||||
|  |  | ||||||
| @@ -1,93 +0,0 @@ | |||||||
| Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. |  | ||||||
|  |  | ||||||
| This Font Software is licensed under the SIL Open Font License, Version 1.1. |  | ||||||
|  |  | ||||||
| This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ----------------------------------------------------------- |  | ||||||
| SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 |  | ||||||
| ----------------------------------------------------------- |  | ||||||
|  |  | ||||||
| PREAMBLE |  | ||||||
| The goals of the Open Font License (OFL) are to stimulate worldwide |  | ||||||
| development of collaborative font projects, to support the font creation |  | ||||||
| efforts of academic and linguistic communities, and to provide a free and |  | ||||||
| open framework in which fonts may be shared and improved in partnership |  | ||||||
| with others. |  | ||||||
|  |  | ||||||
| The OFL allows the licensed fonts to be used, studied, modified and |  | ||||||
| redistributed freely as long as they are not sold by themselves. The |  | ||||||
| fonts, including any derivative works, can be bundled, embedded,  |  | ||||||
| redistributed and/or sold with any software provided that any reserved |  | ||||||
| names are not used by derivative works. The fonts and derivatives, |  | ||||||
| however, cannot be released under any other type of license. The |  | ||||||
| requirement for fonts to remain under this license does not apply |  | ||||||
| to any document created using the fonts or their derivatives. |  | ||||||
|  |  | ||||||
| DEFINITIONS |  | ||||||
| "Font Software" refers to the set of files released by the Copyright |  | ||||||
| Holder(s) under this license and clearly marked as such. This may |  | ||||||
| include source files, build scripts and documentation. |  | ||||||
|  |  | ||||||
| "Reserved Font Name" refers to any names specified as such after the |  | ||||||
| copyright statement(s). |  | ||||||
|  |  | ||||||
| "Original Version" refers to the collection of Font Software components as |  | ||||||
| distributed by the Copyright Holder(s). |  | ||||||
|  |  | ||||||
| "Modified Version" refers to any derivative made by adding to, deleting, |  | ||||||
| or substituting -- in part or in whole -- any of the components of the |  | ||||||
| Original Version, by changing formats or by porting the Font Software to a |  | ||||||
| new environment. |  | ||||||
|  |  | ||||||
| "Author" refers to any designer, engineer, programmer, technical |  | ||||||
| writer or other person who contributed to the Font Software. |  | ||||||
|  |  | ||||||
| PERMISSION & CONDITIONS |  | ||||||
| Permission is hereby granted, free of charge, to any person obtaining |  | ||||||
| a copy of the Font Software, to use, study, copy, merge, embed, modify, |  | ||||||
| redistribute, and sell modified and unmodified copies of the Font |  | ||||||
| Software, subject to the following conditions: |  | ||||||
|  |  | ||||||
| 1) Neither the Font Software nor any of its individual components, |  | ||||||
| in Original or Modified Versions, may be sold by itself. |  | ||||||
|  |  | ||||||
| 2) Original or Modified Versions of the Font Software may be bundled, |  | ||||||
| redistributed and/or sold with any software, provided that each copy |  | ||||||
| contains the above copyright notice and this license. These can be |  | ||||||
| included either as stand-alone text files, human-readable headers or |  | ||||||
| in the appropriate machine-readable metadata fields within text or |  | ||||||
| binary files as long as those fields can be easily viewed by the user. |  | ||||||
|  |  | ||||||
| 3) No Modified Version of the Font Software may use the Reserved Font |  | ||||||
| Name(s) unless explicit written permission is granted by the corresponding |  | ||||||
| Copyright Holder. This restriction only applies to the primary font name as |  | ||||||
| presented to the users. |  | ||||||
|  |  | ||||||
| 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font |  | ||||||
| Software shall not be used to promote, endorse or advertise any |  | ||||||
| Modified Version, except to acknowledge the contribution(s) of the |  | ||||||
| Copyright Holder(s) and the Author(s) or with their explicit written |  | ||||||
| permission. |  | ||||||
|  |  | ||||||
| 5) The Font Software, modified or unmodified, in part or in whole, |  | ||||||
| must be distributed entirely under this license, and must not be |  | ||||||
| distributed under any other license. The requirement for fonts to |  | ||||||
| remain under this license does not apply to any document created |  | ||||||
| using the Font Software. |  | ||||||
|  |  | ||||||
| TERMINATION |  | ||||||
| This license becomes null and void if any of the above conditions are |  | ||||||
| not met. |  | ||||||
|  |  | ||||||
| DISCLAIMER |  | ||||||
| THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |  | ||||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF |  | ||||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT |  | ||||||
| OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE |  | ||||||
| COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |  | ||||||
| INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL |  | ||||||
| DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |  | ||||||
| FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM |  | ||||||
| OTHER DEALINGS IN THE FONT SOFTWARE. |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| @font-face{ |  | ||||||
|     font-family: 'Source Code Pro'; |  | ||||||
|     font-weight: 400; |  | ||||||
|     font-style: normal; |  | ||||||
|     font-stretch: normal; |  | ||||||
|     src: url('EOT/SourceCodePro-Regular.eot') format('embedded-opentype'), |  | ||||||
|          url('WOFF2/TTF/SourceCodePro-Regular.ttf.woff2') format('woff2'), |  | ||||||
|          url('WOFF/OTF/SourceCodePro-Regular.otf.woff') format('woff'), |  | ||||||
|          url('OTF/SourceCodePro-Regular.otf') format('opentype'), |  | ||||||
|          url('TTF/SourceCodePro-Regular.ttf') format('truetype'); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @font-face{ |  | ||||||
|     font-family: 'Source Code Pro'; |  | ||||||
|     font-weight: 700; |  | ||||||
|     font-style: normal; |  | ||||||
|     font-stretch: normal; |  | ||||||
|     src: url('EOT/SourceCodePro-Bold.eot') format('embedded-opentype'), |  | ||||||
|          url('WOFF2/TTF/SourceCodePro-Bold.ttf.woff2') format('woff2'), |  | ||||||
|          url('WOFF/OTF/SourceCodePro-Bold.otf.woff') format('woff'), |  | ||||||
|          url('OTF/SourceCodePro-Bold.otf') format('opentype'), |  | ||||||
|          url('TTF/SourceCodePro-Bold.ttf') format('truetype'); |  | ||||||
| } |  | ||||||
| @@ -1,123 +0,0 @@ | |||||||
| /* |  | ||||||
|  |  | ||||||
| github.com style (c) Vasily Polovnyov <vast@whiteants.net> |  | ||||||
|  |  | ||||||
| */ |  | ||||||
|  |  | ||||||
| .hljs { |  | ||||||
|   display: block; |  | ||||||
|   overflow-x: auto; |  | ||||||
|   padding: 0.5em; |  | ||||||
|   color: #333; |  | ||||||
|   background: #f8f8f8; |  | ||||||
|   -webkit-text-size-adjust: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-comment, |  | ||||||
| .diff .hljs-header, |  | ||||||
| .hljs-javadoc { |  | ||||||
|   color: #998; |  | ||||||
|   font-style: italic; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-keyword, |  | ||||||
| .css .rule .hljs-keyword, |  | ||||||
| .hljs-winutils, |  | ||||||
| .nginx .hljs-title, |  | ||||||
| .hljs-subst, |  | ||||||
| .hljs-request, |  | ||||||
| .hljs-status { |  | ||||||
|   color: #1184CE; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-number, |  | ||||||
| .hljs-hexcolor, |  | ||||||
| .ruby .hljs-constant { |  | ||||||
|   color: #ed225d; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-string, |  | ||||||
| .hljs-tag .hljs-value, |  | ||||||
| .hljs-phpdoc, |  | ||||||
| .hljs-dartdoc, |  | ||||||
| .tex .hljs-formula { |  | ||||||
|   color: #ed225d; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-title, |  | ||||||
| .hljs-id, |  | ||||||
| .scss .hljs-preprocessor { |  | ||||||
|   color: #900; |  | ||||||
|   font-weight: bold; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-list .hljs-keyword, |  | ||||||
| .hljs-subst { |  | ||||||
|   font-weight: normal; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-class .hljs-title, |  | ||||||
| .hljs-type, |  | ||||||
| .vhdl .hljs-literal, |  | ||||||
| .tex .hljs-command { |  | ||||||
|   color: #458; |  | ||||||
|   font-weight: bold; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-tag, |  | ||||||
| .hljs-tag .hljs-title, |  | ||||||
| .hljs-rules .hljs-property, |  | ||||||
| .django .hljs-tag .hljs-keyword { |  | ||||||
|   color: #000080; |  | ||||||
|   font-weight: normal; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-attribute, |  | ||||||
| .hljs-variable, |  | ||||||
| .lisp .hljs-body { |  | ||||||
|   color: #008080; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-regexp { |  | ||||||
|   color: #009926; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-symbol, |  | ||||||
| .ruby .hljs-symbol .hljs-string, |  | ||||||
| .lisp .hljs-keyword, |  | ||||||
| .clojure .hljs-keyword, |  | ||||||
| .scheme .hljs-keyword, |  | ||||||
| .tex .hljs-special, |  | ||||||
| .hljs-prompt { |  | ||||||
|   color: #990073; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-built_in { |  | ||||||
|   color: #0086b3; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-preprocessor, |  | ||||||
| .hljs-pragma, |  | ||||||
| .hljs-pi, |  | ||||||
| .hljs-doctype, |  | ||||||
| .hljs-shebang, |  | ||||||
| .hljs-cdata { |  | ||||||
|   color: #999; |  | ||||||
|   font-weight: bold; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-deletion { |  | ||||||
|   background: #fdd; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-addition { |  | ||||||
|   background: #dfd; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .diff .hljs-change { |  | ||||||
|   background: #0086b3; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hljs-chunk { |  | ||||||
|   color: #aaa; |  | ||||||
| } |  | ||||||
| @@ -1,168 +0,0 @@ | |||||||
| /* global anchors */ |  | ||||||
|  |  | ||||||
| // add anchor links to headers |  | ||||||
| anchors.options.placement = 'left'; |  | ||||||
| anchors.add('h3'); |  | ||||||
|  |  | ||||||
| // Filter UI |  | ||||||
| var tocElements = document.getElementById('toc').getElementsByTagName('li'); |  | ||||||
|  |  | ||||||
| document.getElementById('filter-input').addEventListener('keyup', function(e) { |  | ||||||
|   var i, element, children; |  | ||||||
|  |  | ||||||
|   // enter key |  | ||||||
|   if (e.keyCode === 13) { |  | ||||||
|     // go to the first displayed item in the toc |  | ||||||
|     for (i = 0; i < tocElements.length; i++) { |  | ||||||
|       element = tocElements[i]; |  | ||||||
|       if (!element.classList.contains('display-none')) { |  | ||||||
|         location.replace(element.firstChild.href); |  | ||||||
|         return e.preventDefault(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   var match = function() { |  | ||||||
|     return true; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   var value = this.value.toLowerCase(); |  | ||||||
|  |  | ||||||
|   if (!value.match(/^\s*$/)) { |  | ||||||
|     match = function(element) { |  | ||||||
|       var html = element.firstChild.innerHTML; |  | ||||||
|       return html && html.toLowerCase().indexOf(value) !== -1; |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   for (i = 0; i < tocElements.length; i++) { |  | ||||||
|     element = tocElements[i]; |  | ||||||
|     children = Array.from(element.getElementsByTagName('li')); |  | ||||||
|     if (match(element) || children.some(match)) { |  | ||||||
|       element.classList.remove('display-none'); |  | ||||||
|     } else { |  | ||||||
|       element.classList.add('display-none'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| var items = document.getElementsByClassName('toggle-sibling'); |  | ||||||
| for (var j = 0; j < items.length; j++) { |  | ||||||
|   items[j].addEventListener('click', toggleSibling); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function toggleSibling() { |  | ||||||
|   var stepSibling = this.parentNode.getElementsByClassName('toggle-target')[0]; |  | ||||||
|   var icon = this.getElementsByClassName('icon')[0]; |  | ||||||
|   var klass = 'display-none'; |  | ||||||
|   if (stepSibling.classList.contains(klass)) { |  | ||||||
|     stepSibling.classList.remove(klass); |  | ||||||
|     icon.innerHTML = '▾'; |  | ||||||
|   } else { |  | ||||||
|     stepSibling.classList.add(klass); |  | ||||||
|     icon.innerHTML = '▸'; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function showHashTarget(targetId) { |  | ||||||
|   if (targetId) { |  | ||||||
|     var hashTarget = document.getElementById(targetId); |  | ||||||
|     // new target is hidden |  | ||||||
|     if ( |  | ||||||
|       hashTarget && |  | ||||||
|       hashTarget.offsetHeight === 0 && |  | ||||||
|       hashTarget.parentNode.parentNode.classList.contains('display-none') |  | ||||||
|     ) { |  | ||||||
|       hashTarget.parentNode.parentNode.classList.remove('display-none'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function scrollIntoView(targetId) { |  | ||||||
|   // Only scroll to element if we don't have a stored scroll position. |  | ||||||
|   if (targetId && !history.state) { |  | ||||||
|     var hashTarget = document.getElementById(targetId); |  | ||||||
|     if (hashTarget) { |  | ||||||
|       hashTarget.scrollIntoView(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function gotoCurrentTarget() { |  | ||||||
|   showHashTarget(location.hash.substring(1)); |  | ||||||
|   scrollIntoView(location.hash.substring(1)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| window.addEventListener('hashchange', gotoCurrentTarget); |  | ||||||
| gotoCurrentTarget(); |  | ||||||
|  |  | ||||||
| var toclinks = document.getElementsByClassName('pre-open'); |  | ||||||
| for (var k = 0; k < toclinks.length; k++) { |  | ||||||
|   toclinks[k].addEventListener('mousedown', preOpen, false); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function preOpen() { |  | ||||||
|   showHashTarget(this.hash.substring(1)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var split_left = document.querySelector('#split-left'); |  | ||||||
| var split_right = document.querySelector('#split-right'); |  | ||||||
| var split_parent = split_left.parentNode; |  | ||||||
| var cw_with_sb = split_left.clientWidth; |  | ||||||
| split_left.style.overflow = 'hidden'; |  | ||||||
| var cw_without_sb = split_left.clientWidth; |  | ||||||
| split_left.style.overflow = ''; |  | ||||||
|  |  | ||||||
| Split(['#split-left', '#split-right'], { |  | ||||||
|   elementStyle: function(dimension, size, gutterSize) { |  | ||||||
|     return { |  | ||||||
|       'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)' |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   gutterStyle: function(dimension, gutterSize) { |  | ||||||
|     return { |  | ||||||
|       'flex-basis': gutterSize + 'px' |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   gutterSize: 20, |  | ||||||
|   sizes: [33, 67] |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| // Chrome doesn't remember scroll position properly so do it ourselves. |  | ||||||
| // Also works on Firefox and Edge. |  | ||||||
|  |  | ||||||
| function updateState() { |  | ||||||
|   history.replaceState( |  | ||||||
|     { |  | ||||||
|       left_top: split_left.scrollTop, |  | ||||||
|       right_top: split_right.scrollTop |  | ||||||
|     }, |  | ||||||
|     document.title |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function loadState(ev) { |  | ||||||
|   if (ev) { |  | ||||||
|     // Edge doesn't replace change history.state on popstate. |  | ||||||
|     history.replaceState(ev.state, document.title); |  | ||||||
|   } |  | ||||||
|   if (history.state) { |  | ||||||
|     split_left.scrollTop = history.state.left_top; |  | ||||||
|     split_right.scrollTop = history.state.right_top; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| window.addEventListener('load', function() { |  | ||||||
|   // Restore after Firefox scrolls to hash. |  | ||||||
|   setTimeout(function() { |  | ||||||
|     loadState(); |  | ||||||
|     // Update with initial scroll position. |  | ||||||
|     updateState(); |  | ||||||
|     // Update scroll positions only after we've loaded because Firefox |  | ||||||
|     // emits an initial scroll event with 0. |  | ||||||
|     split_left.addEventListener('scroll', updateState); |  | ||||||
|     split_right.addEventListener('scroll', updateState); |  | ||||||
|   }, 1); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| window.addEventListener('popstate', loadState); |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| .gutter { |  | ||||||
|     background-color: #f5f5f5; |  | ||||||
|     background-repeat: no-repeat; |  | ||||||
|     background-position: 50%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .gutter.gutter-vertical { |  | ||||||
|     background-image:  url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); |  | ||||||
|     cursor: ns-resize; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .gutter.gutter-horizontal { |  | ||||||
|     background-image:  url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); |  | ||||||
|     cursor: ew-resize; |  | ||||||
| } |  | ||||||
| @@ -1,586 +0,0 @@ | |||||||
| /*! Split.js - v1.3.5 */ |  | ||||||
| // https://github.com/nathancahill/Split.js |  | ||||||
| // Copyright (c) 2017 Nathan Cahill; Licensed MIT |  | ||||||
|  |  | ||||||
| (function(global, factory) { |  | ||||||
|   typeof exports === 'object' && typeof module !== 'undefined' |  | ||||||
|     ? (module.exports = factory()) |  | ||||||
|     : typeof define === 'function' && define.amd |  | ||||||
|       ? define(factory) |  | ||||||
|       : (global.Split = factory()); |  | ||||||
| })(this, function() { |  | ||||||
|   'use strict'; |  | ||||||
|   // The programming goals of Split.js are to deliver readable, understandable and |  | ||||||
|   // maintainable code, while at the same time manually optimizing for tiny minified file size, |  | ||||||
|   // browser compatibility without additional requirements, graceful fallback (IE8 is supported) |  | ||||||
|   // and very few assumptions about the user's page layout. |  | ||||||
|   var global = window; |  | ||||||
|   var document = global.document; |  | ||||||
|  |  | ||||||
|   // Save a couple long function names that are used frequently. |  | ||||||
|   // This optimization saves around 400 bytes. |  | ||||||
|   var addEventListener = 'addEventListener'; |  | ||||||
|   var removeEventListener = 'removeEventListener'; |  | ||||||
|   var getBoundingClientRect = 'getBoundingClientRect'; |  | ||||||
|   var NOOP = function() { |  | ||||||
|     return false; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   // Figure out if we're in IE8 or not. IE8 will still render correctly, |  | ||||||
|   // but will be static instead of draggable. |  | ||||||
|   var isIE8 = global.attachEvent && !global[addEventListener]; |  | ||||||
|  |  | ||||||
|   // This library only needs two helper functions: |  | ||||||
|   // |  | ||||||
|   // The first determines which prefixes of CSS calc we need. |  | ||||||
|   // We only need to do this once on startup, when this anonymous function is called. |  | ||||||
|   // |  | ||||||
|   // Tests -webkit, -moz and -o prefixes. Modified from StackOverflow: |  | ||||||
|   // http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167 |  | ||||||
|   var calc = |  | ||||||
|     ['', '-webkit-', '-moz-', '-o-'] |  | ||||||
|       .filter(function(prefix) { |  | ||||||
|         var el = document.createElement('div'); |  | ||||||
|         el.style.cssText = 'width:' + prefix + 'calc(9px)'; |  | ||||||
|  |  | ||||||
|         return !!el.style.length; |  | ||||||
|       }) |  | ||||||
|       .shift() + 'calc'; |  | ||||||
|  |  | ||||||
|   // The second helper function allows elements and string selectors to be used |  | ||||||
|   // interchangeably. In either case an element is returned. This allows us to |  | ||||||
|   // do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`. |  | ||||||
|   var elementOrSelector = function(el) { |  | ||||||
|     if (typeof el === 'string' || el instanceof String) { |  | ||||||
|       return document.querySelector(el); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return el; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   // The main function to initialize a split. Split.js thinks about each pair |  | ||||||
|   // of elements as an independant pair. Dragging the gutter between two elements |  | ||||||
|   // only changes the dimensions of elements in that pair. This is key to understanding |  | ||||||
|   // how the following functions operate, since each function is bound to a pair. |  | ||||||
|   // |  | ||||||
|   // A pair object is shaped like this: |  | ||||||
|   // |  | ||||||
|   // { |  | ||||||
|   //     a: DOM element, |  | ||||||
|   //     b: DOM element, |  | ||||||
|   //     aMin: Number, |  | ||||||
|   //     bMin: Number, |  | ||||||
|   //     dragging: Boolean, |  | ||||||
|   //     parent: DOM element, |  | ||||||
|   //     isFirst: Boolean, |  | ||||||
|   //     isLast: Boolean, |  | ||||||
|   //     direction: 'horizontal' | 'vertical' |  | ||||||
|   // } |  | ||||||
|   // |  | ||||||
|   // The basic sequence: |  | ||||||
|   // |  | ||||||
|   // 1. Set defaults to something sane. `options` doesn't have to be passed at all. |  | ||||||
|   // 2. Initialize a bunch of strings based on the direction we're splitting. |  | ||||||
|   //    A lot of the behavior in the rest of the library is paramatized down to |  | ||||||
|   //    rely on CSS strings and classes. |  | ||||||
|   // 3. Define the dragging helper functions, and a few helpers to go with them. |  | ||||||
|   // 4. Loop through the elements while pairing them off. Every pair gets an |  | ||||||
|   //    `pair` object, a gutter, and special isFirst/isLast properties. |  | ||||||
|   // 5. Actually size the pair elements, insert gutters and attach event listeners. |  | ||||||
|   var Split = function(ids, options) { |  | ||||||
|     if (options === void 0) options = {}; |  | ||||||
|  |  | ||||||
|     var dimension; |  | ||||||
|     var clientDimension; |  | ||||||
|     var clientAxis; |  | ||||||
|     var position; |  | ||||||
|     var paddingA; |  | ||||||
|     var paddingB; |  | ||||||
|     var elements; |  | ||||||
|  |  | ||||||
|     // All DOM elements in the split should have a common parent. We can grab |  | ||||||
|     // the first elements parent and hope users read the docs because the |  | ||||||
|     // behavior will be whacky otherwise. |  | ||||||
|     var parent = elementOrSelector(ids[0]).parentNode; |  | ||||||
|     var parentFlexDirection = global.getComputedStyle(parent).flexDirection; |  | ||||||
|  |  | ||||||
|     // Set default options.sizes to equal percentages of the parent element. |  | ||||||
|     var sizes = |  | ||||||
|       options.sizes || |  | ||||||
|       ids.map(function() { |  | ||||||
|         return 100 / ids.length; |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|     // Standardize minSize to an array if it isn't already. This allows minSize |  | ||||||
|     // to be passed as a number. |  | ||||||
|     var minSize = options.minSize !== undefined ? options.minSize : 100; |  | ||||||
|     var minSizes = Array.isArray(minSize) |  | ||||||
|       ? minSize |  | ||||||
|       : ids.map(function() { |  | ||||||
|           return minSize; |  | ||||||
|         }); |  | ||||||
|     var gutterSize = options.gutterSize !== undefined ? options.gutterSize : 10; |  | ||||||
|     var snapOffset = options.snapOffset !== undefined ? options.snapOffset : 30; |  | ||||||
|     var direction = options.direction || 'horizontal'; |  | ||||||
|     var cursor = |  | ||||||
|       options.cursor || |  | ||||||
|       (direction === 'horizontal' ? 'ew-resize' : 'ns-resize'); |  | ||||||
|     var gutter = |  | ||||||
|       options.gutter || |  | ||||||
|       function(i, gutterDirection) { |  | ||||||
|         var gut = document.createElement('div'); |  | ||||||
|         gut.className = 'gutter gutter-' + gutterDirection; |  | ||||||
|         return gut; |  | ||||||
|       }; |  | ||||||
|     var elementStyle = |  | ||||||
|       options.elementStyle || |  | ||||||
|       function(dim, size, gutSize) { |  | ||||||
|         var style = {}; |  | ||||||
|  |  | ||||||
|         if (typeof size !== 'string' && !(size instanceof String)) { |  | ||||||
|           if (!isIE8) { |  | ||||||
|             style[dim] = calc + '(' + size + '% - ' + gutSize + 'px)'; |  | ||||||
|           } else { |  | ||||||
|             style[dim] = size + '%'; |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           style[dim] = size; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return style; |  | ||||||
|       }; |  | ||||||
|     var gutterStyle = |  | ||||||
|       options.gutterStyle || |  | ||||||
|       function(dim, gutSize) { |  | ||||||
|         return (obj = {}), (obj[dim] = gutSize + 'px'), obj; |  | ||||||
|         var obj; |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|     // 2. Initialize a bunch of strings based on the direction we're splitting. |  | ||||||
|     // A lot of the behavior in the rest of the library is paramatized down to |  | ||||||
|     // rely on CSS strings and classes. |  | ||||||
|     if (direction === 'horizontal') { |  | ||||||
|       dimension = 'width'; |  | ||||||
|       clientDimension = 'clientWidth'; |  | ||||||
|       clientAxis = 'clientX'; |  | ||||||
|       position = 'left'; |  | ||||||
|       paddingA = 'paddingLeft'; |  | ||||||
|       paddingB = 'paddingRight'; |  | ||||||
|     } else if (direction === 'vertical') { |  | ||||||
|       dimension = 'height'; |  | ||||||
|       clientDimension = 'clientHeight'; |  | ||||||
|       clientAxis = 'clientY'; |  | ||||||
|       position = 'top'; |  | ||||||
|       paddingA = 'paddingTop'; |  | ||||||
|       paddingB = 'paddingBottom'; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // 3. Define the dragging helper functions, and a few helpers to go with them. |  | ||||||
|     // Each helper is bound to a pair object that contains it's metadata. This |  | ||||||
|     // also makes it easy to store references to listeners that that will be |  | ||||||
|     // added and removed. |  | ||||||
|     // |  | ||||||
|     // Even though there are no other functions contained in them, aliasing |  | ||||||
|     // this to self saves 50 bytes or so since it's used so frequently. |  | ||||||
|     // |  | ||||||
|     // The pair object saves metadata like dragging state, position and |  | ||||||
|     // event listener references. |  | ||||||
|  |  | ||||||
|     function setElementSize(el, size, gutSize) { |  | ||||||
|       // Split.js allows setting sizes via numbers (ideally), or if you must, |  | ||||||
|       // by string, like '300px'. This is less than ideal, because it breaks |  | ||||||
|       // the fluid layout that `calc(% - px)` provides. You're on your own if you do that, |  | ||||||
|       // make sure you calculate the gutter size by hand. |  | ||||||
|       var style = elementStyle(dimension, size, gutSize); |  | ||||||
|  |  | ||||||
|       // eslint-disable-next-line no-param-reassign |  | ||||||
|       Object.keys(style).forEach(function(prop) { |  | ||||||
|         return (el.style[prop] = style[prop]); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setGutterSize(gutterElement, gutSize) { |  | ||||||
|       var style = gutterStyle(dimension, gutSize); |  | ||||||
|  |  | ||||||
|       // eslint-disable-next-line no-param-reassign |  | ||||||
|       Object.keys(style).forEach(function(prop) { |  | ||||||
|         return (gutterElement.style[prop] = style[prop]); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Actually adjust the size of elements `a` and `b` to `offset` while dragging. |  | ||||||
|     // calc is used to allow calc(percentage + gutterpx) on the whole split instance, |  | ||||||
|     // which allows the viewport to be resized without additional logic. |  | ||||||
|     // Element a's size is the same as offset. b's size is total size - a size. |  | ||||||
|     // Both sizes are calculated from the initial parent percentage, |  | ||||||
|     // then the gutter size is subtracted. |  | ||||||
|     function adjust(offset) { |  | ||||||
|       var a = elements[this.a]; |  | ||||||
|       var b = elements[this.b]; |  | ||||||
|       var percentage = a.size + b.size; |  | ||||||
|  |  | ||||||
|       a.size = (offset / this.size) * percentage; |  | ||||||
|       b.size = percentage - (offset / this.size) * percentage; |  | ||||||
|  |  | ||||||
|       setElementSize(a.element, a.size, this.aGutterSize); |  | ||||||
|       setElementSize(b.element, b.size, this.bGutterSize); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // drag, where all the magic happens. The logic is really quite simple: |  | ||||||
|     // |  | ||||||
|     // 1. Ignore if the pair is not dragging. |  | ||||||
|     // 2. Get the offset of the event. |  | ||||||
|     // 3. Snap offset to min if within snappable range (within min + snapOffset). |  | ||||||
|     // 4. Actually adjust each element in the pair to offset. |  | ||||||
|     // |  | ||||||
|     // --------------------------------------------------------------------- |  | ||||||
|     // |    | <- a.minSize               ||              b.minSize -> |    | |  | ||||||
|     // |    |  | <- this.snapOffset      ||     this.snapOffset -> |  |    | |  | ||||||
|     // |    |  |                         ||                        |  |    | |  | ||||||
|     // |    |  |                         ||                        |  |    | |  | ||||||
|     // --------------------------------------------------------------------- |  | ||||||
|     // | <- this.start                                        this.size -> | |  | ||||||
|     function drag(e) { |  | ||||||
|       var offset; |  | ||||||
|  |  | ||||||
|       if (!this.dragging) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Get the offset of the event from the first side of the |  | ||||||
|       // pair `this.start`. Supports touch events, but not multitouch, so only the first |  | ||||||
|       // finger `touches[0]` is counted. |  | ||||||
|       if ('touches' in e) { |  | ||||||
|         offset = e.touches[0][clientAxis] - this.start; |  | ||||||
|       } else { |  | ||||||
|         offset = e[clientAxis] - this.start; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // If within snapOffset of min or max, set offset to min or max. |  | ||||||
|       // snapOffset buffers a.minSize and b.minSize, so logic is opposite for both. |  | ||||||
|       // Include the appropriate gutter sizes to prevent overflows. |  | ||||||
|       if (offset <= elements[this.a].minSize + snapOffset + this.aGutterSize) { |  | ||||||
|         offset = elements[this.a].minSize + this.aGutterSize; |  | ||||||
|       } else if ( |  | ||||||
|         offset >= |  | ||||||
|         this.size - (elements[this.b].minSize + snapOffset + this.bGutterSize) |  | ||||||
|       ) { |  | ||||||
|         offset = this.size - (elements[this.b].minSize + this.bGutterSize); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Actually adjust the size. |  | ||||||
|       adjust.call(this, offset); |  | ||||||
|  |  | ||||||
|       // Call the drag callback continously. Don't do anything too intensive |  | ||||||
|       // in this callback. |  | ||||||
|       if (options.onDrag) { |  | ||||||
|         options.onDrag(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Cache some important sizes when drag starts, so we don't have to do that |  | ||||||
|     // continously: |  | ||||||
|     // |  | ||||||
|     // `size`: The total size of the pair. First + second + first gutter + second gutter. |  | ||||||
|     // `start`: The leading side of the first element. |  | ||||||
|     // |  | ||||||
|     // ------------------------------------------------ |  | ||||||
|     // |      aGutterSize -> |||                      | |  | ||||||
|     // |                     |||                      | |  | ||||||
|     // |                     |||                      | |  | ||||||
|     // |                     ||| <- bGutterSize       | |  | ||||||
|     // ------------------------------------------------ |  | ||||||
|     // | <- start                             size -> | |  | ||||||
|     function calculateSizes() { |  | ||||||
|       // Figure out the parent size minus padding. |  | ||||||
|       var a = elements[this.a].element; |  | ||||||
|       var b = elements[this.b].element; |  | ||||||
|  |  | ||||||
|       this.size = |  | ||||||
|         a[getBoundingClientRect]()[dimension] + |  | ||||||
|         b[getBoundingClientRect]()[dimension] + |  | ||||||
|         this.aGutterSize + |  | ||||||
|         this.bGutterSize; |  | ||||||
|       this.start = a[getBoundingClientRect]()[position]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // stopDragging is very similar to startDragging in reverse. |  | ||||||
|     function stopDragging() { |  | ||||||
|       var self = this; |  | ||||||
|       var a = elements[self.a].element; |  | ||||||
|       var b = elements[self.b].element; |  | ||||||
|  |  | ||||||
|       if (self.dragging && options.onDragEnd) { |  | ||||||
|         options.onDragEnd(); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       self.dragging = false; |  | ||||||
|  |  | ||||||
|       // Remove the stored event listeners. This is why we store them. |  | ||||||
|       global[removeEventListener]('mouseup', self.stop); |  | ||||||
|       global[removeEventListener]('touchend', self.stop); |  | ||||||
|       global[removeEventListener]('touchcancel', self.stop); |  | ||||||
|  |  | ||||||
|       self.parent[removeEventListener]('mousemove', self.move); |  | ||||||
|       self.parent[removeEventListener]('touchmove', self.move); |  | ||||||
|  |  | ||||||
|       // Delete them once they are removed. I think this makes a difference |  | ||||||
|       // in memory usage with a lot of splits on one page. But I don't know for sure. |  | ||||||
|       delete self.stop; |  | ||||||
|       delete self.move; |  | ||||||
|  |  | ||||||
|       a[removeEventListener]('selectstart', NOOP); |  | ||||||
|       a[removeEventListener]('dragstart', NOOP); |  | ||||||
|       b[removeEventListener]('selectstart', NOOP); |  | ||||||
|       b[removeEventListener]('dragstart', NOOP); |  | ||||||
|  |  | ||||||
|       a.style.userSelect = ''; |  | ||||||
|       a.style.webkitUserSelect = ''; |  | ||||||
|       a.style.MozUserSelect = ''; |  | ||||||
|       a.style.pointerEvents = ''; |  | ||||||
|  |  | ||||||
|       b.style.userSelect = ''; |  | ||||||
|       b.style.webkitUserSelect = ''; |  | ||||||
|       b.style.MozUserSelect = ''; |  | ||||||
|       b.style.pointerEvents = ''; |  | ||||||
|  |  | ||||||
|       self.gutter.style.cursor = ''; |  | ||||||
|       self.parent.style.cursor = ''; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // startDragging calls `calculateSizes` to store the inital size in the pair object. |  | ||||||
|     // It also adds event listeners for mouse/touch events, |  | ||||||
|     // and prevents selection while dragging so avoid the selecting text. |  | ||||||
|     function startDragging(e) { |  | ||||||
|       // Alias frequently used variables to save space. 200 bytes. |  | ||||||
|       var self = this; |  | ||||||
|       var a = elements[self.a].element; |  | ||||||
|       var b = elements[self.b].element; |  | ||||||
|  |  | ||||||
|       // Call the onDragStart callback. |  | ||||||
|       if (!self.dragging && options.onDragStart) { |  | ||||||
|         options.onDragStart(); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Don't actually drag the element. We emulate that in the drag function. |  | ||||||
|       e.preventDefault(); |  | ||||||
|  |  | ||||||
|       // Set the dragging property of the pair object. |  | ||||||
|       self.dragging = true; |  | ||||||
|  |  | ||||||
|       // Create two event listeners bound to the same pair object and store |  | ||||||
|       // them in the pair object. |  | ||||||
|       self.move = drag.bind(self); |  | ||||||
|       self.stop = stopDragging.bind(self); |  | ||||||
|  |  | ||||||
|       // All the binding. `window` gets the stop events in case we drag out of the elements. |  | ||||||
|       global[addEventListener]('mouseup', self.stop); |  | ||||||
|       global[addEventListener]('touchend', self.stop); |  | ||||||
|       global[addEventListener]('touchcancel', self.stop); |  | ||||||
|  |  | ||||||
|       self.parent[addEventListener]('mousemove', self.move); |  | ||||||
|       self.parent[addEventListener]('touchmove', self.move); |  | ||||||
|  |  | ||||||
|       // Disable selection. Disable! |  | ||||||
|       a[addEventListener]('selectstart', NOOP); |  | ||||||
|       a[addEventListener]('dragstart', NOOP); |  | ||||||
|       b[addEventListener]('selectstart', NOOP); |  | ||||||
|       b[addEventListener]('dragstart', NOOP); |  | ||||||
|  |  | ||||||
|       a.style.userSelect = 'none'; |  | ||||||
|       a.style.webkitUserSelect = 'none'; |  | ||||||
|       a.style.MozUserSelect = 'none'; |  | ||||||
|       a.style.pointerEvents = 'none'; |  | ||||||
|  |  | ||||||
|       b.style.userSelect = 'none'; |  | ||||||
|       b.style.webkitUserSelect = 'none'; |  | ||||||
|       b.style.MozUserSelect = 'none'; |  | ||||||
|       b.style.pointerEvents = 'none'; |  | ||||||
|  |  | ||||||
|       // Set the cursor, both on the gutter and the parent element. |  | ||||||
|       // Doing only a, b and gutter causes flickering. |  | ||||||
|       self.gutter.style.cursor = cursor; |  | ||||||
|       self.parent.style.cursor = cursor; |  | ||||||
|  |  | ||||||
|       // Cache the initial sizes of the pair. |  | ||||||
|       calculateSizes.call(self); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // 5. Create pair and element objects. Each pair has an index reference to |  | ||||||
|     // elements `a` and `b` of the pair (first and second elements). |  | ||||||
|     // Loop through the elements while pairing them off. Every pair gets a |  | ||||||
|     // `pair` object, a gutter, and isFirst/isLast properties. |  | ||||||
|     // |  | ||||||
|     // Basic logic: |  | ||||||
|     // |  | ||||||
|     // - Starting with the second element `i > 0`, create `pair` objects with |  | ||||||
|     //   `a = i - 1` and `b = i` |  | ||||||
|     // - Set gutter sizes based on the _pair_ being first/last. The first and last |  | ||||||
|     //   pair have gutterSize / 2, since they only have one half gutter, and not two. |  | ||||||
|     // - Create gutter elements and add event listeners. |  | ||||||
|     // - Set the size of the elements, minus the gutter sizes. |  | ||||||
|     // |  | ||||||
|     // ----------------------------------------------------------------------- |  | ||||||
|     // |     i=0     |         i=1         |        i=2       |      i=3     | |  | ||||||
|     // |             |       isFirst       |                  |     isLast   | |  | ||||||
|     // |           pair 0                pair 1             pair 2           | |  | ||||||
|     // |             |                     |                  |              | |  | ||||||
|     // ----------------------------------------------------------------------- |  | ||||||
|     var pairs = []; |  | ||||||
|     elements = ids.map(function(id, i) { |  | ||||||
|       // Create the element object. |  | ||||||
|       var element = { |  | ||||||
|         element: elementOrSelector(id), |  | ||||||
|         size: sizes[i], |  | ||||||
|         minSize: minSizes[i] |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       var pair; |  | ||||||
|  |  | ||||||
|       if (i > 0) { |  | ||||||
|         // Create the pair object with it's metadata. |  | ||||||
|         pair = { |  | ||||||
|           a: i - 1, |  | ||||||
|           b: i, |  | ||||||
|           dragging: false, |  | ||||||
|           isFirst: i === 1, |  | ||||||
|           isLast: i === ids.length - 1, |  | ||||||
|           direction: direction, |  | ||||||
|           parent: parent |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         // For first and last pairs, first and last gutter width is half. |  | ||||||
|         pair.aGutterSize = gutterSize; |  | ||||||
|         pair.bGutterSize = gutterSize; |  | ||||||
|  |  | ||||||
|         if (pair.isFirst) { |  | ||||||
|           pair.aGutterSize = gutterSize / 2; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (pair.isLast) { |  | ||||||
|           pair.bGutterSize = gutterSize / 2; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // if the parent has a reverse flex-direction, switch the pair elements. |  | ||||||
|         if ( |  | ||||||
|           parentFlexDirection === 'row-reverse' || |  | ||||||
|           parentFlexDirection === 'column-reverse' |  | ||||||
|         ) { |  | ||||||
|           var temp = pair.a; |  | ||||||
|           pair.a = pair.b; |  | ||||||
|           pair.b = temp; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Determine the size of the current element. IE8 is supported by |  | ||||||
|       // staticly assigning sizes without draggable gutters. Assigns a string |  | ||||||
|       // to `size`. |  | ||||||
|       // |  | ||||||
|       // IE9 and above |  | ||||||
|       if (!isIE8) { |  | ||||||
|         // Create gutter elements for each pair. |  | ||||||
|         if (i > 0) { |  | ||||||
|           var gutterElement = gutter(i, direction); |  | ||||||
|           setGutterSize(gutterElement, gutterSize); |  | ||||||
|  |  | ||||||
|           gutterElement[addEventListener]( |  | ||||||
|             'mousedown', |  | ||||||
|             startDragging.bind(pair) |  | ||||||
|           ); |  | ||||||
|           gutterElement[addEventListener]( |  | ||||||
|             'touchstart', |  | ||||||
|             startDragging.bind(pair) |  | ||||||
|           ); |  | ||||||
|  |  | ||||||
|           parent.insertBefore(gutterElement, element.element); |  | ||||||
|  |  | ||||||
|           pair.gutter = gutterElement; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Set the element size to our determined size. |  | ||||||
|       // Half-size gutters for first and last elements. |  | ||||||
|       if (i === 0 || i === ids.length - 1) { |  | ||||||
|         setElementSize(element.element, element.size, gutterSize / 2); |  | ||||||
|       } else { |  | ||||||
|         setElementSize(element.element, element.size, gutterSize); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       var computedSize = element.element[getBoundingClientRect]()[dimension]; |  | ||||||
|  |  | ||||||
|       if (computedSize < element.minSize) { |  | ||||||
|         element.minSize = computedSize; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // After the first iteration, and we have a pair object, append it to the |  | ||||||
|       // list of pairs. |  | ||||||
|       if (i > 0) { |  | ||||||
|         pairs.push(pair); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return element; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     function setSizes(newSizes) { |  | ||||||
|       newSizes.forEach(function(newSize, i) { |  | ||||||
|         if (i > 0) { |  | ||||||
|           var pair = pairs[i - 1]; |  | ||||||
|           var a = elements[pair.a]; |  | ||||||
|           var b = elements[pair.b]; |  | ||||||
|  |  | ||||||
|           a.size = newSizes[i - 1]; |  | ||||||
|           b.size = newSize; |  | ||||||
|  |  | ||||||
|           setElementSize(a.element, a.size, pair.aGutterSize); |  | ||||||
|           setElementSize(b.element, b.size, pair.bGutterSize); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function destroy() { |  | ||||||
|       pairs.forEach(function(pair) { |  | ||||||
|         pair.parent.removeChild(pair.gutter); |  | ||||||
|         elements[pair.a].element.style[dimension] = ''; |  | ||||||
|         elements[pair.b].element.style[dimension] = ''; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (isIE8) { |  | ||||||
|       return { |  | ||||||
|         setSizes: setSizes, |  | ||||||
|         destroy: destroy |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       setSizes: setSizes, |  | ||||||
|       getSizes: function getSizes() { |  | ||||||
|         return elements.map(function(element) { |  | ||||||
|           return element.size; |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|       collapse: function collapse(i) { |  | ||||||
|         if (i === pairs.length) { |  | ||||||
|           var pair = pairs[i - 1]; |  | ||||||
|  |  | ||||||
|           calculateSizes.call(pair); |  | ||||||
|  |  | ||||||
|           if (!isIE8) { |  | ||||||
|             adjust.call(pair, pair.size - pair.bGutterSize); |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           var pair$1 = pairs[i]; |  | ||||||
|  |  | ||||||
|           calculateSizes.call(pair$1); |  | ||||||
|  |  | ||||||
|           if (!isIE8) { |  | ||||||
|             adjust.call(pair$1, pair$1.aGutterSize); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       destroy: destroy |  | ||||||
|     }; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return Split; |  | ||||||
| }); |  | ||||||
| @@ -1,140 +0,0 @@ | |||||||
| .documentation { |  | ||||||
|   font-family: Helvetica, sans-serif; |  | ||||||
|   color: #666; |  | ||||||
|   line-height: 1.5; |  | ||||||
|   background: #f5f5f5; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .black { |  | ||||||
|   color: #666; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .bg-white { |  | ||||||
|   background-color: #fff; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h4 { |  | ||||||
|   margin: 20px 0 10px 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .documentation h3 { |  | ||||||
|   color: #000; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .border-bottom { |  | ||||||
|   border-color: #ddd; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| a { |  | ||||||
|   color: #1184CE; |  | ||||||
|   text-decoration: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .documentation a[href]:hover { |  | ||||||
|   text-decoration: underline; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| a:hover { |  | ||||||
|   cursor: pointer; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .py1-ul li { |  | ||||||
|   padding: 5px 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .max-height-100 { |  | ||||||
|   max-height: 100%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .height-viewport-100 { |  | ||||||
|   height: 100vh; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| section:target h3 { |  | ||||||
|   font-weight:700; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .documentation td, |  | ||||||
| .documentation th { |  | ||||||
|     padding: .25rem .25rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h1:hover .anchorjs-link, |  | ||||||
| h2:hover .anchorjs-link, |  | ||||||
| h3:hover .anchorjs-link, |  | ||||||
| h4:hover .anchorjs-link { |  | ||||||
|   opacity: 1; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .fix-3 { |  | ||||||
|   width: 25%; |  | ||||||
|   max-width: 244px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .fix-3 { |  | ||||||
|   width: 25%; |  | ||||||
|   max-width: 244px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (min-width: 52em) { |  | ||||||
|   .fix-margin-3 { |  | ||||||
|     margin-left: 25%; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .pre, pre, code, .code { |  | ||||||
|   font-family: Source Code Pro,Menlo,Consolas,Liberation Mono,monospace; |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .fill-light { |  | ||||||
|   background: #F9F9F9; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .width2 { |  | ||||||
|   width: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .input { |  | ||||||
|   font-family: inherit; |  | ||||||
|   display: block; |  | ||||||
|   width: 100%; |  | ||||||
|   height: 2rem; |  | ||||||
|   padding: .5rem; |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
|   border: 1px solid #ccc; |  | ||||||
|   font-size: .875rem; |  | ||||||
|   border-radius: 3px; |  | ||||||
|   box-sizing: border-box; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table { |  | ||||||
|   border-collapse: collapse; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .prose table th, |  | ||||||
| .prose table td { |  | ||||||
|   text-align: left; |  | ||||||
|   padding:8px; |  | ||||||
|   border:1px solid #ddd; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .prose table th:nth-child(1) { border-right: none; } |  | ||||||
| .prose table th:nth-child(2) { border-left: none; } |  | ||||||
|  |  | ||||||
| .prose table { |  | ||||||
|   border:1px solid #ddd; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .prose-big { |  | ||||||
|   font-size: 18px; |  | ||||||
|   line-height: 30px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .quiet { |  | ||||||
|   opacity: 0.7; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .minishadow { |  | ||||||
|   box-shadow: 2px 2px 10px #f3f3f3; |  | ||||||
| } |  | ||||||
| @@ -1,770 +0,0 @@ | |||||||
| <!doctype html> |  | ||||||
| <html> |  | ||||||
| <head> |  | ||||||
|   <meta charset='utf-8' /> |  | ||||||
|   <title>seasoned-request 1.0.0 | Documentation</title> |  | ||||||
|   <meta name='description' content='seasoned request app'> |  | ||||||
|   <meta name='viewport' content='width=device-width,initial-scale=1'> |  | ||||||
|   <link href='assets/bass.css' rel='stylesheet' /> |  | ||||||
|   <link href='assets/style.css' rel='stylesheet' /> |  | ||||||
|   <link href='assets/github.css' rel='stylesheet' /> |  | ||||||
|   <link href='assets/split.css' rel='stylesheet' /> |  | ||||||
| </head> |  | ||||||
| <body class='documentation m0'> |  | ||||||
|     <div class='flex'> |  | ||||||
|       <div id='split-left' class='overflow-auto fs0 height-viewport-100'> |  | ||||||
|         <div class='py1 px2'> |  | ||||||
|           <h3 class='mb0 no-anchor'>seasoned-request</h3> |  | ||||||
|           <div class='mb1'><code>1.0.0</code></div> |  | ||||||
|           <input |  | ||||||
|             placeholder='Filter' |  | ||||||
|             id='filter-input' |  | ||||||
|             class='col12 block input' |  | ||||||
|             type='text' /> |  | ||||||
|           <div id='toc'> |  | ||||||
|             <ul class='list-reset h5 py1-ul'> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#getmovie' |  | ||||||
|                   class=""> |  | ||||||
|                   getMovie |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#getshow' |  | ||||||
|                   class=""> |  | ||||||
|                   getShow |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#gettmdblistbypath' |  | ||||||
|                   class=""> |  | ||||||
|                   getTmdbListByPath |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#searchtmdb' |  | ||||||
|                   class=""> |  | ||||||
|                   searchTmdb |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#searchtorrents' |  | ||||||
|                   class=""> |  | ||||||
|                   searchTorrents |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#addmagnet' |  | ||||||
|                   class=""> |  | ||||||
|                   addMagnet |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#request' |  | ||||||
|                   class=""> |  | ||||||
|                   request |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|                  |  | ||||||
|                 <li><a |  | ||||||
|                   href='#elasticsearchmoviesandshows' |  | ||||||
|                   class=""> |  | ||||||
|                   elasticSearchMoviesAndShows |  | ||||||
|                    |  | ||||||
|                 </a> |  | ||||||
|                  |  | ||||||
|                 </li> |  | ||||||
|                |  | ||||||
|             </ul> |  | ||||||
|           </div> |  | ||||||
|           <div class='mt1 h6 quiet'> |  | ||||||
|             <a href='https://documentation.js.org/reading-documentation.html'>Need help reading this?</a> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <div id='split-right' class='relative overflow-auto height-viewport-100'> |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='getmovie'> |  | ||||||
|       getMovie |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Fetches tmdb movie by id. Can optionally include cast credits in result object.</p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>getMovie</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>credits</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a> |  | ||||||
|             = <code>false</code>)</code> |  | ||||||
| 	    Include credits |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         Tmdb response |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='getshow'> |  | ||||||
|       getShow |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Fetches tmdb show by id. Can optionally include cast credits in result object.</p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>getShow</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>credits</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a> |  | ||||||
|             = <code>false</code>)</code> |  | ||||||
| 	    Include credits |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         Tmdb response |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='gettmdblistbypath'> |  | ||||||
|       getTmdbListByPath |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Fetches tmdb list by path.</p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>getTmdbListByPath</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>listPath</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code> |  | ||||||
| 	    Path of list |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>page</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a> |  | ||||||
|             = <code>1</code>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         Tmdb list response |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='searchtmdb'> |  | ||||||
|       searchTmdb |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Fetches tmdb movies and shows by query.</p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>searchTmdb</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>query</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>page</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a> |  | ||||||
|             = <code>1</code>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         Tmdb response |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='searchtorrents'> |  | ||||||
|       searchTorrents |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Search for torrents by query</p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>searchTorrents</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>query</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>authorization_token</span> <code class='quiet'>(any)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>credits</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>)</code> |  | ||||||
| 	    Include credits |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         Torrent response |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='addmagnet'> |  | ||||||
|       addMagnet |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Add magnet to download queue.</p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>addMagnet</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>magnet</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code> |  | ||||||
| 	    Magnet link |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>name</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>)</code> |  | ||||||
| 	    Name of torrent |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>tmdb_id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         Success/Failure response |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='request'> |  | ||||||
|       request |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Request a movie or show from id. If authorization token is included the user will be linked |  | ||||||
| to the requested item.</p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>request</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>)</code> |  | ||||||
| 	    Movie or show id |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>type</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code> |  | ||||||
| 	    Movie or show type |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>authorization_token</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>? |  | ||||||
|             = <code>undefined</code>)</code> |  | ||||||
| 	    To identify the requesting user |  | ||||||
|  |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         Success/Failure response |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|            |  | ||||||
|           <section class='p2 mb2 clearfix bg-white minishadow'> |  | ||||||
|  |  | ||||||
|    |  | ||||||
|   <div class='clearfix'> |  | ||||||
|      |  | ||||||
|     <h3 class='fl m0' id='elasticsearchmoviesandshows'> |  | ||||||
|       elasticSearchMoviesAndShows |  | ||||||
|     </h3> |  | ||||||
|      |  | ||||||
|      |  | ||||||
|   </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|   <p>Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and |  | ||||||
| Tv Shows. See tmdb docs for more info: <a href="https://developers.themoviedb.org/3/getting-started/daily-file-exports">https://developers.themoviedb.org/3/getting-started/daily-file-exports</a></p> |  | ||||||
|  |  | ||||||
|     <div class='pre p1 fill-light mt0'>elasticSearchMoviesAndShows</div> |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|     <div class='py1 quiet mt1 prose-big'>Parameters</div> |  | ||||||
|     <div class='prose'> |  | ||||||
|        |  | ||||||
|         <div class='space-bottom0'> |  | ||||||
|           <div> |  | ||||||
|             <span class='code bold'>query</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code> |  | ||||||
| 	     |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|         </div> |  | ||||||
|        |  | ||||||
|     </div> |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|      |  | ||||||
|       <div class='py1 quiet mt1 prose-big'>Returns</div> |  | ||||||
|       <code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>: |  | ||||||
|         List of movies and shows matching query |  | ||||||
|  |  | ||||||
|        |  | ||||||
|      |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
|  |  | ||||||
|    |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
|            |  | ||||||
|          |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   <script src='assets/anchor.js'></script> |  | ||||||
|   <script src='assets/split.js'></script> |  | ||||||
|   <script src='assets/site.js'></script> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
							
								
								
									
										129
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										30
									
								
								nginx.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | |||||||
|  | server { | ||||||
|  |   listen 5000 default_server; | ||||||
|  |   listen [::]:5000 default_server; | ||||||
|  |  | ||||||
|  |   server_name $SEASONED_DOMAIN; | ||||||
|  |   root /usr/share/nginx/html; | ||||||
|  |  | ||||||
|  |   gzip on; | ||||||
|  |   gzip_types application/javascript; | ||||||
|  |   gzip_min_length 1000; | ||||||
|  |   gzip_static on; | ||||||
|  |  | ||||||
|  |   location /favicons { | ||||||
|  |     autoindex on; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   location /dist { | ||||||
|  |     add_header Content-Type application/javascript; | ||||||
|  |     try_files $uri =404; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   location /api { | ||||||
|  |     proxy_pass $SEASONED_API; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   location / { | ||||||
|  |     try_files $uri $uri/ /index.html; | ||||||
|  |     index index.html; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										79
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,42 +1,61 @@ | |||||||
| { | { | ||||||
|   "name": "seasoned-request", |   "name": "seasoned-request", | ||||||
|   "description": "seasoned request app", |   "description": "seasoned request app", | ||||||
|   "version": "1.0.0", |   "version": "1.22.17", | ||||||
|   "author": "Kevin Midboe", |   "author": "Kevin Midboe", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "cross-env NODE_ENV=development webpack-dev-server --hot", |     "dev": "NODE_ENV=development webpack server", | ||||||
|     "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", |     "build": "yarn build:ts && yarn build:webpack", | ||||||
|     "start": "node server.js", |     "build:ts": "tsc --project tsconfig.json", | ||||||
|     "docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md" |     "build:webpack": "NODE_ENV=production webpack-cli build --progress", | ||||||
|  |     "postbuild": "cp public/dist/index.html public/index.html", | ||||||
|  |     "clean": "rm -r public/dist 2> /dev/null; rm public/index.html 2> /dev/null; rm -r lib 2> /dev/null", | ||||||
|  |     "start": "echo 'Start using docker, consult README'", | ||||||
|  |     "lint": "eslint src --ext .ts,.vue", | ||||||
|  |     "docs": "documentation build src/api.ts -f html -o docs/api && documentation build src/api.ts -f md -o docs/api.md" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "axios": "^0.15.3", |     "chart.js": "3.9.1", | ||||||
|     "babel-plugin-transform-object-rest-spread": "^6.26.0", |     "connect-history-api-fallback": "2.0.0", | ||||||
|     "connect-history-api-fallback": "^1.3.0", |     "dotenv": "^16.0.1", | ||||||
|     "express": "^4.16.1", |     "express": "4.18.1", | ||||||
|     "vue": "^2.5.2", |     "vue": "3.2.37", | ||||||
|     "vue-axios": "^1.2.2", |     "vue-router": "4.1.3", | ||||||
|     "vue-data-tablee": "^0.12.1", |     "vuex": "4.0.2" | ||||||
|     "vue-js-modal": "^1.3.16", |  | ||||||
|     "vue-router": "^3.0.1", |  | ||||||
|     "vuex": "^3.1.0" |  | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/core": "^7.4.5", |     "@babel/core": "7.18.10", | ||||||
|     "@babel/plugin-transform-runtime": "^7.4.4", |     "@babel/plugin-transform-runtime": "7.18.10", | ||||||
|     "@babel/preset-env": "^7.4.5", |     "@babel/preset-env": "7.18.10", | ||||||
|     "@babel/runtime": "^7.4.5", |     "@babel/runtime": "7.18.9", | ||||||
|     "babel-loader": "^8.0.6", |     "@types/express": "4.17.13", | ||||||
|     "cross-env": "^3.0.0", |     "@types/node": "18.6.1", | ||||||
|     "css-loader": "^0.25.0", |     "@typescript-eslint/eslint-plugin": "5.33.0", | ||||||
|     "documentation": "^11.0.0", |     "@typescript-eslint/parser": "5.33.0", | ||||||
|     "file-loader": "^0.9.0", |     "@vue/cli": "5.0.8", | ||||||
|     "node-sass": "^4.5.0", |     "@vue/cli-service": "5.0.8", | ||||||
|     "sass-loader": "^5.0.1", |     "@vue/eslint-config-airbnb": "6.0.0", | ||||||
|     "vue-loader": "^10.0.0", |     "babel-loader": "8.2.5", | ||||||
|     "vue-template-compiler": "2.6.10", |     "css-loader": "6.7.1", | ||||||
|     "webpack": "^2.2.0", |     "documentation": "13.2.5", | ||||||
|     "webpack-dev-server": "^2.2.0" |     "eslint": "8.21.0", | ||||||
|  |     "eslint-config-prettier": "8.5.0", | ||||||
|  |     "eslint-plugin-import": "2.26.0", | ||||||
|  |     "eslint-plugin-prettier": "4.2.1", | ||||||
|  |     "eslint-plugin-vue": "9.3.0", | ||||||
|  |     "eslint-plugin-vuejs-accessibility": "1.2.0", | ||||||
|  |     "file-loader": "6.2.0", | ||||||
|  |     "html-webpack-plugin": "5.5.0", | ||||||
|  |     "prettier": "2.7.1", | ||||||
|  |     "sass": "1.54.3", | ||||||
|  |     "sass-loader": "13.0.2", | ||||||
|  |     "terser-webpack-plugin": "5.3.3", | ||||||
|  |     "ts-loader": "9.3.1", | ||||||
|  |     "typescript": "4.7.4", | ||||||
|  |     "vue-loader": "17.0.0", | ||||||
|  |     "webpack": "5.74.0", | ||||||
|  |     "webpack-cli": "4.10.0", | ||||||
|  |     "webpack-dev-server": "4.9.3" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB | 
| Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/assets/dune.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 641 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/assets/mandalorian.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 331 KiB | 
							
								
								
									
										13
									
								
								public/assets/no-image.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.3 KiB | 
							
								
								
									
										13
									
								
								public/assets/no-image_small.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.3 KiB | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 275 KiB | 
| Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 423 KiB | 
| Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB | 
| Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 889 B | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										23
									
								
								server.js
									
									
									
									
									
								
							
							
						
						| @@ -1,23 +0,0 @@ | |||||||
| var express = require('express'); |  | ||||||
| var path = require('path'); |  | ||||||
| const compression = require('compression') |  | ||||||
| var history = require('connect-history-api-fallback'); |  | ||||||
|  |  | ||||||
| app = express(); |  | ||||||
|  |  | ||||||
| app.use(compression()) |  | ||||||
| app.use('/dist', express.static(path.join(__dirname + "/dist"))); |  | ||||||
| app.use('/dist', express.static(path.join(__dirname + "/dist/"))); |  | ||||||
| app.use('/favicons', express.static(path.join(__dirname + "/favicons"))); |  | ||||||
| app.use(history({ |  | ||||||
|     index: '/' |  | ||||||
| })); |  | ||||||
|  |  | ||||||
| var port = process.env.PORT || 5000; |  | ||||||
|  |  | ||||||
| app.get('/', function(req, res) { |  | ||||||
|     res.sendFile(path.join(__dirname + '/index.html')); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app.listen(port); |  | ||||||
							
								
								
									
										216
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						| @@ -1,194 +1,70 @@ | |||||||
| <template> | <template> | ||||||
|   <div id="app"> |   <div id="app"> | ||||||
|  |  | ||||||
|     <!-- Header and hamburger navigation --> |     <!-- Header and hamburger navigation --> | ||||||
|     <navigation></navigation> |     <NavigationHeader class="header" /> | ||||||
|  |  | ||||||
|     <!-- Header with search field --> |     <div class="navigation-icons-gutter desktop-only"> | ||||||
|     <header class="header"> |       <NavigationIcons /> | ||||||
|       <search-input v-model="query"></search-input> |     </div> | ||||||
|     </header> |  | ||||||
|  |  | ||||||
|     <!-- Movie popup that will show above existing rendered content --> |  | ||||||
|     <movie-popup v-if="moviePopupIsVisible" :id="popupID" :type="popupType"></movie-popup> |  | ||||||
|  |  | ||||||
|     <!-- Display the component assigned to the given route (default: home) --> |     <!-- Display the component assigned to the given route (default: home) --> | ||||||
|     <router-view class="content"></router-view> |     <router-view :key="router.currentRoute.value.path" class="content" /> | ||||||
|  |  | ||||||
|  |     <!-- Popup that will show above existing rendered content --> | ||||||
|  |     <popup /> | ||||||
|  |  | ||||||
|  |     <darkmode-toggle /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import Vue from 'vue' |   import { useRouter } from "vue-router"; | ||||||
| import Navigation from '@/components/Navigation.vue' |   import NavigationHeader from "@/components/header/NavigationHeader.vue"; | ||||||
| import MoviePopup from '@/components/MoviePopup.vue' |   import NavigationIcons from "@/components/header/NavigationIcons.vue"; | ||||||
| import SearchInput from '@/components/SearchInput.vue' |   import Popup from "@/components/Popup.vue"; | ||||||
|  |   import DarkmodeToggle from "@/components/ui/DarkmodeToggle.vue"; | ||||||
|  |  | ||||||
| export default { |   const router = useRouter(); | ||||||
|   name: 'app', |  | ||||||
|   components: { |  | ||||||
|     Navigation, |  | ||||||
|     MoviePopup, |  | ||||||
|     SearchInput |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       query: '', |  | ||||||
|       moviePopupIsVisible: false, |  | ||||||
|       popupID: 0, |  | ||||||
|       popupType: 'movie' |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     let that = this |  | ||||||
|     Vue.prototype.$popup = { |  | ||||||
|       get isOpen() { |  | ||||||
|         return that.moviePopupIsVisible |  | ||||||
|       }, |  | ||||||
|       open: (id, type) => { |  | ||||||
|         this.popupID = id || this.popupID |  | ||||||
|         this.popupType = type || this.popupType |  | ||||||
|         this.moviePopupIsVisible = true |  | ||||||
|         console.log('opened') |  | ||||||
|       }, |  | ||||||
|       close: () => { |  | ||||||
|         this.moviePopupIsVisible = false |  | ||||||
|         console.log('closed') |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     console.log('MoviePopup registered at this.$popup and has state: ', this.$popup.isOpen) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| .content { |  | ||||||
|     @include tablet-min{ |  | ||||||
|     width: calc(100% - 95px); |  | ||||||
|     padding-top: $header-size; |  | ||||||
|     margin-left: 95px; |  | ||||||
|     position: relative; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
| @import "./src/scss/main"; |   @import "src/scss/main"; | ||||||
| @import "./src/scss/variables"; |   @import "src/scss/media-queries"; | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| *{ |  | ||||||
|   box-sizing: border-box; |  | ||||||
| } |  | ||||||
| html, body{ |  | ||||||
|   height: 100%; |  | ||||||
| } |  | ||||||
| body{ |  | ||||||
|   font-family: 'Roboto', sans-serif; |  | ||||||
|   line-height: 1.6; |  | ||||||
|   background: $c-light; |  | ||||||
|   color: $c-dark; |  | ||||||
|   &.hidden{ |  | ||||||
|     overflow: hidden; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| input, textarea, button{ |  | ||||||
|   font-family: 'Roboto', sans-serif; |  | ||||||
| } |  | ||||||
| figure{ |  | ||||||
|   padding: 0; |  | ||||||
|   margin: 0; |  | ||||||
| } |  | ||||||
| img{ |  | ||||||
|   display: block; |  | ||||||
|   // max-width: 100%; |  | ||||||
|   height: auto; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .wrapper{ |   #app { | ||||||
|   position: relative; |     display: grid; | ||||||
| } |     grid-template-rows: var(--header-size); | ||||||
| .header{ |     grid-template-columns: var(--header-size) 1fr; | ||||||
|   position: fixed; |  | ||||||
|   background: $c-white; |  | ||||||
|   z-index: 15; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|  |  | ||||||
|   @include tablet-min{ |     @include mobile { | ||||||
|     width: calc(100% - 170px); |       grid-template-columns: 1fr; | ||||||
|     margin-left: 95px; |  | ||||||
|     border-top: 0; |  | ||||||
|     border-bottom: 0; |  | ||||||
|     top: 0; |  | ||||||
|   } |  | ||||||
|   &__search{ |  | ||||||
|     display: flex; |  | ||||||
|     position: relative; |  | ||||||
|     z-index: 5; |  | ||||||
|     width: 100%; |  | ||||||
|     position: fixed; |  | ||||||
|     top: 0; |  | ||||||
|     right: 55px; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       position: relative; |  | ||||||
|       height: 75px; |  | ||||||
|       right: 0; |  | ||||||
|     } |     } | ||||||
|     &-input{ |  | ||||||
|       display: block; |     .header { | ||||||
|  |       position: fixed; | ||||||
|  |       top: 0; | ||||||
|       width: 100%; |       width: 100%; | ||||||
|       padding: 15px 20px 15px 45px; |       z-index: 15; | ||||||
|       outline: none; |  | ||||||
|       border: 0; |  | ||||||
|       background-color: transparent; |  | ||||||
|       color: $c-dark; |  | ||||||
|       font-weight: 300; |  | ||||||
|       font-size: 16px; |  | ||||||
|       @include tablet-min{ |  | ||||||
|         padding: 15px 30px 15px 60px; |  | ||||||
|       } |  | ||||||
|       @include tablet-landscape-min{ |  | ||||||
|         padding: 15px 30px 15px 80px; |  | ||||||
|       } |  | ||||||
|       @include desktop-min{ |  | ||||||
|         padding: 15px 30px 15px 90px; |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|     &-arrow { |  | ||||||
|       height: 19px; |  | ||||||
|       width: 30px; |  | ||||||
|       display: flex; |  | ||||||
|       align-self: center; |  | ||||||
|       margin-right: 30px; |  | ||||||
|  |  | ||||||
|       -moz-transition: all 0.5s ease; |     .navigation-icons-gutter { | ||||||
|       -webkit-transition: all 0.5s ease; |       position: fixed; | ||||||
|       transition: all 0.5s ease; |       height: 100vh; | ||||||
|  |       margin: 0; | ||||||
|       &.down { |       top: var(--header-size); | ||||||
|         -ms-transform: rotate(180deg); |       width: var(--header-size); | ||||||
|         -moz-transform: rotate(180deg); |       background-color: var(--background-color-secondary); | ||||||
|         -webkit-transform: rotate(180deg); |  | ||||||
|         transform: rotate(180deg); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|     &-input:focus + &-icon{ |  | ||||||
|       fill: $c-dark; |     .content { | ||||||
|  |       display: grid; | ||||||
|  |       grid-column: 2 / 3; | ||||||
|  |       grid-row: 2; | ||||||
|  |       z-index: 5; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         grid-column: 1 / 3; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| // router view transition |  | ||||||
| .fade-enter-active, .fade-leave-active { |  | ||||||
|   transition-property: opacity; |  | ||||||
|   transition-duration: 0.25s; |  | ||||||
| } |  | ||||||
| .fade-enter-active { |  | ||||||
|   transition-delay: 0.25s; |  | ||||||
| } |  | ||||||
| .fade-enter, .fade-leave-active { |  | ||||||
|   opacity: 0 |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										255
									
								
								src/api.js
									
									
									
									
									
								
							
							
						
						| @@ -1,255 +0,0 @@ | |||||||
| import axios from 'axios' |  | ||||||
| import storage from '@/storage.js' |  | ||||||
| import config from '@/config.json' |  | ||||||
| import path from 'path' |  | ||||||
|  |  | ||||||
| const SEASONED_URL = config.SEASONED_URL |  | ||||||
| const ELASTIC_URL = config.ELASTIC_URL |  | ||||||
| const ELASTIC_INDEX = config.ELASTIC_INDEX |  | ||||||
|  |  | ||||||
| // TODO |  | ||||||
| //  - Move autorization token and errors here? |  | ||||||
|  |  | ||||||
| // - - - TMDB - - -  |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Fetches tmdb movie by id. Can optionally include cast credits in result object. |  | ||||||
|  * @param {number} id |  | ||||||
|  * @param {boolean} [credits=false] Include credits |  | ||||||
|  * @returns {object} Tmdb response |  | ||||||
|  */ |  | ||||||
| const getMovie = (id, credits=false) => { |  | ||||||
|   const url = new URL('v2/movie', SEASONED_URL) |  | ||||||
|   url.pathname = path.join(url.pathname, id.toString()) |  | ||||||
|   if (credits) { |  | ||||||
|     url.searchParams.append('credits', true) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return axios.get(url.href) |  | ||||||
|     .catch(error => { console.error(`api error getting movie: ${id}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Fetches tmdb show by id. Can optionally include cast credits in result object. |  | ||||||
|  * @param {number} id |  | ||||||
|  * @param {boolean} [credits=false] Include credits |  | ||||||
|  * @returns {object} Tmdb response |  | ||||||
|  */ |  | ||||||
| const getShow = (id, credits=false) => { |  | ||||||
|   const url = new URL('v2/show', SEASONED_URL) |  | ||||||
|   url.pathname = path.join(url.pathname, id.toString()) |  | ||||||
|   if (credits) { |  | ||||||
|     url.searchParams.append('credits', true) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return axios.get(url.href) |  | ||||||
|     .catch(error => { console.error(`api error getting show: ${id}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Fetches tmdb list by path. |  | ||||||
|  * @param {string} listPath Path of list |  | ||||||
|  * @param {number} [page=1] |  | ||||||
|  * @returns {object} Tmdb list response |  | ||||||
|  */ |  | ||||||
| const getTmdbListByPath = (listPath, page=1) => { |  | ||||||
|   const url = new URL(listPath, SEASONED_URL) |  | ||||||
|   url.searchParams.append('page', page) |  | ||||||
|   // TODO - remove. this is temporary fix for user-requests endpoint (also import) |  | ||||||
|   const headers = { authorization: storage.token } |  | ||||||
|  |  | ||||||
|   return axios.get(url.href, { headers: headers }) |  | ||||||
|     .catch(error => { console.error(`api error getting list: ${listPath}, page: ${page}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Fetches tmdb movies and shows by query. |  | ||||||
|  * @param {string} query |  | ||||||
|  * @param {number} [page=1] |  | ||||||
|  * @returns {object} Tmdb response |  | ||||||
|  */ |  | ||||||
| const searchTmdb = (query, page=1) => { |  | ||||||
|   const url = new URL('v2/search', SEASONED_URL) |  | ||||||
|   url.searchParams.append('query', query) |  | ||||||
|   url.searchParams.append('page', page) |  | ||||||
|  |  | ||||||
|   return axios.get(url.href) |  | ||||||
|     .catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // - - - Torrents - - -  |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Search for torrents by query |  | ||||||
|  * @param {string} query |  | ||||||
|  * @param {boolean} credits Include credits |  | ||||||
|  * @returns {object} Torrent response |  | ||||||
|  */ |  | ||||||
| const searchTorrents = (query, authorization_token) => { |  | ||||||
|   const url = new URL('v1/pirate/search', SEASONED_URL) |  | ||||||
|   url.searchParams.append('query', query) |  | ||||||
|  |  | ||||||
|   const headers = { authorization: storage.token } |  | ||||||
|  |  | ||||||
|   return axios.get(url.href, { headers: headers }) |  | ||||||
|     .catch(error => { console.error(`api error searching torrents: ${query}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Add magnet to download queue. |  | ||||||
|  * @param {string} magnet Magnet link |  | ||||||
|  * @param {boolean} name Name of torrent |  | ||||||
|  * @param {boolean} tmdb_id |  | ||||||
|  * @returns {object} Success/Failure response |  | ||||||
|  */ |  | ||||||
| const addMagnet = (magnet, name, tmdb_id) => { |  | ||||||
|   const url = new URL('v1/pirate/add', SEASONED_URL) |  | ||||||
|  |  | ||||||
|   const body = { |  | ||||||
|     magnet: magnet, |  | ||||||
|     name: name, |  | ||||||
|     tmdb_id: tmdb_id |  | ||||||
|   } |  | ||||||
|   const headers = { authorization: storage.token } |  | ||||||
|  |  | ||||||
|   return axios.post(url.href, body, { headers: headers }) |  | ||||||
|     .catch(error => { console.error(`api error adding magnet: ${name}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // - - - Plex/Request - - -  |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Request a movie or show from id. If authorization token is included the user will be linked |  | ||||||
|  * to the requested item. |  | ||||||
|  * @param {number} id Movie or show id |  | ||||||
|  * @param {string} type Movie or show type |  | ||||||
|  * @param {string} [authorization_token] To identify the requesting user |  | ||||||
|  * @returns {object} Success/Failure response |  | ||||||
|  */ |  | ||||||
| const request = (id, type, authorization_token=undefined) => { |  | ||||||
|   const url = new URL('v2/request', SEASONED_URL) |  | ||||||
| //  url.pathname = path.join(url.pathname, id.toString()) |  | ||||||
| //  url.searchParams.append('type', type) |  | ||||||
|  |  | ||||||
|   const headers = { |  | ||||||
|     'Authorization': authorization_token, |  | ||||||
|     'Content-Type': 'application/json' |  | ||||||
|   } |  | ||||||
|   const body = { |  | ||||||
|     id: id, |  | ||||||
|     type: type |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { |  | ||||||
|     method: 'POST', |  | ||||||
|     headers: headers, |  | ||||||
|     body: JSON.stringify(body) |  | ||||||
|   }) |  | ||||||
|   .then(resp => resp.json()) |  | ||||||
|   .catch(error => { console.error(`api error requesting: ${id}, type: ${type}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Check request status by tmdb id and type |  | ||||||
|  * @param {number} tmdb id |  | ||||||
|  * @param {string} type |  | ||||||
|  * @returns {object} Success/Failure response |  | ||||||
|  */ |  | ||||||
| const getRequestStatus = (id, type, authorization_token=undefined) => { |  | ||||||
|   const url = new URL('v2/request', SEASONED_URL) |  | ||||||
|   url.pathname = path.join(url.pathname, id.toString()) |  | ||||||
|   url.searchParams.append('type', type) |  | ||||||
|  |  | ||||||
|   return fetch(url.href) |  | ||||||
|     .then(resp => { |  | ||||||
|       const status = resp.status; |  | ||||||
|       if (status === 200) { return true } |  | ||||||
|       else if (status === 404) { return false } |  | ||||||
|       else { |  | ||||||
|         console.error(`api error getting request status for id ${id} and type ${type}`) |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|     .catch(err => Promise.reject(err)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // - - - Authenticate with plex - - - |  | ||||||
|  |  | ||||||
| const plexAuthenticate = (username, password) => { |  | ||||||
|   const url = new URL('https://plex.tv/users/sign_in.json') |  | ||||||
|   url.searchParams.append('user[login]', username) |  | ||||||
|   url.searchParams.append('user[password]', password) |  | ||||||
|  |  | ||||||
|   const headers = { |  | ||||||
|     'Content-Type': 'application/json', |  | ||||||
|     'X-Plex-Platform': 'Linux', |  | ||||||
|     'X-Plex-Version': 'v2.0.24', |  | ||||||
|     'X-Plex-Platform-Version': '4.13.0-36-generic', |  | ||||||
|     'X-Plex-Device-Name': 'Tautulli', |  | ||||||
|     'X-Plex-Client-Identifier': '123' |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return axios.post(url.href, { headers: headers }) |  | ||||||
|     .catch(error => { console.error(`api error authentication plex: ${username}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // - - - Random emoji - - - |  | ||||||
|  |  | ||||||
| const getEmoji = () => { |  | ||||||
|   const url = path.join(SEASONED_URL, 'v1/emoji') |  | ||||||
|  |  | ||||||
|   return axios.get(url) |  | ||||||
|     .catch(error => { console.log('api error getting emoji'); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // - - - ELASTIC SEARCH - - - |  | ||||||
| // This elastic index contains titles mapped to ids. Lightning search |  | ||||||
| // used for autocomplete |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and |  | ||||||
|  * Tv Shows. See tmdb docs for more info: https://developers.themoviedb.org/3/getting-started/daily-file-exports |  | ||||||
|  * @param {string} query |  | ||||||
|  * @returns {object} List of movies and shows matching query |  | ||||||
|  */ |  | ||||||
| const elasticSearchMoviesAndShows = (query) => { |  | ||||||
|   const url = new URL(path.join(ELASTIC_INDEX, '/_search'), ELASTIC_URL) |  | ||||||
|   const headers = { |  | ||||||
|     'Content-Type': 'application/json' |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const body = { |  | ||||||
|     "sort" : [ |  | ||||||
|       { "popularity" : {"order" : "desc"}}, |  | ||||||
|       "_score" |  | ||||||
|     ], |  | ||||||
|     "query": { |  | ||||||
|       "bool": { |  | ||||||
|         "should": [{ |  | ||||||
|           "match_phrase_prefix": { |  | ||||||
|             "original_name": query |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "match_phrase_prefix": { |  | ||||||
|             "original_title": query |  | ||||||
|           } |  | ||||||
|         }] |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "size": 6 |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { |  | ||||||
|     method: 'POST', |  | ||||||
|     headers: headers, |  | ||||||
|     body: JSON.stringify(body) |  | ||||||
|   }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .catch(error => { console.log(`api error searching elasticsearch: ${query}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| export { getMovie, getShow, getTmdbListByPath, searchTmdb, searchTorrents, addMagnet, request, getRequestStatus, plexAuthenticate, getEmoji, elasticSearchMoviesAndShows } |  | ||||||
							
								
								
									
										536
									
								
								src/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,536 @@ | |||||||
|  | import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList"; | ||||||
|  | import type { | ||||||
|  |   IRequestStatusResponse, | ||||||
|  |   IRequestSubmitResponse | ||||||
|  | } from "./interfaces/IRequestResponse"; | ||||||
|  |  | ||||||
|  | const { ELASTIC, ELASTIC_INDEX } = process.env; | ||||||
|  | const API_HOSTNAME = window.location.origin; | ||||||
|  |  | ||||||
|  | // - - - TMDB - - - | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb movie by id. Can optionally include cast credits in result object. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getMovie = ( | ||||||
|  |   id, | ||||||
|  |   { | ||||||
|  |     checkExistance, | ||||||
|  |     credits, | ||||||
|  |     releaseDates | ||||||
|  |   }: { checkExistance: boolean; credits: boolean; releaseDates?: boolean } | ||||||
|  | ) => { | ||||||
|  |   const url = new URL("/api/v2/movie", API_HOSTNAME); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|  |   if (checkExistance) { | ||||||
|  |     url.searchParams.append("check_existance", "true"); | ||||||
|  |   } | ||||||
|  |   if (credits) { | ||||||
|  |     url.searchParams.append("credits", "true"); | ||||||
|  |   } | ||||||
|  |   if (releaseDates) { | ||||||
|  |     url.searchParams.append("release_dates", "true"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting movie: ${id}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb show by id. Can optionally include cast credits in result object. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @param {boolean} [credits=false] Include credits | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getShow = ( | ||||||
|  |   id, | ||||||
|  |   { | ||||||
|  |     checkExistance, | ||||||
|  |     credits, | ||||||
|  |     releaseDates | ||||||
|  |   }: { checkExistance: boolean; credits: boolean; releaseDates?: boolean } | ||||||
|  | ) => { | ||||||
|  |   const url = new URL("/api/v2/show", API_HOSTNAME); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|  |   if (checkExistance) { | ||||||
|  |     url.searchParams.append("check_existance", "true"); | ||||||
|  |   } | ||||||
|  |   if (credits) { | ||||||
|  |     url.searchParams.append("credits", "true"); | ||||||
|  |   } | ||||||
|  |   if (releaseDates) { | ||||||
|  |     url.searchParams.append("release_dates", "true"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting show: ${id}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb person by id. Can optionally include cast credits in result object. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @param {boolean} [credits=false] Include credits | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getPerson = (id, credits = false) => { | ||||||
|  |   const url = new URL("/api/v2/person", API_HOSTNAME); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|  |   if (credits) { | ||||||
|  |     url.searchParams.append("credits", "true"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting person: ${id}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb movie credits by id. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getMovieCredits = (id: number): Promise<IMediaCredits> => { | ||||||
|  |   const url = new URL("/api/v2/movie", API_HOSTNAME); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting movie: ${id}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb show credits by id. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getShowCredits = (id: number): Promise<IMediaCredits> => { | ||||||
|  |   const url = new URL("/api/v2/show", API_HOSTNAME); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting show: ${id}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb person credits by id. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getPersonCredits = (id: number): Promise<IPersonCredits> => { | ||||||
|  |   const url = new URL("/api/v2/person", API_HOSTNAME); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting person: ${id}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb list by name. | ||||||
|  |  * @param {string} name List the fetch | ||||||
|  |  * @param {number} [page=1] | ||||||
|  |  * @returns {object} Tmdb list response | ||||||
|  |  */ | ||||||
|  | const getTmdbMovieListByName = (name: string, page = 1): Promise<IList> => { | ||||||
|  |   const url = new URL(`/api/v2/movie/${name}`, API_HOSTNAME); | ||||||
|  |   url.searchParams.append("page", page.toString()); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(resp => resp.json()); | ||||||
|  |   // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches requested items. | ||||||
|  |  * @param {number} [page=1] | ||||||
|  |  * @returns {object} Request response | ||||||
|  |  */ | ||||||
|  | const getRequests = (page = 1) => { | ||||||
|  |   const url = new URL("/api/v2/request", API_HOSTNAME); | ||||||
|  |   url.searchParams.append("page", page.toString()); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(resp => resp.json()); | ||||||
|  |   // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getUserRequests = (page = 1) => { | ||||||
|  |   const url = new URL("/api/v1/user/requests", API_HOSTNAME); | ||||||
|  |   url.searchParams.append("page", page.toString()); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(resp => resp.json()); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb movies and shows by query. | ||||||
|  |  * @param {string} query | ||||||
|  |  * @param {number} [page=1] | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const searchTmdb = (query, page = 1, adult = false, mediaType = null) => { | ||||||
|  |   const url = new URL("/api/v2/search", API_HOSTNAME); | ||||||
|  |   if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) { | ||||||
|  |     url.pathname += `/${mediaType}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   url.searchParams.append("query", query); | ||||||
|  |   url.searchParams.append("page", page.toString()); | ||||||
|  |   url.searchParams.append("adult", adult.toString()); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error searching: ${query}, page: ${page}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Torrents - - - | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Search for torrents by query | ||||||
|  |  * @param {string} query | ||||||
|  |  * @param {boolean} credits Include credits | ||||||
|  |  * @returns {object} Torrent response | ||||||
|  |  */ | ||||||
|  | const searchTorrents = query => { | ||||||
|  |   const url = new URL("/api/v1/pirate/search", API_HOSTNAME); | ||||||
|  |   url.searchParams.append("query", query); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Add magnet to download queue. | ||||||
|  |  * @param {string} magnet Magnet link | ||||||
|  |  * @param {boolean} name Name of torrent | ||||||
|  |  * @param {boolean} tmdbId | ||||||
|  |  * @returns {object} Success/Failure response | ||||||
|  |  */ | ||||||
|  | const addMagnet = (magnet: string, name: string, tmdbId: number | null) => { | ||||||
|  |   const url = new URL("/api/v1/pirate/add", API_HOSTNAME); | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       magnet, | ||||||
|  |       name, | ||||||
|  |       tmdb_id: tmdbId | ||||||
|  |     }) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error adding magnet: ${name} ${error}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Plex/Request - - - | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Request a movie or show from id. If authorization token is included the user will be linked | ||||||
|  |  * to the requested item. | ||||||
|  |  * @param {number} id Movie or show id | ||||||
|  |  * @param {string} type Movie or show type | ||||||
|  |  * @returns {object} Success/Failure response | ||||||
|  |  */ | ||||||
|  | const request = (id, type): Promise<IRequestSubmitResponse> => { | ||||||
|  |   const url = new URL("/api/v2/request", API_HOSTNAME); | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ id, type }) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error requesting: ${id}, type: ${type}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Check request status by tmdb id and type | ||||||
|  |  * @param {number} tmdb id | ||||||
|  |  * @param {string} type | ||||||
|  |  * @returns {object} Success/Failure response | ||||||
|  |  */ | ||||||
|  | const getRequestStatus = (id, type = null): Promise<IRequestStatusResponse> => { | ||||||
|  |   const url = new URL("/api/v2/request", API_HOSTNAME); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|  |   url.searchParams.append("type", type); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(err => Promise.reject(err)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const watchLink = (title, year) => { | ||||||
|  |   const url = new URL("/api/v1/plex/watch-link", API_HOSTNAME); | ||||||
|  |   url.searchParams.append("title", title); | ||||||
|  |   url.searchParams.append("year", year); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(response => response.link); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const movieImages = id => { | ||||||
|  |   const url = new URL(`v2/movie/${id}/images`, API_HOSTNAME); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(resp => resp.json()); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Seasoned user endpoints - - - | ||||||
|  |  | ||||||
|  | const register = (username, password) => { | ||||||
|  |   const url = new URL("/api/v1/user", API_HOSTNAME); | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ username, password }) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       const errorMessage = | ||||||
|  |         "Unexpected error occured before receiving response. Error:"; | ||||||
|  |       // eslint-disable-next-line no-console | ||||||
|  |       console.error(errorMessage, error); | ||||||
|  |       // TODO log to sentry the issue here | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const login = (username, password, throwError = false) => { | ||||||
|  |   const url = new URL("/api/v1/user/login", API_HOSTNAME); | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ username, password }) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options).then(resp => { | ||||||
|  |     if (resp.status === 200) return resp.json(); | ||||||
|  |  | ||||||
|  |     if (throwError) throw resp; | ||||||
|  |     console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console | ||||||
|  |     return Promise.reject(resp); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const logout = (throwError = false) => { | ||||||
|  |   const url = new URL("/api/v1/user/logout", API_HOSTNAME); | ||||||
|  |   const options = { method: "POST" }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options).then(resp => { | ||||||
|  |     if (resp.status === 200) return resp.json(); | ||||||
|  |  | ||||||
|  |     if (throwError) throw resp; | ||||||
|  |     console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console | ||||||
|  |     return Promise.reject(resp); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getSettings = () => { | ||||||
|  |   const url = new URL("/api/v1/user/settings", API_HOSTNAME); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.log("api error getting user settings"); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateSettings = settings => { | ||||||
|  |   const url = new URL("/api/v1/user/settings", API_HOSTNAME); | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     method: "PUT", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify(settings) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.log("api error updating user settings"); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Authenticate with plex - - - | ||||||
|  |  | ||||||
|  | const linkPlexAccount = (username, password) => { | ||||||
|  |   const url = new URL("/api/v1/user/link_plex", API_HOSTNAME); | ||||||
|  |   const body = { username, password }; | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify(body) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error linking plex account: ${username}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const unlinkPlexAccount = () => { | ||||||
|  |   const url = new URL("/api/v1/user/unlink_plex", API_HOSTNAME); | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error unlinking your plex account`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - User graphs - - - | ||||||
|  |  | ||||||
|  | const fetchGraphData = (urlPath, days, chartType) => { | ||||||
|  |   const url = new URL(`/api/v1/user/${urlPath}`, API_HOSTNAME); | ||||||
|  |   url.searchParams.append("days", days); | ||||||
|  |   url.searchParams.append("y_axis", chartType); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(resp => { | ||||||
|  |     if (!resp.ok) { | ||||||
|  |       console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console | ||||||
|  |       throw Error(resp.statusText); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return resp.json(); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Random emoji - - - | ||||||
|  |  | ||||||
|  | const getEmoji = () => { | ||||||
|  |   const url = new URL("/api/v1/emoji", API_HOSTNAME); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.log("api error getting emoji"); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - ELASTIC SEARCH - - - | ||||||
|  | // This elastic index contains titles mapped to ids. Lightning search | ||||||
|  | // used for autocomplete | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and | ||||||
|  |  * Tv Shows. See tmdb docs for more info: https://developers.themoviedb.org/3/getting-started/daily-file-exports | ||||||
|  |  * @param {string} query | ||||||
|  |  * @returns {object} List of movies and shows matching query | ||||||
|  |  */ | ||||||
|  | const elasticSearchMoviesAndShows = (query, count = 22) => { | ||||||
|  |   const url = new URL(`${ELASTIC_INDEX}/_search`, ELASTIC); | ||||||
|  |  | ||||||
|  |   const body = { | ||||||
|  |     sort: [{ popularity: { order: "desc" } }, "_score"], | ||||||
|  |     query: { | ||||||
|  |       bool: { | ||||||
|  |         should: [ | ||||||
|  |           { | ||||||
|  |             match_phrase_prefix: { | ||||||
|  |               original_name: query | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             match_phrase_prefix: { | ||||||
|  |               original_title: query | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     size: count | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify(body) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |   getMovie, | ||||||
|  |   getShow, | ||||||
|  |   getPerson, | ||||||
|  |   getMovieCredits, | ||||||
|  |   getShowCredits, | ||||||
|  |   getPersonCredits, | ||||||
|  |   getTmdbMovieListByName, | ||||||
|  |   searchTmdb, | ||||||
|  |   getUserRequests, | ||||||
|  |   getRequests, | ||||||
|  |   searchTorrents, | ||||||
|  |   addMagnet, | ||||||
|  |   request, | ||||||
|  |   watchLink, | ||||||
|  |   movieImages, | ||||||
|  |   getRequestStatus, | ||||||
|  |   linkPlexAccount, | ||||||
|  |   unlinkPlexAccount, | ||||||
|  |   register, | ||||||
|  |   login, | ||||||
|  |   logout, | ||||||
|  |   getSettings, | ||||||
|  |   updateSettings, | ||||||
|  |   fetchGraphData, | ||||||
|  |   getEmoji, | ||||||
|  |   elasticSearchMoviesAndShows | ||||||
|  | }; | ||||||
| Before Width: | Height: | Size: 1.9 KiB | 
| @@ -1,72 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section class="not-found"> |  | ||||||
|     <div class="not-found__content"> |  | ||||||
|       <h2 class="not-found__title">Page Not Found</h2> |  | ||||||
|     </div> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '../storage.js' |  | ||||||
| export default { |  | ||||||
|   created(){ |  | ||||||
|     document.title = 'Page Not Found' + storage.pageTitlePostfix; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss"> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| .not-found{ |  | ||||||
|   width: 100%; |  | ||||||
|   height: calc(100vh - 100px); |  | ||||||
|   background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%; |  | ||||||
|   background-size: cover; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   @include tablet-min{ |  | ||||||
|     height: calc(100vh - 75px); |  | ||||||
|   } |  | ||||||
|   &:before{ |  | ||||||
|     content: ""; |  | ||||||
|     display: block; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |  | ||||||
|     left: 0; |  | ||||||
|     width: 100%; |  | ||||||
|     height: 100%; |  | ||||||
|     background: rgba($c-light, 0.7); |  | ||||||
|   } |  | ||||||
|   &-shortList{ |  | ||||||
|     width: 100%; |  | ||||||
|   } |  | ||||||
|   &__content{ |  | ||||||
|     width: 100%; |  | ||||||
|     padding: 0 20px; |  | ||||||
|     text-align: center; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       padding: 20px 0 0 0; |  | ||||||
|     } |  | ||||||
|     &-shortList { |  | ||||||
|       width: 100%; |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|     &__title{ |  | ||||||
|       font-size: 24px; |  | ||||||
|       font-weight: 500; |  | ||||||
|       color: $c-dark; |  | ||||||
|       position: relative; |  | ||||||
|       margin: 0; |  | ||||||
|       @include tablet-min{ |  | ||||||
|         font-size: 28px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &__button{ |  | ||||||
|       position: relative; |  | ||||||
|       margin-top: 20px; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										51
									
								
								src/components/CastList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="cast"> | ||||||
|  |     <ol class="persons"> | ||||||
|  |       <CastListItem | ||||||
|  |         v-for="credit in cast" | ||||||
|  |         :key="credit.id" | ||||||
|  |         :credit-item="credit" | ||||||
|  |       /> | ||||||
|  |     </ol> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps } from "vue"; | ||||||
|  |   import CastListItem from "src/components/CastListItem.vue"; | ||||||
|  |   import type { | ||||||
|  |     IMovie, | ||||||
|  |     IShow, | ||||||
|  |     IPerson, | ||||||
|  |     ICast, | ||||||
|  |     ICrew | ||||||
|  |   } from "../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     cast: Array<IMovie | IShow | IPerson | ICast | ICrew>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   defineProps<Props>(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  |   .cast { | ||||||
|  |     position: relative; | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |  | ||||||
|  |     ol { | ||||||
|  |       overflow-x: scroll; | ||||||
|  |       padding: 0; | ||||||
|  |       list-style-type: none; | ||||||
|  |       margin: 0; | ||||||
|  |       display: flex; | ||||||
|  |  | ||||||
|  |       scrollbar-width: none; /* for Firefox */ | ||||||
|  |  | ||||||
|  |       &::-webkit-scrollbar { | ||||||
|  |         display: none; /* for Chrome, Safari, and Opera */ | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										117
									
								
								src/components/CastListItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,117 @@ | |||||||
|  | <template> | ||||||
|  |   <li class="card"> | ||||||
|  |     <a @click="openCastItem" @keydown.enter="openCastItem"> | ||||||
|  |       <img :src="pictureUrl" alt="Movie or person poster image" /> | ||||||
|  |       <p class="name">{{ creditItem.name || creditItem.title }}</p> | ||||||
|  |       <p class="meta">{{ creditItem.character || creditItem.year }}</p> | ||||||
|  |     </a> | ||||||
|  |   </li> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps, computed } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import type { ICast, ICrew, IMovie, IShow } from "../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     creditItem: ICast | ICrew | IMovie | IShow; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const store = useStore(); | ||||||
|  |  | ||||||
|  |   const pictureUrl = computed(() => { | ||||||
|  |     const baseUrl = "https://image.tmdb.org/t/p/w185"; | ||||||
|  |  | ||||||
|  |     if ("profile_path" in props.creditItem && props.creditItem.profile_path) { | ||||||
|  |       return baseUrl + props.creditItem.profile_path; | ||||||
|  |     } | ||||||
|  |     if ("poster" in props.creditItem && props.creditItem.poster) { | ||||||
|  |       return baseUrl + props.creditItem.poster; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return "/assets/no-image_small.svg"; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function openCastItem() { | ||||||
|  |     store.dispatch("popup/open", { ...props.creditItem }); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  |   li a p:first-of-type { | ||||||
|  |     padding-top: 10px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   li.card p { | ||||||
|  |     font-size: 1em; | ||||||
|  |     padding: 0 10px; | ||||||
|  |     margin: 0; | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     max-height: calc(10px + ((16px * var(--line-height)) * 3)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   li.card { | ||||||
|  |     margin: 10px; | ||||||
|  |     margin-right: 4px; | ||||||
|  |     padding-bottom: 10px; | ||||||
|  |     border-radius: 8px; | ||||||
|  |     overflow: hidden; | ||||||
|  |     cursor: pointer; | ||||||
|  |  | ||||||
|  |     min-width: 140px; | ||||||
|  |     width: 140px; | ||||||
|  |     background-color: var(--background-color-secondary); | ||||||
|  |     color: var(--text-color); | ||||||
|  |  | ||||||
|  |     transition: all 0.3s ease; | ||||||
|  |     transform: scale(0.97) translateZ(0); | ||||||
|  |  | ||||||
|  |     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||||||
|  |  | ||||||
|  |     &:first-of-type { | ||||||
|  |       margin-left: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); | ||||||
|  |       transform: scale(1.03); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .name { | ||||||
|  |       font-weight: 500; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .character { | ||||||
|  |       font-size: 0.9em; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .meta { | ||||||
|  |       font-size: 0.9em; | ||||||
|  |       color: var(--text-color-70); | ||||||
|  |       display: -webkit-box; | ||||||
|  |       overflow: hidden; | ||||||
|  |       -webkit-line-clamp: 1; | ||||||
|  |       -webkit-box-orient: vertical; | ||||||
|  |       // margin-top: auto; | ||||||
|  |       max-height: calc((0.9em * var(--line-height)) * 1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     a { | ||||||
|  |       display: block; | ||||||
|  |       text-decoration: none; | ||||||
|  |       height: 100%; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     img { | ||||||
|  |       width: 100%; | ||||||
|  |       height: auto; | ||||||
|  |       max-height: 210px; | ||||||
|  |       background-color: var(--background-color); | ||||||
|  |       object-fit: cover; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										170
									
								
								src/components/Graph.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,170 @@ | |||||||
|  | <template> | ||||||
|  |   <canvas ref="graphCanvas"></canvas> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, defineProps, onMounted, watch } from "vue"; | ||||||
|  |   import { | ||||||
|  |     Chart, | ||||||
|  |     LineElement, | ||||||
|  |     BarElement, | ||||||
|  |     PointElement, | ||||||
|  |     LineController, | ||||||
|  |     BarController, | ||||||
|  |     LinearScale, | ||||||
|  |     CategoryScale, | ||||||
|  |     Legend, | ||||||
|  |     Title, | ||||||
|  |     Tooltip, | ||||||
|  |     ChartType | ||||||
|  |   } from "chart.js"; | ||||||
|  |  | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { convertSecondsToHumanReadable } from "../utils"; | ||||||
|  |   import { GraphValueTypes } from "../interfaces/IGraph"; | ||||||
|  |   import type { IGraphDataset, IGraphData } from "../interfaces/IGraph"; | ||||||
|  |  | ||||||
|  |   Chart.register( | ||||||
|  |     LineElement, | ||||||
|  |     BarElement, | ||||||
|  |     PointElement, | ||||||
|  |     LineController, | ||||||
|  |     BarController, | ||||||
|  |     LinearScale, | ||||||
|  |     CategoryScale, | ||||||
|  |     Legend, | ||||||
|  |     Title, | ||||||
|  |     Tooltip | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     name?: string; | ||||||
|  |     data: IGraphData; | ||||||
|  |     type: ChartType; | ||||||
|  |     stacked: boolean; | ||||||
|  |  | ||||||
|  |     datasetDescriptionSuffix: string; | ||||||
|  |     tooltipDescriptionSuffix: string; | ||||||
|  |     graphValueType?: GraphValueTypes; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Chart.defaults.elements.point.radius = 0; | ||||||
|  |   Chart.defaults.elements.point.hitRadius = 10; | ||||||
|  |   // Chart.defaults.elements.point.pointHoverRadius = 10; | ||||||
|  |   Chart.defaults.elements.point.hoverBorderWidth = 4; | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const graphCanvas: Ref<HTMLCanvasElement> = ref(null); | ||||||
|  |   let graphInstance = null; | ||||||
|  |  | ||||||
|  |   /* eslint-disable no-use-before-define */ | ||||||
|  |   onMounted(() => generateGraph()); | ||||||
|  |   watch(() => props.data, generateGraph); | ||||||
|  |   /* eslint-enable no-use-before-define */ | ||||||
|  |  | ||||||
|  |   const graphTemplates = [ | ||||||
|  |     { | ||||||
|  |       backgroundColor: "rgba(54, 162, 235, 0.2)", | ||||||
|  |       borderColor: "rgba(54, 162, 235, 1)", | ||||||
|  |       borderWidth: 1, | ||||||
|  |       tension: 0.4 | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       backgroundColor: "rgba(255, 159, 64, 0.2)", | ||||||
|  |       borderColor: "rgba(255, 159, 64, 1)", | ||||||
|  |       borderWidth: 1, | ||||||
|  |       tension: 0.4 | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       backgroundColor: "rgba(255, 99, 132, 0.2)", | ||||||
|  |       borderColor: "rgba(255, 99, 132, 1)", | ||||||
|  |       borderWidth: 1, | ||||||
|  |       tension: 0.4 | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |   // const gridColor = getComputedStyle(document.documentElement).getPropertyValue( | ||||||
|  |   //   "--text-color-5" | ||||||
|  |   // ); | ||||||
|  |  | ||||||
|  |   function hydrateGraphLineOptions(dataset: IGraphDataset, index: number) { | ||||||
|  |     return { | ||||||
|  |       label: `${dataset.name} ${props.datasetDescriptionSuffix}`, | ||||||
|  |       data: dataset.data, | ||||||
|  |       ...graphTemplates[index] | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function removeEmptyDataset(dataset: IGraphDataset) { | ||||||
|  |     /* eslint-disable-next-line no-unneeded-ternary */ | ||||||
|  |     return dataset.data.every(point => point === 0) ? false : true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function generateGraph() { | ||||||
|  |     const datasets = props.data.series | ||||||
|  |       .filter(removeEmptyDataset) | ||||||
|  |       .map(hydrateGraphLineOptions); | ||||||
|  |  | ||||||
|  |     const graphOptions = { | ||||||
|  |       maintainAspectRatio: false, | ||||||
|  |       plugins: { | ||||||
|  |         tooltip: { | ||||||
|  |           callbacks: { | ||||||
|  |             // title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`, | ||||||
|  |             label: tooltipItem => { | ||||||
|  |               const context = tooltipItem.dataset.label.split(" ")[0]; | ||||||
|  |               const text = `${context} ${props.tooltipDescriptionSuffix}`; | ||||||
|  |  | ||||||
|  |               let value = tooltipItem.raw; | ||||||
|  |               if (props.graphValueType === GraphValueTypes.Time) { | ||||||
|  |                 value = convertSecondsToHumanReadable(value); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               return ` ${text}: ${value}`; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       scales: { | ||||||
|  |         xAxes: { | ||||||
|  |           stacked: props.stacked, | ||||||
|  |           gridLines: { | ||||||
|  |             display: false | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         yAxes: { | ||||||
|  |           stacked: props.stacked, | ||||||
|  |           ticks: { | ||||||
|  |             callback: value => { | ||||||
|  |               if (props.graphValueType === GraphValueTypes.Time) { | ||||||
|  |                 return convertSecondsToHumanReadable(value); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               return value; | ||||||
|  |             }, | ||||||
|  |             beginAtZero: true | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const chartData = { | ||||||
|  |       labels: props.data.labels.toString().split(","), | ||||||
|  |       datasets | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (graphInstance) { | ||||||
|  |       graphInstance.clear(); | ||||||
|  |       graphInstance.data = chartData; | ||||||
|  |       graphInstance.update("none"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     graphInstance = new Chart(graphCanvas.value, { | ||||||
|  |       type: props.type, | ||||||
|  |       data: chartData, | ||||||
|  |       options: graphOptions | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped></style> | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section> |  | ||||||
|     <LandingBanner /> |  | ||||||
|  |  | ||||||
|     <movies-list v-for="item in homepageLists" :propList="item" :shortList="true"></movies-list> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '../storage.js' |  | ||||||
| import LandingBanner from '@/components/LandingBanner.vue' |  | ||||||
| import MoviesList from './MoviesList.vue' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   name: 'home', |  | ||||||
|   components: { LandingBanner, MoviesList }, |  | ||||||
|   data(){ |  | ||||||
|     return { |  | ||||||
|       homepageLists: storage.homepageLists, |  | ||||||
|       imageFile: 'dist/pulp-fiction.jpg' |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     document.title = 'TMDb'; |  | ||||||
|     storage.backTitle = document.title; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| @@ -1,112 +1,218 @@ | |||||||
| <template> | <template> | ||||||
|   <header v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }"> |   <header ref="headerElement" :class="{ expanded, noselect: true }"> | ||||||
|  |     <img ref="imageElement" :src="bannerImage" alt="Page banner image" /> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|       <h1 class="title">Request new movies or tv shows for plex</h1> |       <h1 class="title">Request movies or tv shows</h1> | ||||||
|       <strong class="subtitle">Made with Vue.js</strong> |       <strong class="subtitle" | ||||||
|  |         >Create a profile to track and view requests</strong | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div | ||||||
|  |       class="expand-icon" | ||||||
|  |       @click="expand" | ||||||
|  |       @keydown.enter="expand" | ||||||
|  |       @mouseover="upgradeImage" | ||||||
|  |       @focus="focus" | ||||||
|  |     > | ||||||
|  |       <IconExpand v-if="!expanded" /> | ||||||
|  |       <IconShrink v-else /> | ||||||
|     </div> |     </div> | ||||||
|   </header> |   </header> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { ref } from "vue"; | ||||||
|   props: { |   import IconExpand from "@/icons/IconExpand.vue"; | ||||||
|     image: { |   import IconShrink from "@/icons/IconShrink.vue"; | ||||||
|       type: String, |   import type { Ref } from "vue"; | ||||||
|       required: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       imageFile: 'dist/pulp-fiction.jpg' |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     if (this.image && this.image.length > 0) { |  | ||||||
|       this.imageFile = this.image |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |   const ASSET_URL = "https://request.movie/assets/"; | ||||||
|  |   const images: Array<string> = [ | ||||||
|  |     "pulp-fiction.jpg", | ||||||
|  |     "arrival.jpg", | ||||||
|  |     "disaster-artist.jpg", | ||||||
|  |     "dune.jpg", | ||||||
|  |     "mandalorian.jpg" | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   const bannerImage: Ref<string> = ref(); | ||||||
|  |   const expanded: Ref<boolean> = ref(false); | ||||||
|  |   const headerElement: Ref<HTMLElement> = ref(null); | ||||||
|  |   const imageElement: Ref<HTMLImageElement> = ref(null); | ||||||
|  |   const defaultHeaderHeight: Ref<string> = ref(); | ||||||
|  |   // const disableProxy = true; | ||||||
|  |  | ||||||
|  |   function expand() { | ||||||
|  |     expanded.value = !expanded.value; | ||||||
|  |     let height = defaultHeaderHeight?.value; | ||||||
|  |  | ||||||
|  |     if (expanded.value) { | ||||||
|  |       const aspectRation = | ||||||
|  |         imageElement.value.naturalHeight / imageElement.value.naturalWidth; | ||||||
|  |       height = `${imageElement.value.clientWidth * aspectRation}px`; | ||||||
|  |       defaultHeaderHeight.value = headerElement.value.style.height; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     headerElement.value.style.setProperty("--header-height", height); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function focus(event: FocusEvent) { | ||||||
|  |     event.preventDefault(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function randomImage(): string { | ||||||
|  |     const image = images[Math.floor(Math.random() * images.length)]; | ||||||
|  |     return ASSET_URL + image; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bannerImage.value = randomImage(); | ||||||
|  |  | ||||||
|  |   // function sliceToHeaderSize(url: string): string { | ||||||
|  |   //   let width = headerElement.value?.getBoundingClientRect()?.width || 1349; | ||||||
|  |   //   let height = headerElement.value?.getBoundingClientRect()?.height || 261; | ||||||
|  |  | ||||||
|  |   //   if (disableProxy) return url; | ||||||
|  |  | ||||||
|  |   //   return buildProxyURL(width, height, url); | ||||||
|  |   // } | ||||||
|  |  | ||||||
|  |   // function upgradeImage() { | ||||||
|  |   //   if (disableProxy || imageUpgraded.value == true) return; | ||||||
|  |  | ||||||
|  |   //   const headerSize = 90; | ||||||
|  |   //   const height = window.innerHeight - headerSize; | ||||||
|  |   //   const width = window.innerWidth - headerSize; | ||||||
|  |  | ||||||
|  |   //   const proxyHost = `http://imgproxy.schleppe:8080/insecure/`; | ||||||
|  |   //   const proxySizeOptions = `q:65/plain/`; | ||||||
|  |  | ||||||
|  |   //   bannerImage.value = `${proxyHost}${proxySizeOptions}${ | ||||||
|  |   //     ASSET_URL + image.value | ||||||
|  |   //   }`; | ||||||
|  |   // } | ||||||
|  |  | ||||||
|  |   // function buildProxyURL(width: number, height: number, asset: string): string { | ||||||
|  |   //   const proxyHost = `http://imgproxy.schleppe:8080/insecure/`; | ||||||
|  |   //   const proxySizeOptions = `resize:fill:${width}:${height}:ce/q:65/plain/`; | ||||||
|  |   //   return `${proxyHost}${proxySizeOptions}${asset}`; | ||||||
|  |   // } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "./src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "./src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| header { |   header { | ||||||
|   width: 100%; |  | ||||||
|   height: 200px; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   background-size: cover; |  | ||||||
|   background-repeat: no-repeat; |  | ||||||
|   background-position: 50% 50%; |  | ||||||
|   position: relative; |  | ||||||
|   background-color: $c-dark; |  | ||||||
|   @include tablet-min { |  | ||||||
|     height: 284px; |  | ||||||
|   } |  | ||||||
|   &:before { |  | ||||||
|     content: ""; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |  | ||||||
|     left: 0; |  | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     height: 100%; |     display: flex; | ||||||
|     background: rgba($c-light, 0.7); |     align-items: center; | ||||||
|   } |     justify-content: center; | ||||||
|    |  | ||||||
|   .container { |  | ||||||
|     text-align: center; |  | ||||||
|     position: relative; |     position: relative; | ||||||
|   } |     transition: height 0.5s ease; | ||||||
|  |     overflow: hidden; | ||||||
|  |     --header-height: 25vh; | ||||||
|  |  | ||||||
|   .title { |     height: var(--header-height); | ||||||
|     font-weight: 500; |  | ||||||
|     font-size: 22px; |     > * { | ||||||
|     text-transform: uppercase; |       z-index: 1; | ||||||
|     letter-spacing: 0.5px; |  | ||||||
|     color: $c-dark; |  | ||||||
|     margin: 0; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       font-size: 28px; |  | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .subtitle { |     img { | ||||||
|     display: block; |       position: absolute; | ||||||
|     font-size: 14px; |       z-index: 0; | ||||||
|     font-weight: 300; |       object-fit: cover; | ||||||
|     color: $c-dark; |       width: 100%; | ||||||
|     margin: 5px 0; |     } | ||||||
|     @include tablet-min{ |  | ||||||
|       font-size: 16px; |     &.expanded { | ||||||
|  |       // height: calc(100vh - var(--header-size)); | ||||||
|  |       // width: calc(100vw - var(--header-size)); | ||||||
|  |  | ||||||
|  |       // @include mobile { | ||||||
|  |       //   width: 100vw; | ||||||
|  |       //   height: 100vh; | ||||||
|  |       // } | ||||||
|  |  | ||||||
|  |       &:before { | ||||||
|  |         background-color: transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .title, | ||||||
|  |       .subtitle { | ||||||
|  |         opacity: 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .expand-icon { | ||||||
|  |       visibility: hidden; | ||||||
|  |       opacity: 0; | ||||||
|  |       transition: all 0.5s ease-in-out; | ||||||
|  |       height: 1.8rem; | ||||||
|  |       width: 1.8rem; | ||||||
|  |       fill: var(--text-color-50); | ||||||
|  |  | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0.5rem; | ||||||
|  |       right: 1rem; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         cursor: pointer; | ||||||
|  |         fill: var(--text-color-90); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .link { |  | ||||||
|     text-decoration: none; |  | ||||||
|     color: $c-dark; |  | ||||||
|     font-size: 13px; |  | ||||||
|     font-weight: 300; |  | ||||||
|     opacity: 0.7; |  | ||||||
|     transition: opacity 0.5s ease; |  | ||||||
|     &:hover { |     &:hover { | ||||||
|  |       .expand-icon { | ||||||
|  |         visibility: visible; | ||||||
|  |         opacity: 1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:before { | ||||||
|  |       content: ""; | ||||||
|  |       z-index: 1; | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|  |       left: 0; | ||||||
|  |       width: 100%; | ||||||
|  |       height: 100%; | ||||||
|  |       background-color: var(--background-70); | ||||||
|  |       transition: inherit; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .container { | ||||||
|  |       text-align: center; | ||||||
|  |       position: relative; | ||||||
|  |       transition: color 0.5s ease; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .title { | ||||||
|  |       font-weight: 500; | ||||||
|  |       font-size: 22px; | ||||||
|  |       text-transform: uppercase; | ||||||
|  |       letter-spacing: 0.5px; | ||||||
|  |       color: $text-color; | ||||||
|  |       margin: 0; | ||||||
|       opacity: 1; |       opacity: 1; | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         font-size: 2.5rem; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     span { |  | ||||||
|       display: inline-block; |     .subtitle { | ||||||
|       vertical-align: middle; |       display: block; | ||||||
|     } |       font-size: 14px; | ||||||
|     &-icon { |       font-weight: 300; | ||||||
|       display: inline-block; |       color: $text-color-70; | ||||||
|       vertical-align: middle; |       margin: 5px 0; | ||||||
|       margin-right: 2px; |       opacity: 1; | ||||||
|       width: 16px; |  | ||||||
|       height: 15px; |       @include tablet-min { | ||||||
|       fill: $c-dark; |         font-size: 1.3rem; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
| @@ -1,456 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section class="movie"> |  | ||||||
|  |  | ||||||
|     <!-- HEADER w/ POSTER --> |  | ||||||
|     <header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }"> |  | ||||||
|       <div class="movie__wrap movie__wrap--header"> |  | ||||||
|         <figure class="movie__poster"> |  | ||||||
|           <img v-if="movie && poster === null" |  | ||||||
|             class="movies-item__img is-loaded" |  | ||||||
|             alt="movie poster image" |  | ||||||
|             src="~assets/no-image.png"> |  | ||||||
|           <img v-else-if="poster === undefined" |  | ||||||
|             class="movies-item__img grey" |  | ||||||
|             alt="movie poster image"> |  | ||||||
|             <!-- src="~assets/placeholder.png"> --> |  | ||||||
|           <img v-else |  | ||||||
|             class="movies-item__img is-loaded" |  | ||||||
|             alt="movie poster image" |  | ||||||
|             :src="ASSET_URL + ASSET_SIZES[0] + poster"> |  | ||||||
|         </figure> |  | ||||||
|  |  | ||||||
|         <div class="movie__title"> |  | ||||||
|           <h1>{{ title }}</h1> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </header> |  | ||||||
|  |  | ||||||
|     <!-- Siderbar and movie info --> |  | ||||||
|     <div class="movie__main"> |  | ||||||
|       <div class="movie__wrap movie__wrap--main"> |  | ||||||
|  |  | ||||||
|         <!-- SIDEBAR ACTIONS --> |  | ||||||
|         <div class="movie__actions" v-if="movie"> |  | ||||||
|  |  | ||||||
|           <sidebar-action |  | ||||||
|             :text="'Not yet in plex'" :iconRef="'#iconNot_exsits'" |  | ||||||
|             :textActive="'Already in plex 🎉'" :iconRefActive="'#iconExists'" |  | ||||||
|             :active="matched"></sidebar-action> |  | ||||||
|           <sidebar-action |  | ||||||
|             @click="sendRequest" |  | ||||||
|             :text="'Request to be downloaded?'" :iconRef="'#iconSent'" |  | ||||||
|             :textActive="'Requested to be downloaded'" |  | ||||||
|             :active="requested"></sidebar-action> |  | ||||||
|           <sidebar-action |  | ||||||
|             v-if="admin" @click="showTorrents=!showTorrents" |  | ||||||
|             :text="'Search for torrents'" :iconRef="'#icon_torrents'" |  | ||||||
|             :active="showTorrents"></sidebar-action> |  | ||||||
|           <sidebar-action |  | ||||||
|             @click="openTmdb()" |  | ||||||
|             :iconRef="'#icon_info'" :text="'See more info'"></sidebar-action> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <!-- Loading placeholder --> |  | ||||||
|         <div class="movie__actions text-input__loading" v-else> |  | ||||||
|           <div class="movie__actions-link" v-for="_ in admin ? Array(4) : Array(3)"> |  | ||||||
|             <div class="movie__actions-text text-input__loading--line" style="margin:9px; margin-left: -3px;"></div> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         <!-- MOVIE INFO --> |  | ||||||
|         <div class="movie__info"> |  | ||||||
|           <div class="movie__description" v-if="movie"> {{ movie.overview }}</div> |  | ||||||
|  |  | ||||||
|           <!-- Loading placeholder --> |  | ||||||
|           <div v-else class="movie__description"> |  | ||||||
|             <loading-placeholder :count="12" /> |  | ||||||
|           </div> |  | ||||||
|  |  | ||||||
|           <div class="movie__details" v-if="movie"> |  | ||||||
|             <div v-if="movie.year" class="movie__details-block"> |  | ||||||
|               <h2 class="movie__details-title">Release Date</h2> |  | ||||||
|               <div class="movie__details-text">{{ movie.year }}</div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|              <div v-if="movie.rank" class="movie__details-block"> |  | ||||||
|               <h2 class="movie__details-title">Rating</h2> |  | ||||||
|               <div class="movie__details-text">{{ movie.rank }}</div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div v-if="movie.type == 'show'" class="movie__details-block"> |  | ||||||
|               <h2 class="movie__details-title">Seasons</h2> |  | ||||||
|               <div class="movie__details-text">{{ movie.seasons }}</div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div v-if="movie.genres" class="movie__details-block"> |  | ||||||
|               <h2 class="movie__details-title">Genres</h2> |  | ||||||
|               <div class="movie__details-text">{{ nestedDataToString(movie.genres) }}</div> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|  |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <!-- TODO: change this classname, this is general  --> |  | ||||||
|  |  | ||||||
|         <div class="movie__admin" v-if="movie && movie.credits"> |  | ||||||
|           <h2 class="movie__details-title">Cast</h2> |  | ||||||
|           <div style="display: flex; flex-wrap: wrap;"> |  | ||||||
|             <person v-for="cast in movie.credits.cast" :info="cast" |  | ||||||
|               style="flex-basis: 0;"></person> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <!-- TORRENT LIST --> |  | ||||||
|       <TorrentList v-if="movie" :show="showTorrents" :query="title" :tmdb_id="id" |  | ||||||
|                    :admin="admin"></TorrentList> |  | ||||||
|     </div> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '@/storage.js' |  | ||||||
| import img from '@/directives/v-image.js' |  | ||||||
| import TorrentList from './TorrentList.vue' |  | ||||||
| import Person from './Person.vue' |  | ||||||
| import SidebarAction from './movie/SidebarAction.vue' |  | ||||||
|  |  | ||||||
| import LoadingPlaceholder from './ui/LoadingPlaceholder.vue' |  | ||||||
|  |  | ||||||
| import { getMovie, getShow, request, getRequestStatus } from '@/api.js' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   props: ['id', 'type'], |  | ||||||
|   components: { TorrentList, Person, LoadingPlaceholder, SidebarAction }, |  | ||||||
|   directives: { img: img }, // TODO decide to remove or use |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       ASSET_URL: 'https://image.tmdb.org/t/p/', |  | ||||||
|       ASSET_SIZES: ['w500', 'w780', 'original'], |  | ||||||
|       movie: undefined, |  | ||||||
|       title: undefined, |  | ||||||
|       poster: undefined, |  | ||||||
|       backdrop: undefined, |  | ||||||
|       matched: false, |  | ||||||
|       userLoggedIn: storage.sessionId ? true : false, |  | ||||||
|       requested: false, |  | ||||||
|       admin: localStorage.getItem('admin'), |  | ||||||
|       showTorrents: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     parseResponse(resp) { |  | ||||||
|       let movie = resp.data; |  | ||||||
|       this.movie = { ...movie } |  | ||||||
|       this.title = movie.title |  | ||||||
|       this.poster = movie.poster |  | ||||||
|       this.backdrop = movie.backdrop |  | ||||||
|       this.matched = movie.existsInPlex |  | ||||||
|       this.checkIfRequested(movie) |  | ||||||
|         .then(status => this.requested = status) |  | ||||||
|  |  | ||||||
|       document.title = movie.title + storage.pageTitlePostfix |  | ||||||
|     }, |  | ||||||
|     async checkIfRequested(movie) { |  | ||||||
|       return await getRequestStatus(movie.id, movie.type) |  | ||||||
|     }, |  | ||||||
|     nestedDataToString(data) { |  | ||||||
|       let nestedArray = [] |  | ||||||
|       data.forEach(item => nestedArray.push(item)); |  | ||||||
|       return nestedArray.join(', '); |  | ||||||
|     }, |  | ||||||
|     sendRequest(){ |  | ||||||
|       request(this.id, this.type, storage.token) |  | ||||||
|         .then(resp => { |  | ||||||
|           if (resp.success) { |  | ||||||
|             this.requested = true |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|     }, |  | ||||||
|     openTmdb(){ |  | ||||||
|       const tmdbType = this.type === 'show' ? 'tv' : this.type |  | ||||||
|       window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     id: function(val){ |  | ||||||
|       if (this.type === 'movie') { |  | ||||||
|         this.fetchMovie(val); |  | ||||||
|       } else { |  | ||||||
|         this.fetchShow(val) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     document.title = this.prevDocumentTitle |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     this.prevDocumentTitle = document.title |  | ||||||
|  |  | ||||||
|     if (this.type === 'movie') { |  | ||||||
|       getMovie(this.id) |  | ||||||
|         .then(this.parseResponse) |  | ||||||
|         .catch(error => { |  | ||||||
|           this.$router.push({ name: '404' }); |  | ||||||
|         }) |  | ||||||
|     } else { |  | ||||||
|       getShow(this.id) |  | ||||||
|         .then(this.parseResponse) |  | ||||||
|         .catch(error => { |  | ||||||
|           this.$router.push({ name: '404' }); |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     console.log('admin: ', this.admin) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/loading-placeholder"; |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| .movie { |  | ||||||
|   &__wrap { |  | ||||||
|     display: flex; |  | ||||||
|     &--header { |  | ||||||
|       align-items: center; |  | ||||||
|       height: 100%; |  | ||||||
|     } |  | ||||||
|     &--main { |  | ||||||
|       display: flex; |  | ||||||
|       flex-wrap: wrap; |  | ||||||
|       flex-direction: column; |  | ||||||
|       @include tablet-min{ |  | ||||||
|         flex-direction: row; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__header { |  | ||||||
|     height: 250px; |  | ||||||
|     position: relative; |  | ||||||
|     background-size: cover; |  | ||||||
|     background-repeat: no-repeat; |  | ||||||
|     background-position: 50% 50%; |  | ||||||
|     background-color: $c-dark; |  | ||||||
|     @include tablet-min { |  | ||||||
|       height: 350px; |  | ||||||
|     } |  | ||||||
|     &:before { |  | ||||||
|       content: ""; |  | ||||||
|       display: block; |  | ||||||
|       position: absolute; |  | ||||||
|       top: 0; |  | ||||||
|       left: 0; |  | ||||||
|       z-index: 0; |  | ||||||
|       width: 100%; |  | ||||||
|       height: 100%; |  | ||||||
|       background: rgba($c-dark, 0.85); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__poster { |  | ||||||
|     display: none; |  | ||||||
|     @include tablet-min { |  | ||||||
|       background: $c-white; |  | ||||||
|       height: 0; |  | ||||||
|       display: block; |  | ||||||
|       position: absolute; |  | ||||||
|       width: calc(45% - 40px); |  | ||||||
|       top: 40px; |  | ||||||
|       left: 40px; |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__img { |  | ||||||
|     display: block; |  | ||||||
|     width: 100%; |  | ||||||
|     opacity: 0; |  | ||||||
|     transform: scale(0.97) translateZ(0); |  | ||||||
|     transition: opacity 0.5s ease, transform 0.5s ease; |  | ||||||
|  |  | ||||||
|     &.is-loaded { |  | ||||||
|       opacity: 1; |  | ||||||
|       transform: scale(1); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__title { |  | ||||||
|     position: relative; |  | ||||||
|     padding: 20px; |  | ||||||
|     color: $c-green; |  | ||||||
|     text-align: center; |  | ||||||
|     width: 100%; |  | ||||||
|     @include tablet-min { |  | ||||||
|       width: 55%; |  | ||||||
|       text-align: left; |  | ||||||
|       margin-left: 45%; |  | ||||||
|       padding: 30px 30px 30px 40px; |  | ||||||
|     } |  | ||||||
|     h1 { |  | ||||||
|       font-weight: 500; |  | ||||||
|       line-height: 1.4; |  | ||||||
|       font-size: 24px; |  | ||||||
|       @include tablet-min { |  | ||||||
|         font-size: 30px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     span { |  | ||||||
|       display: block; |  | ||||||
|       font-size: 14px; |  | ||||||
|       font-weight: 300; |  | ||||||
|       color: rgba($c-white, 0.7); |  | ||||||
|       margin-top: 10px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__main { |  | ||||||
|     background: $c-light; |  | ||||||
|     min-height: calc(100vh - 250px); |  | ||||||
|     @include tablet-min { |  | ||||||
|       min-height: 0; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     height: 100%; |  | ||||||
|   } |  | ||||||
|     &__actions { |  | ||||||
|       text-align: center; |  | ||||||
|       // min-height: 394px; |  | ||||||
|       width: 100%; |  | ||||||
|       order: 2; |  | ||||||
|       padding: 20px; |  | ||||||
|       border-top: 1px solid rgba($c-dark, 0.05); |  | ||||||
|       @include tablet-min { |  | ||||||
|         order: 1; |  | ||||||
|         width: 45%; |  | ||||||
|         padding: 185px 0 40px 40px; |  | ||||||
|         border-top: 0; |  | ||||||
|       } |  | ||||||
|       &-link { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         text-decoration: none; |  | ||||||
|         text-transform: uppercase; |  | ||||||
|         color: rgba($c-dark, 0.5); |  | ||||||
|         transition: color 0.5s ease; |  | ||||||
|         font-size: 11px; |  | ||||||
|         padding: 5px 0; |  | ||||||
|         border-bottom: 1px solid rgba($c-dark, 0.05); |  | ||||||
|         &:hover { |  | ||||||
|           color: rgba($c-dark, 0.75); |  | ||||||
|         } |  | ||||||
|         &.active { |  | ||||||
|           color: $c-dark; |  | ||||||
|         } |  | ||||||
|         &.pending { |  | ||||||
|           color: #f8bd2d; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       &-icon { |  | ||||||
|         width: 18px; |  | ||||||
|         height: 18px; |  | ||||||
|         margin: 0 10px 0 0; |  | ||||||
|         fill: rgba($c-dark, 0.5); |  | ||||||
|         transition: fill 0.5s ease, transform 0.5s ease; |  | ||||||
|         &.waiting { |  | ||||||
|           transform: scale(0.8, 0.8); |  | ||||||
|         } |  | ||||||
|         &.pending { |  | ||||||
|           fill: #f8bd2d; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       &-link:hover &-icon { |  | ||||||
|         fill: rgba($c-dark, 0.75); |  | ||||||
|         cursor: pointer; |  | ||||||
|       } |  | ||||||
|       &-link.active &-icon { |  | ||||||
|         fill: $c-green; |  | ||||||
|       } |  | ||||||
|       &-text { |  | ||||||
|         display: block; |  | ||||||
|         padding-top: 2px; |  | ||||||
|         cursor: pointer; |  | ||||||
|         margin:4.4px; |  | ||||||
|         margin-left: -3px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &__info { |  | ||||||
|       width: 100%; |  | ||||||
|       padding: 20px; |  | ||||||
|       order: 1; |  | ||||||
|       @include tablet-min { |  | ||||||
|         order: 2; |  | ||||||
|         padding: 40px; |  | ||||||
|         width: 55%; |  | ||||||
|         margin-left: 45%; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &__actions + &__info { |  | ||||||
|       margin-left: 0; |  | ||||||
|     } |  | ||||||
|     &__description { |  | ||||||
|       font-weight: 300; |  | ||||||
|       font-size: 13px; |  | ||||||
|       line-height: 1.8; |  | ||||||
|       margin-bottom: 20px; |  | ||||||
|       @include tablet-min { |  | ||||||
|         margin-bottom: 30px; |  | ||||||
|         font-size: 14px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &__details { |  | ||||||
|       &-block { |  | ||||||
|         float: left; |  | ||||||
|       } |  | ||||||
|       &-block:not(:last-child) { |  | ||||||
|         margin-bottom: 20px; |  | ||||||
|         margin-right: 20px; |  | ||||||
|         @include tablet-min { |  | ||||||
|           margin-bottom: 30px; |  | ||||||
|           margin-right: 30px; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       &-title { |  | ||||||
|         margin: 0; |  | ||||||
|         font-weight: 400; |  | ||||||
|         text-transform: uppercase; |  | ||||||
|         font-size: 14px; |  | ||||||
|         color: $c-green; |  | ||||||
|         @include tablet-min { |  | ||||||
|           font-size: 16px; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       &-text { |  | ||||||
|         font-weight: 300; |  | ||||||
|         font-size: 14px; |  | ||||||
|         margin-top: 5px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &__admin { |  | ||||||
|       width: 100%; |  | ||||||
|       padding: 20px; |  | ||||||
|       order: 2; |  | ||||||
|       @include tablet-min { |  | ||||||
|         order: 3; |  | ||||||
|         padding: 40px; |  | ||||||
|         padding-top: 0px; |  | ||||||
|         width: 100%; |  | ||||||
|       } |  | ||||||
|       &-title { |  | ||||||
|           margin: 0; |  | ||||||
|           font-weight: 400; |  | ||||||
|           text-transform: uppercase; |  | ||||||
|           text-align: center; |  | ||||||
|           font-size: 14px; |  | ||||||
|           color: $c-green; |  | ||||||
|           padding-bottom: 20px; |  | ||||||
|           @include tablet-min { |  | ||||||
|             font-size: 16px; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="container info"> |  | ||||||
|     <movie :id="$route.params.id" :type="'page'"></movie> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import Movie from './Movie.vue'; |  | ||||||
| export default { |  | ||||||
|   components: { Movie } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| @@ -1,87 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="movie-popup" @click="$popup.close()"> |  | ||||||
|     <div class="movie-popup__box" @click.stop> |  | ||||||
|       <movie :id="id" :type="type"></movie> |  | ||||||
|       <button class="movie-popup__close" @click="$popup.close()"></button> |  | ||||||
|     </div> |  | ||||||
|     <i class="loader"></i> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import Movie from './Movie.vue'; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   props: ['id', 'type'], |  | ||||||
|   components: { Movie }, |  | ||||||
|   created(){ |  | ||||||
|     let that = this |  | ||||||
|     window.addEventListener('keyup', function(e){ |  | ||||||
|       if (e.keyCode == 27) { |  | ||||||
|         that.$popup.close() |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss"> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| .movie-popup{ |  | ||||||
|   position: fixed; |  | ||||||
|   top: 0; |  | ||||||
|   left: 0; |  | ||||||
|   z-index: 20; |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100vh; |  | ||||||
|   background: rgba($c-dark, 0.93); |  | ||||||
|   -webkit-overflow-scrolling: touch; |  | ||||||
|   overflow: auto; |  | ||||||
|   &__box{ |  | ||||||
|     width: 100%; |  | ||||||
|     max-width: 768px; |  | ||||||
|     position: relative; |  | ||||||
|     z-index: 5; |  | ||||||
|     background: $c-dark; |  | ||||||
|     padding-bottom: 50px; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       padding-bottom: 0; |  | ||||||
|       margin: 40px auto; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__close{ |  | ||||||
|     display: block; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |  | ||||||
|     right: 0; |  | ||||||
|     border: 0; |  | ||||||
|     background: transparent; |  | ||||||
|     width: 40px; |  | ||||||
|     height: 40px; |  | ||||||
|     transition: background 0.5s ease; |  | ||||||
|     cursor: pointer; |  | ||||||
|     &:before, |  | ||||||
|     &:after{ |  | ||||||
|       content: ""; |  | ||||||
|       display: block; |  | ||||||
|       position: absolute; |  | ||||||
|       top: 19px; |  | ||||||
|       left: 10px; |  | ||||||
|       width: 20px; |  | ||||||
|       height: 2px; |  | ||||||
|       background: $c-white; |  | ||||||
|     } |  | ||||||
|     &:before{ |  | ||||||
|       transform: rotate(45deg); |  | ||||||
|     } |  | ||||||
|     &:after{ |  | ||||||
|       transform: rotate(-45deg); |  | ||||||
|     } |  | ||||||
|     &:hover{ |  | ||||||
|       background: $c-green; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,328 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <div class='movies-list' v-if="!error"> |  | ||||||
|       <header class='list-header'> |  | ||||||
|         <h2 class='header__title'>{{ listTitle }}</h2> |  | ||||||
|  |  | ||||||
|         <router-link class='header__view-more' |  | ||||||
|                      :to="'/list/' + list.route" |  | ||||||
|                      v-if='shortList'> |  | ||||||
|                      View All</router-link> |  | ||||||
|  |  | ||||||
|         <div v-else style="line-height: 0;"> |  | ||||||
|           <span class='header__result-count' v-if="totalResults">{{ resultCount }} results</span> |  | ||||||
|           <loading-placeholder v-else :count="1" lineClass='short nomargin'></loading-placeholder> |  | ||||||
|         </div> |  | ||||||
|       </header> |  | ||||||
|  |  | ||||||
|       <!-- <ul class="filter"> |  | ||||||
|         <li class="filter-item" v-for="(item, index) in results" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item.title }}</li> |  | ||||||
|       </ul> --> |  | ||||||
|  |  | ||||||
|       <ul class='results'> |  | ||||||
|         <movies-list-item v-for='movie in results' :movie="movie" :shortList="shortList"></movies-list-item> |  | ||||||
|       </ul> |  | ||||||
|  |  | ||||||
|       <loader v-if="loader" /> |  | ||||||
|  |  | ||||||
|       <div class='end-section' v-if="!shortList"> |  | ||||||
|         <seasoned-button v-if="currentPage < totalPages" @click="loadMore">load more</seasoned-button> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div v-else style="display: flex; height: 50vh; width: 100%; justify-content: center; align-items: center;"> |  | ||||||
|  |  | ||||||
|       <h1 v-if="error">{{ error }}</h1> |  | ||||||
|       <h1 v-else>Unable to load list: {{ listTitle }}</h1> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '@/storage.js' |  | ||||||
| import MoviesListItem from '@/components/MoviesListItem.vue' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton.vue' |  | ||||||
| import LoadingPlaceholder from '@/components/ui/LoadingPlaceholder.vue' |  | ||||||
| import Loader from '@/components/ui/Loader.vue' |  | ||||||
| import { searchTmdb, getTmdbListByPath } from '@/api.js' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     shortList: { |  | ||||||
|       type: Boolean, |  | ||||||
|       default: false |  | ||||||
|     }, |  | ||||||
|     propList: Object |  | ||||||
|   }, |  | ||||||
|   components: { MoviesListItem, SeasonedButton, LoadingPlaceholder, Loader }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       listTitle: 'No listname found', |  | ||||||
|       results: [], |  | ||||||
|       currentPage: 1, |  | ||||||
|       totalResults: 0, |  | ||||||
|       totalPages: -1, |  | ||||||
|       fetchingResults: false, |  | ||||||
|       error: undefined, |  | ||||||
|       loader: false, |  | ||||||
|  |  | ||||||
|       filters: { |  | ||||||
|         status: { |  | ||||||
|           elms: ['all', 'requested', 'downloading', 'downloaded'], |  | ||||||
|           selected: 0, |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     resultCount() { |  | ||||||
|       return this.totalResults.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ") |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     if (this.propList) { |  | ||||||
|       this.list = this.propList |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.setPageFromUrlQuery() |  | ||||||
|     this.parseURI() |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     setTimeout(() => { |  | ||||||
|       if (this.results.length === 0 && this.error === undefined) { |  | ||||||
|         this.loader = true |  | ||||||
|       } |  | ||||||
|     }, 200) |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setPageFromUrlQuery() { |  | ||||||
|       if (this.$route.query.page) |  | ||||||
|         this.currentPage = this.$route.query.page |  | ||||||
|         console.log('url page param found', this.currentPage) |  | ||||||
|     }, |  | ||||||
|     getListByName(name) { |  | ||||||
|       return storage.homepageLists.filter(list => list.route === name)[0] |  | ||||||
|     }, |  | ||||||
|     parseURI() { |  | ||||||
|       const currentRouteName = this.$route.name |  | ||||||
|  |  | ||||||
|       // route name is list - we are in a list view |  | ||||||
|       if (currentRouteName === 'list') { |  | ||||||
|         const nameParam = this.$route.params.name |  | ||||||
|         if (this.getListByName(nameParam)) { |  | ||||||
|           this.list = this.getListByName(nameParam) |  | ||||||
|           this.listTitle = this.list.title |  | ||||||
|           this.fetchListitems() |  | ||||||
|         } else { |  | ||||||
|           this.error = `Unable to load list: ` |  | ||||||
|         } |  | ||||||
|       } // route name is search - we are searcing |  | ||||||
|       else if (currentRouteName === 'search') { |  | ||||||
|         if (this.$route.query.query) { |  | ||||||
|           this.query = decodeURIComponent(this.$route.query.query) |  | ||||||
|           this.listTitle = 'Search results: ' + this.query |  | ||||||
|           this.fetchSearchItems() |  | ||||||
|         } else { |  | ||||||
|           this.error = 'Search query is not defined, please try again' |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|       } // no matched route found - using prop to fetch list items |  | ||||||
|       else { |  | ||||||
|         this.listTitle = this.list.title |  | ||||||
|         this.fetchListitems() |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       document.title = this.listTitle |  | ||||||
|     }, |  | ||||||
|     // TODO these should receive a path not get it from list instance |  | ||||||
|     fetchListitems() { |  | ||||||
|       getTmdbListByPath(this.list.path, this.currentPage) |  | ||||||
|        .then(this.parseResponse) |  | ||||||
|        .catch(error => { |  | ||||||
|           console.error(error) |  | ||||||
|           this.error = 'Network error' |  | ||||||
|        }) |  | ||||||
|     }, |  | ||||||
|     fetchSearchItems() { |  | ||||||
|       searchTmdb(this.query, this.currentPage) |  | ||||||
|         .then(this.parseResponse) |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     // TODO what parts are modular and what parts do we  want the component to deal with |  | ||||||
|     // if we pass in some object and then as we initialize we set to local variables. |  | ||||||
|     // This way we call the http-api from outside and pass the response in to the component[0] |  | ||||||
|     // Could also parse the response we are requesting then return a clean object we can |  | ||||||
|     // pass down[1]. |  | ||||||
|  |  | ||||||
|     // [0] if this is done we should also take the page, total pages, total results and |  | ||||||
|     //     the list of results. Maybe also the title of the list  or use local title as fallback? |  | ||||||
|     // [1] an issue with this that duplicate code will be needed for doing the same with |  | ||||||
|     //     url params and paths. |  | ||||||
|     //      (What if we eliminated folder based routes and implemented the routes in hashes |  | ||||||
|     //       with single page applications today the navigation is simple enought that it |  | ||||||
|     //       would maybe not be needed to have a path-route but a hash-local.storage |  | ||||||
|     //       implementation; would allow sharing and remembering paths is just silly for most |  | ||||||
|     //       Single-Page-Applications that are tightly scoped applications) |  | ||||||
|     parseResponse(response) { |  | ||||||
|       const data = response.data |  | ||||||
|       if (data.page > data.total_pages) { |  | ||||||
|         console.error('You have reached the end') |  | ||||||
|         this.error = 'You have reached the end' |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (this.results.length) { |  | ||||||
|         this.results.push(...data.results) |  | ||||||
|       } else { |  | ||||||
|         this.results = this.shortList ? data.results.slice(0,12) : data.results |  | ||||||
|       } |  | ||||||
|       this.page = data.page |  | ||||||
|       this.totalPages = data.total_pages |  | ||||||
|       this.totalResults = data.total_results || data.results.length |  | ||||||
|  |  | ||||||
|       this.loader = false |  | ||||||
|  |  | ||||||
|       console.info(`Response from list: ${this.listTitle}`, { results: this.results, page: this.page, totalPages: this.totalPages, totalResults: this.totalResults }) |  | ||||||
|     }, |  | ||||||
|     loadMore(){ |  | ||||||
|       this.currentPage++; |  | ||||||
|  |  | ||||||
|       console.log('path and name:', this.$route.path, this.$route.name) |  | ||||||
|       let url = '' |  | ||||||
|  |  | ||||||
|       if (this.$route.path.includes('list')) |  | ||||||
|         url = `/#${this.$route.path}?page=${this.currentPage}` |  | ||||||
|       else if (this.$route.path.includes('search')) |  | ||||||
|         url = `/#/search?query=${this.query}&page=${this.currentPage}` |  | ||||||
|  |  | ||||||
|       console.log('new url', url) |  | ||||||
|       window.history.replaceState({}, 'foo', url) |  | ||||||
|  |  | ||||||
|       this.parseURI() |  | ||||||
|     }, |  | ||||||
|     // sort() { |  | ||||||
|     //   console.log(this.showFilters) |  | ||||||
|     // }, |  | ||||||
|     // toggleFilter(item, index){ |  | ||||||
|     //   this.showFilter = this.showFilter ? false : true; |  | ||||||
|     //   // this.results = this.results.filter(result => result.status != 'downloaded') |  | ||||||
|     // }, |  | ||||||
|     // applyFilter(item, index) { |  | ||||||
|     //   this.filter = item; |  | ||||||
|     //   this.filters.status.selected = index; |  | ||||||
|     //   console.log('applied query filter: ', item, index) |  | ||||||
|     //   this.fetchCategory() |  | ||||||
|     // } |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     $route: function () { |  | ||||||
|       console.log('updated route') |  | ||||||
|       this.results = false |  | ||||||
|       this.currentPage = 1 |  | ||||||
|       this.setPageFromUrlQuery() |  | ||||||
|       this.parseURI() |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| @import "./src/scss/elements"; |  | ||||||
|  |  | ||||||
|   .movies-list { |  | ||||||
|     list-style: none; |  | ||||||
|     display: flex; |  | ||||||
|     flex-wrap: wrap; |  | ||||||
|     padding: 15px; |  | ||||||
|  |  | ||||||
|     .results { |  | ||||||
|       list-style: none; |  | ||||||
|       margin: 0; |  | ||||||
|       padding: 0; |  | ||||||
|       display: flex; |  | ||||||
|       flex-wrap: wrap; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     .list-header { |  | ||||||
|       width: 100%; |  | ||||||
|       display: flex; |  | ||||||
|       flex-flow: row wrap; |  | ||||||
|       align-items: center; |  | ||||||
|       justify-content: space-between; |  | ||||||
|       padding: 20px 10px; |  | ||||||
|  |  | ||||||
|       @include tablet-min{ |  | ||||||
|         padding: 23px 15px; |  | ||||||
|       } |  | ||||||
|       @include tablet-landscape-min{ |  | ||||||
|         padding: 16px 25px; |  | ||||||
|       } |  | ||||||
|       @include desktop-min{ |  | ||||||
|         padding: 8px 30px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .header__title { |  | ||||||
|         line-height: 18px; |  | ||||||
|         margin: 0; |  | ||||||
|         font-size: 18px; |  | ||||||
|         color: #081c24; |  | ||||||
|         font-weight: 300; |  | ||||||
|         // flex-basis: 50%; |  | ||||||
|         text-transform: capitalize; |  | ||||||
|  |  | ||||||
|         @include tablet-min{ |  | ||||||
|           font-size: 18px; |  | ||||||
|           line-height: 18px; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .header__result-count { |  | ||||||
|         font-size: 12px; |  | ||||||
|         font-weight: 300; |  | ||||||
|         letter-spacing: .5px; |  | ||||||
|         color: rgba(8,28,36,.5); |  | ||||||
|         text-align: right; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .header__view-more { |  | ||||||
|         font-size: 13px; |  | ||||||
|         font-weight: 300; |  | ||||||
|         letter-spacing: .5px; |  | ||||||
|         color: rgba($c-dark, 0.5); |  | ||||||
|         text-decoration: none; |  | ||||||
|         transition: color .5s ease; |  | ||||||
|         cursor: pointer; |  | ||||||
|  |  | ||||||
|         &:after{ |  | ||||||
|           content: " →"; |  | ||||||
|         } |  | ||||||
|         &:hover{ |  | ||||||
|           color: $c-dark; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     .end-section { |  | ||||||
|       display: flex; |  | ||||||
|       justify-content: center; |  | ||||||
|       width: 100%; |  | ||||||
|       margin: 1rem 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   @import "./src/scss/media-queries"; |  | ||||||
|   .form__group-input { |  | ||||||
|     padding: 10px 5px 10px 15px; |  | ||||||
|     margin-left: 0; |  | ||||||
|     height: 38px; |  | ||||||
|     width: 150px; |  | ||||||
|     font-size: 15px; |  | ||||||
|     @include desktop-min { |  | ||||||
|       width: 200px; |  | ||||||
|       font-size: 17px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,136 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <li class="movies-item" :class="{'shortList': shortList}"> |  | ||||||
|     <a class="movies-item__link" :class="{'no-image': noImage}" @click.prevent="openMoviePopup(movie.id, movie.type)"> |  | ||||||
|  |  | ||||||
|       <figure class="movies-item__poster"> |  | ||||||
|         <img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt=""> |  | ||||||
|         <img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt=""> |  | ||||||
|       </figure> |  | ||||||
|       <div class="movies-item__content"> |  | ||||||
|         <p class="movies-item__title">{{ movie.title }}</p> |  | ||||||
|         <p class="movies-item__title">{{ movie.year }}</p> |  | ||||||
|       </div> |  | ||||||
|     </a> |  | ||||||
|   </li> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import img from '../directives/v-image.js' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   props: ['movie', 'shortList'], |  | ||||||
|   directives: { |  | ||||||
|     img: img |  | ||||||
|   }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       noImage: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     // TODO handle missing images better and load diff sizes based on screen size |  | ||||||
|     poster() { |  | ||||||
|       if (this.movie.poster) { |  | ||||||
|         return 'https://image.tmdb.org/t/p/w500' + this.movie.poster |  | ||||||
|       } else { |  | ||||||
|         this.noImage = true |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     openMoviePopup(id, type){ |  | ||||||
|       this.$popup.open(id, type) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss"> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| .movies-item { |  | ||||||
|   padding: 10px; |  | ||||||
|   width: 50%; |  | ||||||
|  |  | ||||||
|   @include tablet-min{ |  | ||||||
|     padding: 15px; |  | ||||||
|   } |  | ||||||
|   @include tablet-landscape-min{ |  | ||||||
|     padding: 20px; |  | ||||||
|     width: 25%; |  | ||||||
|   } |  | ||||||
|   @include desktop-min{ |  | ||||||
|     padding: 30px; |  | ||||||
|     width: 20%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include desktop-lg-min{ |  | ||||||
|     padding: 20px; |  | ||||||
|     width: 16.5%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.shortList { |  | ||||||
|     display: none; |  | ||||||
|  |  | ||||||
|     &:nth-child(-n+6) { // show first 6 |  | ||||||
|       display: block; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @include tablet-landscape-min{ |  | ||||||
|       &:nth-child(-n+8) { // show first 8 |  | ||||||
|         display: block; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     @include desktop-min{ |  | ||||||
|       &:nth-child(-n+10) { // show first 10 |  | ||||||
|         display: block; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @include desktop-lg-min{ |  | ||||||
|       display: block; // show all |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__link{ |  | ||||||
|     text-decoration: none; |  | ||||||
|     color: rgba($c-dark, 0.5); |  | ||||||
|     font-weight: 300; |  | ||||||
|   } |  | ||||||
|   &__content{ |  | ||||||
|     padding-top: 15px; |  | ||||||
|   } |  | ||||||
|   &__poster{ |  | ||||||
|     transition: transform 0.5s ease, box-shadow 0.3s ease; |  | ||||||
|     transform: translateZ(0); |  | ||||||
|     background: $c-white; |  | ||||||
|   } |  | ||||||
|   &__img{ |  | ||||||
|     width: 100%; |  | ||||||
|     opacity: 0; |  | ||||||
|     transform: scale(0.97) translateZ(0); |  | ||||||
|     transition: opacity 0.5s ease, transform 0.5s ease; |  | ||||||
|     &.is-loaded{ |  | ||||||
|       opacity: 1; |  | ||||||
|       transform: scale(1); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__link:not(.no-image):hover &__poster{ |  | ||||||
|     transform: scale(1.03); |  | ||||||
|     box-shadow: 0 0 10px rgba($c-dark, 0.1); |  | ||||||
|   } |  | ||||||
|   &__title{ |  | ||||||
|     margin: 0; |  | ||||||
|     font-size: 11px; |  | ||||||
|     letter-spacing: 0.5px; |  | ||||||
|     transition: color 0.5s ease; |  | ||||||
|     @include mobile-ls-min{ |  | ||||||
|       font-size: 12px; |  | ||||||
|     } |  | ||||||
|     @include tablet-min{ |  | ||||||
|       font-size: 14px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__link:hover &__title{ |  | ||||||
|     color: $c-dark; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,307 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <nav class="nav"> |  | ||||||
|       <router-link class="nav__logo" :to="{name: 'home'}" exact title="Vue.js — TMDb App"> |  | ||||||
|         <svg class="nav__logo-image"> |  | ||||||
|           <use xlink:href="#svgLogo"></use> |  | ||||||
|         </svg> |  | ||||||
|       </router-link> |  | ||||||
|       <div class="nav__hamburger" @click="toggleNav"> |  | ||||||
|         <div class="bar"></div> |  | ||||||
|         <div class="bar"></div> |  | ||||||
|         <div class="bar"></div> |  | ||||||
|       </div> |  | ||||||
|       <ul class="nav__list"> |  | ||||||
|         <li class="nav__item" v-for="item in listTypes"> |  | ||||||
|           <router-link class="nav__link" :to="'/list/' + item.route"> |  | ||||||
|             <div class="nav__link-wrap"> |  | ||||||
|               <!-- <img :src="item.icon" class="nav__link-icon"> --> |  | ||||||
|               <svg class="nav__link-icon"> |  | ||||||
|                 <use :xlink:href="'#icon_' + item.route"></use> |  | ||||||
|               </svg> |  | ||||||
|               <span class="nav__link-title">{{ item.title }}</span> |  | ||||||
|             </div> |  | ||||||
|           </router-link> |  | ||||||
|         </li> |  | ||||||
|         <li class="nav__item nav__item--profile"> |  | ||||||
|            <router-link  class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn"> |  | ||||||
|             <div class="nav__link-wrap"> |  | ||||||
|               <svg class="nav__link-icon"> |  | ||||||
|                 <use xlink:href="#iconLogin"></use> |  | ||||||
|               </svg> |  | ||||||
|               <span class="nav__link-title">Sign in</span> |  | ||||||
|             </div> |  | ||||||
|           </router-link> |  | ||||||
|           <router-link  class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn"> |  | ||||||
|             <div class="nav__link-wrap"> |  | ||||||
|               <svg class="nav__link-icon"> |  | ||||||
|                 <use xlink:href="#iconLogin"></use> |  | ||||||
|               </svg> |  | ||||||
|               <span class="nav__link-title">Profile</span> |  | ||||||
|             </div> |  | ||||||
|           </router-link> |  | ||||||
|         </li> |  | ||||||
|       </ul> |  | ||||||
|     </nav> |  | ||||||
|     <div class="spacer"></div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '@/storage.js' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   data(){ |  | ||||||
|     return { |  | ||||||
|       listTypes: storage.homepageLists, |  | ||||||
|       userLoggedIn: localStorage.getItem('token') ? true : false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setUserStatus(){ |  | ||||||
|       this.userLoggedIn = localStorage.getItem('token') ? true : false; |  | ||||||
|     }, |  | ||||||
|     toggleNav(){ |  | ||||||
|       document.querySelector('.nav__hamburger').classList.toggle('nav__hamburger--active'); |  | ||||||
|       document.querySelector('.nav__list').classList.toggle('nav__list--active'); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     eventHub.$on('setUserStatus', this.setUserStatus); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| .spacer { |  | ||||||
|   @include mobile-only { |  | ||||||
|     width: 100%; |  | ||||||
|     height: $header-size-mobile; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .nav { |  | ||||||
|   position: fixed; |  | ||||||
|   top: 0; |  | ||||||
|   left: 0; |  | ||||||
|   width: 100%; |  | ||||||
|   height: 50px; |  | ||||||
|   background: $c-white; |  | ||||||
|   z-index: 10; |  | ||||||
|   display: block; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   @include tablet-min{ |  | ||||||
|     width: 95px; |  | ||||||
|     height: 100vh; |  | ||||||
|   } |  | ||||||
|   &__logo{ |  | ||||||
|     width: 55px; |  | ||||||
|     height: $header-size-mobile; |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: center; |  | ||||||
|     background: $c-dark; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       width: 95px; |  | ||||||
|       height: $header-size; |  | ||||||
|     } |  | ||||||
|     &-image{ |  | ||||||
|       width: 35px; |  | ||||||
|       height: 31px; |  | ||||||
|       fill: $c-green; |  | ||||||
|       transition: transform 0.5s ease; |  | ||||||
|       @include tablet-min{ |  | ||||||
|         width: 45px; |  | ||||||
|         height: 40px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &:hover &-image{ |  | ||||||
|       transform: scale(1.04); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__hamburger{ |  | ||||||
|     display: block; |  | ||||||
|     position: fixed; |  | ||||||
|     width: 55px; |  | ||||||
|     height: 50px; |  | ||||||
|     top: 0; |  | ||||||
|     right: 0; |  | ||||||
|     cursor: pointer; |  | ||||||
|     background: $c-white; |  | ||||||
|     z-index: 10; |  | ||||||
|     border-left: 1px solid $c-light; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     .bar{ |  | ||||||
|       position: absolute; |  | ||||||
|       width: 23px; |  | ||||||
|       height: 1px; |  | ||||||
|       background: rgba($c-dark, 0.5); |  | ||||||
|       transition: all 300ms ease; |  | ||||||
|       &:nth-child(1){ |  | ||||||
|         left: 16px; |  | ||||||
|         top: 17px; |  | ||||||
|       } |  | ||||||
|       &:nth-child(2){ |  | ||||||
|         left: 16px; |  | ||||||
|         top: 25px; |  | ||||||
|         &:after { |  | ||||||
|           content: ""; |  | ||||||
|           position: absolute; |  | ||||||
|           left: 0px; |  | ||||||
|           top: 0px; |  | ||||||
|           width: 23px; |  | ||||||
|           height: 1px; |  | ||||||
|           background: transparent; |  | ||||||
|           transition: all 300ms ease; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       &:nth-child(3){ |  | ||||||
|         right: 15px; |  | ||||||
|         top: 33px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &--active{ |  | ||||||
|       .bar{ |  | ||||||
|         &:nth-child(1), |  | ||||||
|         &:nth-child(3){ |  | ||||||
|           width: 0; |  | ||||||
|         } |  | ||||||
|         &:nth-child(2) { |  | ||||||
|           transform: rotate(-45deg); |  | ||||||
|         } |  | ||||||
|         &:nth-child(2):after { |  | ||||||
|           transform: rotate(-90deg); |  | ||||||
|           background: rgba($c-dark, 0.5); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__list{ |  | ||||||
|     list-style: none; |  | ||||||
|     padding: 0; |  | ||||||
|     margin: 0; |  | ||||||
|     text-align: center; |  | ||||||
|     width: 100%; |  | ||||||
|     position: fixed; |  | ||||||
|     left: 0; |  | ||||||
|     top: 50px; |  | ||||||
|     background: rgba($c-white, 0.98); |  | ||||||
|     border-top: 1px solid $c-light; |  | ||||||
|     @include mobile-only{ |  | ||||||
|       font-size: 0; |  | ||||||
|       opacity: 0; |  | ||||||
|       visibility: hidden; |  | ||||||
|       height: calc(100vh - 50px); |  | ||||||
|       transition: all 0.5s ease; |  | ||||||
|       text-align: left; |  | ||||||
|       &--active{ |  | ||||||
|         opacity: 1; |  | ||||||
|         visibility: visible; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     @include tablet-min{ |  | ||||||
|       display: flex; |  | ||||||
|       background: transparent; |  | ||||||
|       position: relative; |  | ||||||
|       display: block; |  | ||||||
|       width: 100%; |  | ||||||
|       border-top: 0; |  | ||||||
|       top: 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__item{ |  | ||||||
|     @include mobile-only{ |  | ||||||
|       display: inline-block; |  | ||||||
|       text-align: center; |  | ||||||
|       width: 50%; |  | ||||||
|       border-bottom: 1px solid $c-light; |  | ||||||
|       &:nth-child(odd){ |  | ||||||
|         border-right: 1px solid $c-light; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     @include tablet-min{ |  | ||||||
|       width: 100%; |  | ||||||
|       border-bottom: 1px solid $c-light; |  | ||||||
|       &--profile{ |  | ||||||
|         position: fixed; |  | ||||||
|         right: 0; |  | ||||||
|         top: 0; |  | ||||||
|         width: $header-size; |  | ||||||
|         height: $header-size; |  | ||||||
|         border-bottom: 0; |  | ||||||
|         border-left: 1px solid $c-light; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__link{ |  | ||||||
|     width: 100%; |  | ||||||
|     display: flex; |  | ||||||
|     flex-wrap: wrap; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: center; |  | ||||||
|     font-size: 7px; |  | ||||||
|     font-weight: 300; |  | ||||||
|     text-decoration: none; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|     letter-spacing: 0.5px; |  | ||||||
|     color: rgba($c-dark, 0.7); |  | ||||||
|     transition: color 0.5s ease, background 0.5s ease; |  | ||||||
|     position: relative; |  | ||||||
|     cursor: pointer; |  | ||||||
|     &-wrap { |  | ||||||
|       display: flex; |  | ||||||
|       flex-direction: column; |  | ||||||
|       align-items: center; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @include mobile-only{ |  | ||||||
|       font-size: 10px; |  | ||||||
|       padding: 20px 0; |  | ||||||
|     } |  | ||||||
|     @include tablet-min{ |  | ||||||
|       width: 95px; |  | ||||||
|       height: 95px; |  | ||||||
|       font-size: 9px; |  | ||||||
|       &--profile{ |  | ||||||
|         width: 75px; |  | ||||||
|         height: 75px; |  | ||||||
|         background: $c-white; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &-icon{ |  | ||||||
|       width: 20px; |  | ||||||
|       height: 20px; |  | ||||||
|       fill: rgba($c-dark, 0.7); |  | ||||||
|       transition: fill 0.5s ease; |  | ||||||
|       @include tablet-min{ |  | ||||||
|         width: 20px; |  | ||||||
|         height: 20px; |  | ||||||
|         margin-bottom: 5px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|     &-title{ |  | ||||||
|       margin-top: 5px; |  | ||||||
|       display: block; |  | ||||||
|       width: 100%; |  | ||||||
|     } |  | ||||||
|     &:hover{ |  | ||||||
|       color: $c-dark; |  | ||||||
|     } |  | ||||||
|     &:hover &-icon{ |  | ||||||
|       fill: $c-dark; |  | ||||||
|     } |  | ||||||
|     &.is-active{ |  | ||||||
|       color: $c-dark; |  | ||||||
|       background: $c-light; |  | ||||||
|     } |  | ||||||
|     &.is-active &-icon{ |  | ||||||
|       fill: $c-dark; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										110
									
								
								src/components/PageHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,110 @@ | |||||||
|  | <template> | ||||||
|  |   <header> | ||||||
|  |     <h2>{{ prettify }}</h2> | ||||||
|  |     <h3>{{ subtitle }}</h3> | ||||||
|  |  | ||||||
|  |     <router-link | ||||||
|  |       v-if="shortList" | ||||||
|  |       :to="urlify" | ||||||
|  |       class="view-more" | ||||||
|  |       :aria-label="`View all ${title}`" | ||||||
|  |     > | ||||||
|  |       View All | ||||||
|  |     </router-link> | ||||||
|  |  | ||||||
|  |     <div v-else-if="info"> | ||||||
|  |       <div v-if="info instanceof Array" class="flex flex-direction-column"> | ||||||
|  |         <span v-for="item in info" :key="item" class="info">{{ item }}</span> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <span v-else class="info">{{ info }}</span> | ||||||
|  |     </div> | ||||||
|  |   </header> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps, computed } from "vue"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     title: string; | ||||||
|  |     subtitle?: string; | ||||||
|  |     info?: string | Array<string>; | ||||||
|  |     link?: string; | ||||||
|  |     shortList?: boolean; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |  | ||||||
|  |   const urlify = computed(() => { | ||||||
|  |     return `/list/${props.title.toLowerCase().replace(" ", "_")}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const prettify = computed(() => { | ||||||
|  |     return props.title.includes("_") | ||||||
|  |       ? props.title.split("_").join(" ") | ||||||
|  |       : props.title; | ||||||
|  |   }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |  | ||||||
|  |   header { | ||||||
|  |     width: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     align-items: center; | ||||||
|  |     padding: 0.5rem 0.75rem; | ||||||
|  |     background-color: $background-color; | ||||||
|  |  | ||||||
|  |     position: sticky; | ||||||
|  |     position: -webkit-sticky; | ||||||
|  |     top: $header-size; | ||||||
|  |     z-index: 1; | ||||||
|  |  | ||||||
|  |     h2 { | ||||||
|  |       font-size: 1.4rem; | ||||||
|  |       font-weight: 300; | ||||||
|  |       text-transform: capitalize; | ||||||
|  |       line-height: 1.4rem; | ||||||
|  |       margin: 0; | ||||||
|  |       color: $text-color; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .view-more { | ||||||
|  |       font-size: 0.9rem; | ||||||
|  |       font-weight: 300; | ||||||
|  |       letter-spacing: 0.5px; | ||||||
|  |       color: $text-color-70; | ||||||
|  |       text-decoration: none; | ||||||
|  |       transition: color 0.5s ease; | ||||||
|  |       cursor: pointer; | ||||||
|  |  | ||||||
|  |       &:after { | ||||||
|  |         content: " →"; | ||||||
|  |       } | ||||||
|  |       &:hover { | ||||||
|  |         color: $text-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .info { | ||||||
|  |       font-size: 13px; | ||||||
|  |       font-weight: 300; | ||||||
|  |       letter-spacing: 0.5px; | ||||||
|  |       color: $text-color; | ||||||
|  |       text-decoration: none; | ||||||
|  |       text-align: right; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       padding-left: 1.25rem; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include desktop-lg-min { | ||||||
|  |       padding-left: 1.75rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
| @@ -1,63 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="persons"> |  | ||||||
|     <div class="persons--image" :style="{  |  | ||||||
|       'background-image': 'url(' + getPicture(info) + ')' }"></div> |  | ||||||
|     <span>{{ info.name }}</span> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   name: 'Person', |  | ||||||
|   components: { |  | ||||||
|  |  | ||||||
|   }, |  | ||||||
|   props: { |  | ||||||
|     info: Object |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() {}, |  | ||||||
|   beforeMount() {}, |  | ||||||
|   computed: { |  | ||||||
|      |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
| getPicture: (person) => { |  | ||||||
|   if (person) |  | ||||||
|       return 'https://image.tmdb.org/t/p/w185' + person.profile_path; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss"> |  | ||||||
| .persons { |  | ||||||
|   display: flex; |  | ||||||
|   // border: 1px solid black; |  | ||||||
|   flex-direction: column; |  | ||||||
|  |  | ||||||
|   margin: 0 0.5rem; |  | ||||||
|  |  | ||||||
|   span { |  | ||||||
|     font-size: 0.6rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &--image { |  | ||||||
|     border-radius: 50%; |  | ||||||
|     height: 70px; |  | ||||||
|     width: 70px; |  | ||||||
|     // height: auto; |  | ||||||
|     background-size: cover; |  | ||||||
|     background-repeat: no-repeat; |  | ||||||
|     background-position: 50% 50%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &--name { |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										152
									
								
								src/components/Popup.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,152 @@ | |||||||
|  | <template> | ||||||
|  |   <div v-if="isOpen" class="movie-popup" @click="close" @keydown.enter="close"> | ||||||
|  |     <div class="movie-popup__box" @click.stop> | ||||||
|  |       <person v-if="type === 'person'" :id="id" type="person" /> | ||||||
|  |       <movie v-else :id="id" :type="type"></movie> | ||||||
|  |       <button class="movie-popup__close" @click="close"></button> | ||||||
|  |     </div> | ||||||
|  |     <i class="loader"></i> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, onMounted, onBeforeUnmount } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import Movie from "@/components/popup/Movie.vue"; | ||||||
|  |   import Person from "@/components/popup/Person.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { MediaTypes } from "../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   interface URLQueryParameters { | ||||||
|  |     id: number; | ||||||
|  |     type: MediaTypes; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const store = useStore(); | ||||||
|  |   const isOpen: Ref<boolean> = ref(); | ||||||
|  |   const id: Ref<string> = ref(); | ||||||
|  |   const type: Ref<MediaTypes> = ref(); | ||||||
|  |  | ||||||
|  |   const unsubscribe = store.subscribe((mutation, state) => { | ||||||
|  |     if (!mutation.type.includes("popup")) return; | ||||||
|  |  | ||||||
|  |     isOpen.value = state.popup.open; | ||||||
|  |     id.value = state.popup.id; | ||||||
|  |     type.value = state.popup.type; | ||||||
|  |  | ||||||
|  |     if (isOpen.value) { | ||||||
|  |       document.getElementsByTagName("body")[0].classList.add("no-scroll"); | ||||||
|  |     } else { | ||||||
|  |       document.getElementsByTagName("body")[0].classList.remove("no-scroll"); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function getFromURLQuery(): URLQueryParameters { | ||||||
|  |     let _id: number; | ||||||
|  |     let _type: MediaTypes; | ||||||
|  |  | ||||||
|  |     const params = new URLSearchParams(window.location.search); | ||||||
|  |     params.forEach((value, key) => { | ||||||
|  |       if ( | ||||||
|  |         key !== MediaTypes.Movie && | ||||||
|  |         key !== MediaTypes.Show && | ||||||
|  |         key !== MediaTypes.Person | ||||||
|  |       ) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       _id = Number(params.get(key)); | ||||||
|  |       _type = key; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { id: _id, type: _type }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function open(_id: number, _type: string) { | ||||||
|  |     if (!_id || !_type) return; | ||||||
|  |     store.dispatch("popup/open", { id: _id, type: _type }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function close() { | ||||||
|  |     store.dispatch("popup/close"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function checkEventForEscapeKey(event: KeyboardEvent) { | ||||||
|  |     if (event.keyCode !== 27) return; | ||||||
|  |     close(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   window.addEventListener("keyup", checkEventForEscapeKey); | ||||||
|  |  | ||||||
|  |   onMounted(() => { | ||||||
|  |     const query = getFromURLQuery(); | ||||||
|  |     open(query?.id, query?.type); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   onBeforeUnmount(() => { | ||||||
|  |     unsubscribe(); | ||||||
|  |     window.removeEventListener("keyup", checkEventForEscapeKey); | ||||||
|  |   }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   .movie-popup { | ||||||
|  |     position: fixed; | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |     z-index: 20; | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     background: rgba($dark, 0.93); | ||||||
|  |     -webkit-overflow-scrolling: touch; | ||||||
|  |     overflow: auto; | ||||||
|  |  | ||||||
|  |     &__box { | ||||||
|  |       max-width: 768px; | ||||||
|  |       position: relative; | ||||||
|  |       z-index: 5; | ||||||
|  |       margin: 8vh auto; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         margin: 0 0 50px 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &__close { | ||||||
|  |       display: block; | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|  |       right: 0; | ||||||
|  |       border: 0; | ||||||
|  |       background: transparent; | ||||||
|  |       width: 40px; | ||||||
|  |       height: 40px; | ||||||
|  |       transition: background 0.5s ease; | ||||||
|  |       cursor: pointer; | ||||||
|  |       z-index: 5; | ||||||
|  |  | ||||||
|  |       &:before, | ||||||
|  |       &:after { | ||||||
|  |         content: ""; | ||||||
|  |         display: block; | ||||||
|  |         position: absolute; | ||||||
|  |         top: 19px; | ||||||
|  |         left: 10px; | ||||||
|  |         width: 20px; | ||||||
|  |         height: 2px; | ||||||
|  |         background: $white; | ||||||
|  |       } | ||||||
|  |       &:before { | ||||||
|  |         transform: rotate(45deg); | ||||||
|  |       } | ||||||
|  |       &:after { | ||||||
|  |         transform: rotate(-45deg); | ||||||
|  |       } | ||||||
|  |       &:hover { | ||||||
|  |         background: $green; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
| @@ -1,126 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section class="profile"> |  | ||||||
|     <div class="profile__content" v-if="userLoggedIn"> |  | ||||||
|       <header class="profile__header"> |  | ||||||
|         <h2 class="profile__title">{{ emoji }} Welcome {{ userName }}</h2> |  | ||||||
|          |  | ||||||
|         <div class="button--group"> |  | ||||||
|           <seasoned-button @click="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button> |  | ||||||
|  |  | ||||||
|           <seasoned-button @click="logOut">Log out</seasoned-button> |  | ||||||
|         </div> |  | ||||||
|       </header> |  | ||||||
|       <settings v-if="showSettings"></settings> |  | ||||||
|  |  | ||||||
|       <movies-list :propList="user_requestsList"></movies-list> |  | ||||||
|  |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <section class="not-found" v-if="!userLoggedIn"> |  | ||||||
|       <div class="not-found__content"> |  | ||||||
|         <h2 class="not-found__title">Authentication Request Failed</h2> |  | ||||||
|         <router-link :to="{name: 'signin'}" exact title="Sign in here"> |  | ||||||
|           <button class="not-found__button button">Sign In</button> |  | ||||||
|         </router-link> |  | ||||||
|       </div> |  | ||||||
|     </section> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '@/storage.js' |  | ||||||
| import MoviesList from '@/components/MoviesList.vue' |  | ||||||
| import Settings from '@/components/Settings.vue' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton.vue' |  | ||||||
|  |  | ||||||
| import { getEmoji } from '@/api.js' |  | ||||||
| // import CreatedLists from './CreatedLists.vue' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { MoviesList, Settings, SeasonedButton }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       userLoggedIn: '', |  | ||||||
|       userName: '', |  | ||||||
|       emoji: '', |  | ||||||
|       showSettings: false, |  | ||||||
|       user_requestsList: storage.user_requestsList |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     createSession(token){ |  | ||||||
|       axios.get(`https://api.themoviedb.org/3/authentication/session/new?api_key=${storage.apiKey}&request_token=${token}`) |  | ||||||
|       .then(function(resp){ |  | ||||||
|           let data = resp.data; |  | ||||||
|           if(data.success){ |  | ||||||
|             let id = data.session_id; |  | ||||||
|             localStorage.setItem('session_id', id); |  | ||||||
|             eventHub.$emit('setUserStatus'); |  | ||||||
|             this.userLoggedIn = true; |  | ||||||
|             this.getUserInfo(); |  | ||||||
|           } |  | ||||||
|       }.bind(this)); |  | ||||||
|     }, |  | ||||||
|     getUserInfo(){ |  | ||||||
|       this.userName = localStorage.getItem('username');  |  | ||||||
|     }, |  | ||||||
|     toggleSettings() { |  | ||||||
|       this.showSettings = this.showSettings ? false : true; |  | ||||||
|     }, |  | ||||||
|     logOut(){ |  | ||||||
|       localStorage.clear(); |  | ||||||
|       eventHub.$emit('setUserStatus'); |  | ||||||
|       this.$router.push({ name: 'home' }); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     document.title = 'Profile' + storage.pageTitlePostfix; |  | ||||||
|     storage.backTitle = document.title; |  | ||||||
|     if(!localStorage.getItem('token')){ |  | ||||||
|       this.userLoggedIn = false; |  | ||||||
|     } else { |  | ||||||
|       this.userLoggedIn = true; |  | ||||||
|       this.getUserInfo(); |  | ||||||
|  |  | ||||||
|       getEmoji() |  | ||||||
|         .then(resp => this.emoji = resp.data.emoji ) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| // DUPLICATE CODE |  | ||||||
| .profile{ |  | ||||||
|   &__header{ |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: space-between; |  | ||||||
|     padding: 20px; |  | ||||||
|     border-bottom: 1px solid rgba($c-dark, 0.05); |  | ||||||
|     @include tablet-min{ |  | ||||||
|       padding: 29px 30px; |  | ||||||
|     } |  | ||||||
|     @include tablet-landscape-min{ |  | ||||||
|       padding: 29px 50px; |  | ||||||
|     } |  | ||||||
|     @include desktop-min{ |  | ||||||
|       padding: 29px 60px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__title{ |  | ||||||
|     margin: 0; |  | ||||||
|     font-size: 16px; |  | ||||||
|     line-height: 16px; |  | ||||||
|     color: $c-dark; |  | ||||||
|     font-weight: 300; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       font-size: 18px; |  | ||||||
|       line-height: 18px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,202 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section class="profile"> |  | ||||||
|     <div class="profile__content"> |  | ||||||
|       <h2 class='settings__header'>Register new user</h2> |  | ||||||
|  |  | ||||||
|       <form class="form"> |  | ||||||
|         <seasoned-input text="username" icon="Email" |  | ||||||
|                         @inputValue="setValue('username', $event)"></seasoned-input> |  | ||||||
|         <seasoned-input text="password" icon="Keyhole" type="password" |  | ||||||
|                         @inputValue="setValue('password', $event)"></seasoned-input> |  | ||||||
|         <seasoned-input text="repeat password" icon="Keyhole" type="password" |  | ||||||
|                         @inputValue="setValue('passwordRepeat', $event)"></seasoned-input> |  | ||||||
|  |  | ||||||
|         <transition name="message-fade"> |  | ||||||
|           <div class="message" :class="messageClass" v-if="showMessage"> |  | ||||||
|             <span class="message-text">{{ messageText }}</span> |  | ||||||
|             <span class="message-dismiss" v-on:click="dismissMessage">X</span> |  | ||||||
|           </div> |  | ||||||
|         </transition> |  | ||||||
|  |  | ||||||
|         <div class="form__group"> |  | ||||||
|           <seasoned-button @click="requestNewUser">Register</seasoned-button> |  | ||||||
|         </div> |  | ||||||
|       </form> |  | ||||||
|        |  | ||||||
|       <div class="form__group"> |  | ||||||
|         <router-link class="form__group-link" :to="{name: 'signin'}" exact title="Sign in here"> |  | ||||||
|           <span class="form__group-signin">Sign in here</span> |  | ||||||
|         </router-link> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|     </div> |  | ||||||
|     <section class="not-found" v-if="userLoggedIn === false"> |  | ||||||
|       <div class="not-found__content"> |  | ||||||
|         <h2 class="not-found__title">Authentication Request Failed</h2> |  | ||||||
|         <button class="not-found__button button">Log In</button> |  | ||||||
|       </div> |  | ||||||
|     </section> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import axios from 'axios' |  | ||||||
| import storage from '@/storage.js' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton.vue' |  | ||||||
| import SeasonedInput from '@/components/ui/SeasonedInput.vue' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { SeasonedButton, SeasonedInput }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       userLoggedIn: '', |  | ||||||
|       username: undefined, |  | ||||||
|       password: undefined, |  | ||||||
|       passwordRepeat: undefined, |  | ||||||
|       showMessage: false, |  | ||||||
|       messageClass: 'message-success', |  | ||||||
|       messageText: 'hello world' |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     requestNewUser(){ |  | ||||||
|       let username = this.username |  | ||||||
|       let password = this.password |  | ||||||
|       let password_re = this.passwordRepeat |  | ||||||
|  |  | ||||||
|       let verifyCredentials = this.checkCredentials(username, password, password_re); |  | ||||||
|        |  | ||||||
|       if (verifyCredentials.verified) { |  | ||||||
|         axios.post(`https://api.kevinmidboe.com/api/v1/user`, { |  | ||||||
|           username: username, |  | ||||||
|           password: password |  | ||||||
|         }) |  | ||||||
|         .then(function(resp) { |  | ||||||
|           let data = resp.data; |  | ||||||
|           if (data.success){ |  | ||||||
|             this.msg(data.message, 'success'); |  | ||||||
|             localStorage.setItem('token', data.token); |  | ||||||
|             localStorage.setItem('username', username); |  | ||||||
|             localStorage.setItem('admin', data.admin) |  | ||||||
|              |  | ||||||
|             eventHub.$emit('setUserStatus'); |  | ||||||
|             this.$router.push({ name: 'profile' }) |  | ||||||
|           } |  | ||||||
|         }.bind(this)) |  | ||||||
|         .catch(function(error){ |  | ||||||
|           this.msg(error.response.data.error, 'warning') |  | ||||||
|         }.bind(this)); |  | ||||||
|       }  |  | ||||||
|       else { |  | ||||||
|         this.msg(verifyCredentials.reason, 'warning'); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     checkCredentials(username, password, password_re) { |  | ||||||
|       if (password !== password_re) { |  | ||||||
|         return { |  | ||||||
|           verified: false, |  | ||||||
|           reason: 'Passwords do not match' |  | ||||||
|         } |  | ||||||
|       }  |  | ||||||
|       else if (username === undefined) { |  | ||||||
|         return { |  | ||||||
|           verified: false, |  | ||||||
|           reason: 'Please insert username' |  | ||||||
|         } |  | ||||||
|       }  |  | ||||||
|       else { |  | ||||||
|         return { |  | ||||||
|           verified: true, |  | ||||||
|           reason: 'Verified credentials' |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     msg(text, status){ |  | ||||||
|       if (status === 'warning') |  | ||||||
|         this.messageClass = 'message-warning'; |  | ||||||
|       else if (status === 'success') |  | ||||||
|         this.messageClass = 'message-success'; |  | ||||||
|       else |  | ||||||
|         this.messageClass = 'message-info'; |  | ||||||
|       this.messageText = text; |  | ||||||
|       this.showMessage = true; |  | ||||||
|       // setTimeout(() => this.showMessage = false, 3500); |  | ||||||
|     }, |  | ||||||
|     dismissMessage(){ |  | ||||||
|       this.showMessage = false; |  | ||||||
|     }, |  | ||||||
|     setValue(l, t) { |  | ||||||
|       this[l] = t |  | ||||||
|     }, |  | ||||||
|     logOut(){ |  | ||||||
|       localStorage.clear(); |  | ||||||
|       eventHub.$emit('setUserStatus'); |  | ||||||
|       this.$router.push({ name: 'home' }); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     document.title = 'Profile' + storage.pageTitlePostfix; |  | ||||||
|     storage.backTitle = document.title; |  | ||||||
|   }, |  | ||||||
|   mounted(){ |  | ||||||
|     // this.$refs.email.focus(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| @import "./src/scss/message"; |  | ||||||
|  |  | ||||||
| // DUPLICATE CODE |  | ||||||
| .settings { |  | ||||||
|   padding: 35px; |  | ||||||
|  |  | ||||||
|   &__header { |  | ||||||
|     margin: 0; |  | ||||||
|     line-height: 16px; |  | ||||||
|     color: $c-dark; |  | ||||||
|     font-weight: 300; |  | ||||||
|     margin-bottom: 20px; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .profile__content { |  | ||||||
|   padding: 35px; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   flex-direction: column; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| .center { |  | ||||||
|   justify-content: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .form { |  | ||||||
|   // TODO, fix this. if single child it adds weird margin |  | ||||||
|   > div:last-child { |  | ||||||
|     margin-top: 1rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__group{ |  | ||||||
|      justify-content: unset; |  | ||||||
|      &__input-icon { |  | ||||||
|         margin-top: 8px; |  | ||||||
|         height: 22px; |  | ||||||
|         width: 22px; |  | ||||||
|      } |  | ||||||
|      &-input { |  | ||||||
|         padding: 10px 5px 10px 45px; |  | ||||||
|         height: 40px; |  | ||||||
|         font-size: 17px; |  | ||||||
|         width: 75%; |  | ||||||
|         // @include desktop-min { |  | ||||||
|         //    width: 400px; |  | ||||||
|         // } |  | ||||||
|      } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
							
								
								
									
										77
									
								
								src/components/ResultsList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,77 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <ul | ||||||
|  |       v-if="results && results.length" | ||||||
|  |       class="results" | ||||||
|  |       :class="{ shortList: shortList }" | ||||||
|  |     > | ||||||
|  |       <results-list-item | ||||||
|  |         v-for="(result, index) in results" | ||||||
|  |         :key="generateResultKey(index, `${result.type}-${result.id}`)" | ||||||
|  |         :list-item="result" | ||||||
|  |       /> | ||||||
|  |     </ul> | ||||||
|  |  | ||||||
|  |     <span v-else-if="!loading" class="no-results">No results found</span> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps } from "vue"; | ||||||
|  |   import ResultsListItem from "@/components/ResultsListItem.vue"; | ||||||
|  |   import type { ListResults } from "../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     results: Array<ListResults>; | ||||||
|  |     shortList?: boolean; | ||||||
|  |     loading?: boolean; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   defineProps<Props>(); | ||||||
|  |  | ||||||
|  |   function generateResultKey(index: string | number | symbol, value: string) { | ||||||
|  |     return `${String(index)}-${value}`; | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |  | ||||||
|  |   .no-results { | ||||||
|  |     width: 100%; | ||||||
|  |     display: block; | ||||||
|  |     text-align: center; | ||||||
|  |     margin: 1.5rem; | ||||||
|  |     font-size: 1.2rem; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .results { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); | ||||||
|  |     grid-auto-rows: auto; | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  |     list-style: none; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       grid-template-columns: repeat(2, 1fr); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.shortList { | ||||||
|  |       overflow: auto; | ||||||
|  |       grid-auto-flow: column; | ||||||
|  |       max-width: 100vw; | ||||||
|  |  | ||||||
|  |       @include noscrollbar; | ||||||
|  |  | ||||||
|  |       > li { | ||||||
|  |         min-width: 225px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         max-width: calc(100vw - var(--header-size)); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										183
									
								
								src/components/ResultsListItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,183 @@ | |||||||
|  | <template> | ||||||
|  |   <li ref="list-item" class="movie-item"> | ||||||
|  |     <figure | ||||||
|  |       ref="posterElement" | ||||||
|  |       class="movie-item__poster" | ||||||
|  |       @click="openMoviePopup" | ||||||
|  |       @keydown.enter="openMoviePopup" | ||||||
|  |     > | ||||||
|  |       <img | ||||||
|  |         class="movie-item__img" | ||||||
|  |         :alt="posterAltText" | ||||||
|  |         :data-src="poster" | ||||||
|  |         src="/assets/placeholder.png" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <div v-if="listItem.download" class="progress"> | ||||||
|  |         <progress :value="listItem.download.progress" max="100"></progress> | ||||||
|  |         <span | ||||||
|  |           >{{ listItem.download.state }}: | ||||||
|  |           {{ listItem.download.progress }}%</span | ||||||
|  |         > | ||||||
|  |       </div> | ||||||
|  |     </figure> | ||||||
|  |  | ||||||
|  |     <div class="movie-item__info"> | ||||||
|  |       <p v-if="listItem.title || listItem.name" class="movie-item__title"> | ||||||
|  |         {{ listItem.title || listItem.name }} | ||||||
|  |       </p> | ||||||
|  |       <p v-if="listItem.year">{{ listItem.year }}</p> | ||||||
|  |       <p v-if="listItem.type == 'person'"> | ||||||
|  |         Known for: {{ listItem.known_for_department }} | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </li> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, computed, defineProps, onMounted } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import type { IMovie, IShow, IPerson } from "../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     listItem: IMovie | IShow | IPerson; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const store = useStore(); | ||||||
|  |  | ||||||
|  |   const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500"; | ||||||
|  |   const IMAGE_FALLBACK = "/assets/no-image.svg"; | ||||||
|  |   const poster: Ref<string> = ref(); | ||||||
|  |   const posterElement: Ref<HTMLElement> = ref(null); | ||||||
|  |   const observed: Ref<boolean> = ref(false); | ||||||
|  |  | ||||||
|  |   if (props.listItem?.poster) { | ||||||
|  |     poster.value = IMAGE_BASE_URL + props.listItem.poster; | ||||||
|  |   } else { | ||||||
|  |     poster.value = IMAGE_FALLBACK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const posterAltText = computed(() => { | ||||||
|  |     const type = props.listItem.type || ""; | ||||||
|  |     let title = ""; | ||||||
|  |  | ||||||
|  |     if ("name" in props.listItem) title = props.listItem.name; | ||||||
|  |     else if ("title" in props.listItem) title = props.listItem.title; | ||||||
|  |  | ||||||
|  |     return props.listItem.poster | ||||||
|  |       ? `Poster for ${type} ${title}` | ||||||
|  |       : `Missing image for ${type} ${title}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function observePosterAndSetImageSource() { | ||||||
|  |     const imageElement = posterElement.value.getElementsByTagName("img")[0]; | ||||||
|  |     if (imageElement == null) return; | ||||||
|  |  | ||||||
|  |     const imageObserver = new IntersectionObserver(entries => { | ||||||
|  |       entries.forEach(entry => { | ||||||
|  |         if (entry.isIntersecting && observed.value === false) { | ||||||
|  |           const lazyImage = entry.target as HTMLImageElement; | ||||||
|  |           lazyImage.src = lazyImage.dataset.src; | ||||||
|  |           posterElement.value.classList.add("is-loaded"); | ||||||
|  |           observed.value = true; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     imageObserver.observe(imageElement); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onMounted(observePosterAndSetImageSource); | ||||||
|  |  | ||||||
|  |   function openMoviePopup() { | ||||||
|  |     store.dispatch("popup/open", { ...props.listItem }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // const imageSize = computed(() => { | ||||||
|  |   //   if (!posterElement.value) return; | ||||||
|  |   //   const { height, width } = posterElement.value.getBoundingClientRect(); | ||||||
|  |   //   return { | ||||||
|  |   //     height: Math.ceil(height), | ||||||
|  |   //     width: Math.ceil(width) | ||||||
|  |   //   }; | ||||||
|  |   // }); | ||||||
|  |  | ||||||
|  |   // import img from "../directives/v-image"; | ||||||
|  |   //   directives: { | ||||||
|  |   //     img: img | ||||||
|  |   //   }, | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |  | ||||||
|  |   .movie-item { | ||||||
|  |     padding: 15px; | ||||||
|  |     width: 100%; | ||||||
|  |     background-color: var(--background-color); | ||||||
|  |  | ||||||
|  |     &:hover &__info > p { | ||||||
|  |       color: $text-color; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__poster { | ||||||
|  |       text-decoration: none; | ||||||
|  |       color: $text-color-70; | ||||||
|  |       font-weight: 300; | ||||||
|  |       position: relative; | ||||||
|  |       transform: scale(0.97) translateZ(0); | ||||||
|  |  | ||||||
|  |       &::before { | ||||||
|  |         content: ""; | ||||||
|  |         position: absolute; | ||||||
|  |         z-index: 1; | ||||||
|  |         width: 100%; | ||||||
|  |         height: 100%; | ||||||
|  |         background-color: var(--background-color); | ||||||
|  |         transition: 1s background-color ease; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         transform: scale(1.03); | ||||||
|  |         box-shadow: 0 0 10px rgba($dark, 0.1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &.is-loaded::before { | ||||||
|  |         background-color: transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       img { | ||||||
|  |         width: 100%; | ||||||
|  |         border-radius: 10px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__info { | ||||||
|  |       padding-top: 10px; | ||||||
|  |       font-weight: 300; | ||||||
|  |  | ||||||
|  |       > p { | ||||||
|  |         color: $text-color-70; | ||||||
|  |         margin: 0; | ||||||
|  |         font-size: 14px; | ||||||
|  |         letter-spacing: 0.5px; | ||||||
|  |         transition: color 0.5s ease; | ||||||
|  |         cursor: pointer; | ||||||
|  |         @include mobile-ls-min { | ||||||
|  |           font-size: 12px; | ||||||
|  |         } | ||||||
|  |         @include tablet-min { | ||||||
|  |           font-size: 14px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__title { | ||||||
|  |       font-weight: 400; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										203
									
								
								src/components/ResultsSection.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,203 @@ | |||||||
|  | <template> | ||||||
|  |   <div ref="resultSection" class="resultSection"> | ||||||
|  |     <page-header v-bind="{ title, info, shortList }" /> | ||||||
|  |  | ||||||
|  |     <div | ||||||
|  |       v-if="!loadedPages.includes(1) && loading == false" | ||||||
|  |       class="button-container" | ||||||
|  |     > | ||||||
|  |       <seasoned-button class="load-button" :full-width="true" @click="loadLess" | ||||||
|  |         >load previous</seasoned-button | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <results-list v-bind="{ results, shortList, loading }" /> | ||||||
|  |     <loader v-if="loading" /> | ||||||
|  |  | ||||||
|  |     <div ref="loadMoreButton" class="button-container"> | ||||||
|  |       <seasoned-button | ||||||
|  |         v-if="!loading && !shortList && page != totalPages && results.length" | ||||||
|  |         class="load-button" | ||||||
|  |         :full-width="true" | ||||||
|  |         @click="loadMore" | ||||||
|  |         >load more</seasoned-button | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps, ref, computed, onMounted } from "vue"; | ||||||
|  |   import PageHeader from "@/components/PageHeader.vue"; | ||||||
|  |   import ResultsList from "@/components/ResultsList.vue"; | ||||||
|  |   import SeasonedButton from "@/components/ui/SeasonedButton.vue"; | ||||||
|  |   import Loader from "@/components/ui/Loader.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import type { IList, ListResults } from "../interfaces/IList"; | ||||||
|  |   import type ISection from "../interfaces/ISection"; | ||||||
|  |  | ||||||
|  |   interface Props extends ISection { | ||||||
|  |     title: string; | ||||||
|  |     apiFunction: (page: number) => Promise<IList>; | ||||||
|  |     shortList?: boolean; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |  | ||||||
|  |   const results: Ref<ListResults> = ref([]); | ||||||
|  |   const page: Ref<number> = ref(1); | ||||||
|  |   const loadedPages: Ref<number[]> = ref([]); | ||||||
|  |   const totalResults: Ref<number> = ref(0); | ||||||
|  |   const totalPages: Ref<number> = ref(0); | ||||||
|  |   const loading: Ref<boolean> = ref(true); | ||||||
|  |   const autoLoad: Ref<boolean> = ref(false); | ||||||
|  |   const observer: Ref<IntersectionObserver> = ref(null); | ||||||
|  |   const resultSection = ref(null); | ||||||
|  |   const loadMoreButton = ref(null); | ||||||
|  |  | ||||||
|  |   function pageCountString(_page: number, _totalPages: number) { | ||||||
|  |     return `Page ${_page} of ${_totalPages}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function resultCountString(_results: ListResults, _totalResults: number) { | ||||||
|  |     const loadedResults = _results.length; | ||||||
|  |     const __totalResults = _totalResults < 10000 ? _totalResults : "∞"; | ||||||
|  |     return `${loadedResults} of ${__totalResults} results`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setLoading(state: boolean) { | ||||||
|  |     loading.value = state; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const info = computed(() => { | ||||||
|  |     if (results.value.length === 0) return [null, null]; | ||||||
|  |  | ||||||
|  |     const pageCount = pageCountString(page.value, totalPages.value); | ||||||
|  |     const resultCount = resultCountString(results.value, totalResults.value); | ||||||
|  |     return [pageCount, resultCount]; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function getPageFromUrl() { | ||||||
|  |     const _page = new URLSearchParams(window.location.search).get("page"); | ||||||
|  |     if (!_page) return null; | ||||||
|  |  | ||||||
|  |     return Number(_page); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function updateQueryParams() { | ||||||
|  |     const params = new URLSearchParams(window.location.search); | ||||||
|  |     if (params.has("page")) { | ||||||
|  |       params.set("page", page.value?.toString()); | ||||||
|  |     } else if (page.value > 1) { | ||||||
|  |       params.append("page", page.value?.toString()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     window.history.replaceState( | ||||||
|  |       {}, | ||||||
|  |       "search", | ||||||
|  |       `${window.location.protocol}//${window.location.hostname}${ | ||||||
|  |         window.location.port ? `:${window.location.port}` : "" | ||||||
|  |       }${window.location.pathname}${ | ||||||
|  |         params.toString().length ? `?${params}` : "" | ||||||
|  |       }` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getListResults(front = false) { | ||||||
|  |     props | ||||||
|  |       .apiFunction(page.value) | ||||||
|  |       .then(listResponse => { | ||||||
|  |         if (!front) | ||||||
|  |           results.value = results.value.concat(...listResponse.results); | ||||||
|  |         else results.value = listResponse.results.concat(...results.value); | ||||||
|  |  | ||||||
|  |         page.value = listResponse.page; | ||||||
|  |         loadedPages.value.push(page.value); | ||||||
|  |         loadedPages.value = loadedPages.value.sort((a, b) => a - b); | ||||||
|  |         totalPages.value = listResponse.total_pages; | ||||||
|  |         totalResults.value = listResponse.total_results; | ||||||
|  |       }) | ||||||
|  |       .then(updateQueryParams) | ||||||
|  |       .finally(() => setLoading(false)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function loadMore() { | ||||||
|  |     if (!autoLoad.value) { | ||||||
|  |       autoLoad.value = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     loading.value = true; | ||||||
|  |     const maxPage = [...loadedPages.value].slice(-1)[0]; | ||||||
|  |  | ||||||
|  |     if (Number.isNaN(maxPage)) return; | ||||||
|  |     page.value = maxPage + 1; | ||||||
|  |     getListResults(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function loadLess() { | ||||||
|  |     loading.value = true; | ||||||
|  |     const minPage = loadedPages.value[0]; | ||||||
|  |     if (minPage === 1) return; | ||||||
|  |  | ||||||
|  |     page.value = minPage - 1; | ||||||
|  |     getListResults(true); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleButtonIntersection(entries) { | ||||||
|  |     entries.map(entry => | ||||||
|  |       entry.isIntersecting && autoLoad.value ? loadMore() : null | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setupAutoloadObserver() { | ||||||
|  |     observer.value = new IntersectionObserver(handleButtonIntersection, { | ||||||
|  |       root: resultSection.value.$el, | ||||||
|  |       rootMargin: "0px", | ||||||
|  |       threshold: 0 | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     observer.value.observe(loadMoreButton.value); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   page.value = getPageFromUrl() || page.value; | ||||||
|  |   if (results.value?.length === 0) getListResults(); | ||||||
|  |   onMounted(() => { | ||||||
|  |     if (!props?.shortList) setupAutoloadObserver(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   //   beforeDestroy() { | ||||||
|  |   //     this.observer = undefined; | ||||||
|  |   //   } | ||||||
|  |   // }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   .resultSection { | ||||||
|  |     background-color: var(--background-color); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .button-container { | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     display: flex; | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .load-button { | ||||||
|  |     margin: 2rem 0; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       margin: 1rem 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:last-of-type { | ||||||
|  |       margin-bottom: 4rem; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         margin-bottom: 2rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
| @@ -1,277 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|      |  | ||||||
|     <div class="search"> |  | ||||||
|       <input |  | ||||||
|         type="text" |  | ||||||
|         placeholder="Search for a movie or show" |  | ||||||
|         autocorrect="off" |  | ||||||
|         autocapitalize="off" |  | ||||||
|         v-model="query"  |  | ||||||
|         @input="handleInput"  |  | ||||||
|         @click="focus = true" |  | ||||||
|         @keydown.escape="handleEscape" |  | ||||||
|         @keyup.enter="handleSubmit" |  | ||||||
|         @keydown.up="navigateUp" |  | ||||||
|         @keydown.down="navigateDown" /> |  | ||||||
|  |  | ||||||
|       <svg class="search--icon"><use xlink:href="#iconSearch"></use></svg> |  | ||||||
|     </div>  |  | ||||||
|  |  | ||||||
|     <transition name="fade"> |  | ||||||
|       <div class="dropdown" v-if="!disabled && focus && query.length > 0"> |  | ||||||
|         <div class="dropdown--results"> |  | ||||||
|  |  | ||||||
|           <ul v-for="(item, index) in elasticSearchResults" |  | ||||||
|               @click="$popup.open(item.id, item.type)" |  | ||||||
|               :class="{ active: index + 1 === selectedResult}"> |  | ||||||
|                  |  | ||||||
|               {{ item.name }} |  | ||||||
|           </ul> |  | ||||||
|  |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <seasoned-button class="end-section" fullWidth="true"  |  | ||||||
|           @click="focus = false" :active="elasticSearchResults.length + 1 === selectedResult"> |  | ||||||
|           close |  | ||||||
|         </seasoned-button> |  | ||||||
|       </div> |  | ||||||
|     </transition> |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
|  |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton.vue' |  | ||||||
|  |  | ||||||
| import { elasticSearchMoviesAndShows } from '@/api.js' |  | ||||||
| import config from '@/config.json' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   name: 'SearchInput', |  | ||||||
|   components: { |  | ||||||
|     SeasonedButton |  | ||||||
|   }, |  | ||||||
|   props: ['value'], |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       query: this.value, |  | ||||||
|       focus: false, |  | ||||||
|       disabled: false, |  | ||||||
|       scrollListener: undefined, |  | ||||||
|       scrollDistance: 0, |  | ||||||
|       elasticSearchResults: '', |  | ||||||
|       selectedResult: 0 |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     focus: function(val) { |  | ||||||
|       if (val === true) { |  | ||||||
|         window.addEventListener('scroll', this.disableFocus) |  | ||||||
|       } else { |  | ||||||
|         window.removeEventListener('scroll', this.disableFocus) |  | ||||||
|         this.scrollDistance = 0 |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     const elasticUrl = config.ELASTIC_URL |  | ||||||
|     if (elasticUrl === undefined || elasticUrl === false || elasticUrl === '') { |  | ||||||
|       this.disabled = true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     console.log('scroll eventlistener not removed, destroying!') |  | ||||||
|     window.removeEventListener('scroll', this.disableFocus) |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     navigateDown() { |  | ||||||
|       this.focus = true |  | ||||||
|       this.selectedResult++ |  | ||||||
|     }, |  | ||||||
|     navigateUp() { |  | ||||||
|       this.focus = true |  | ||||||
|       this.selectedResult-- |  | ||||||
|     }, |  | ||||||
|     handleInput(e){ |  | ||||||
|       this.selectedResult = 0 |  | ||||||
|       this.$emit('input', this.query); |  | ||||||
|  |  | ||||||
|       if (! this.focus) { |  | ||||||
|         this.focus = true; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       elasticSearchMoviesAndShows(this.query) |  | ||||||
|       .then(resp => { |  | ||||||
|         const data = resp.data.hits.hits |  | ||||||
|  |  | ||||||
|         this.elasticSearchResults = data.map(item => { |  | ||||||
|           const index = item._index.slice(0, -1) |  | ||||||
|           if (index === 'movie') { |  | ||||||
|             return { |  | ||||||
|               name: item._source.original_title, |  | ||||||
|               id: item._source.id, |  | ||||||
|               type: index |  | ||||||
|             } |  | ||||||
|           } else if (index === 'show') { |  | ||||||
|             return { |  | ||||||
|               name: item._source.original_name, |  | ||||||
|               id: item._source.id, |  | ||||||
|               type: index |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         console.log(this.elasticSearchResults) |  | ||||||
|       }) |  | ||||||
|     }, |  | ||||||
|     handleSubmit() { |  | ||||||
|       let searchResults = this.elasticSearchResults |  | ||||||
|  |  | ||||||
|       if (this.selectedResult > searchResults.length) { |  | ||||||
|         this.focus = false |  | ||||||
|         this.selectedResult = 0 |  | ||||||
|       } else if (this.selectedResult > 0) { |  | ||||||
|         const resultItem = searchResults[this.selectedResult - 1] |  | ||||||
|         this.$popup.open(resultItem.id, resultItem.type) |  | ||||||
|       } else { |  | ||||||
|         const encodedQuery = encodeURI(this.query.replace('/ /g, "+"')) |  | ||||||
|         this.$router.push({ name: 'search', query: { query: encodedQuery }}); |  | ||||||
|         this.focus = false |  | ||||||
|         this.selectedResult = 0 |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     handleEscape() { |  | ||||||
|       if (this.$popup.isOpen) { |  | ||||||
|         console.log('THIS WAS FUCKOING OPEN!') |  | ||||||
|       } else { |  | ||||||
|         this.focus = false |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     disableFocus(_) { |  | ||||||
|       this.focus = false |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| @import './src/scss/main'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| .fade-enter-active { |  | ||||||
|   transition: opacity .2s; |  | ||||||
| } |  | ||||||
| .fade-leave-active { |  | ||||||
|   transition: opacity .2s; |  | ||||||
| } |  | ||||||
| .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { |  | ||||||
|   opacity: 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dropdown { |  | ||||||
|   width: 100%; |  | ||||||
|   position: relative; |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   z-index: 5; |  | ||||||
|   min-height: $header-size; |  | ||||||
|   right: 0px; |  | ||||||
|   background-color: white; |  | ||||||
|  |  | ||||||
|   @include mobile-only { |  | ||||||
|     position: fixed; |  | ||||||
|     top: 50px; |  | ||||||
|     padding-top: 20px; |  | ||||||
|     width: calc(100%); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &--results { |  | ||||||
|     padding-left: 60px; |  | ||||||
|     width: 100%; |  | ||||||
|  |  | ||||||
|     @include mobile-only { |  | ||||||
|       padding-left: 45px; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     > ul { |  | ||||||
|       font-size: 1.3rem; |  | ||||||
|       padding: 0; |  | ||||||
|       margin: 0.2rem 0; |  | ||||||
|       width: calc(100% - 25px); |  | ||||||
|       max-width: fit-content; |  | ||||||
|  |  | ||||||
|       list-style: none;       |  | ||||||
|       color: rgba(0, 0, 0, 0.5); |  | ||||||
|       text-transform: capitalize; |  | ||||||
|       cursor: pointer; |  | ||||||
|       border-bottom: 2px solid transparent; |  | ||||||
|  |  | ||||||
|           white-space: nowrap; |  | ||||||
|     text-overflow: ellipsis; |  | ||||||
|     overflow: hidden; |  | ||||||
|  |  | ||||||
|       &.active, &:hover, &:active { |  | ||||||
|         color: $c-dark; |  | ||||||
|         border-bottom: 2px solid black; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .search { |  | ||||||
|   height: $header-size-mobile; |  | ||||||
|   display: flex; |  | ||||||
|   position: fixed; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   z-index: 5; |  | ||||||
|  |  | ||||||
|   // TODO check if this is for mobile |  | ||||||
|   width: calc(100% - 110px); |  | ||||||
|   // width: 100%; |  | ||||||
|   top: 0; |  | ||||||
|   right: 55px; |  | ||||||
|  |  | ||||||
|   @include tablet-min{ |  | ||||||
|     position: relative; |  | ||||||
|     height: $header-size; |  | ||||||
|     width: 100%; |  | ||||||
|     right: 0px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   input { |  | ||||||
|     // height: 75px; |  | ||||||
|     display: block; |  | ||||||
|     width: 100%; |  | ||||||
|     padding: 13px 20px 13px 45px; |  | ||||||
|     outline: none; |  | ||||||
|     border: 0; |  | ||||||
|     background-color: transparent; |  | ||||||
|     color: $c-dark; |  | ||||||
|     font-weight: 300; |  | ||||||
|     font-size: 19px; |  | ||||||
|  |  | ||||||
|     @include tablet-min { |  | ||||||
|       padding: 13px 30px 13px 60px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &--icon{ |  | ||||||
|     width: 20px; |  | ||||||
|     height: 20px; |  | ||||||
|     fill: rgba($c-dark, 0.5); |  | ||||||
|     transition: fill 0.5s ease; |  | ||||||
|     pointer-events: none; |  | ||||||
|     position: absolute; |  | ||||||
|     left: 15px; |  | ||||||
|     top: 15px; |  | ||||||
|  |  | ||||||
|     @include tablet-min{ |  | ||||||
|       top: 27px; |  | ||||||
|       left: 25px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,156 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section class="profile"> |  | ||||||
|     <div class="profile__content" v-if="userLoggedIn"> |  | ||||||
|       <section class='settings'> |  | ||||||
|         <h3 class='settings__header'>Plex account</h3> |  | ||||||
|         <span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span> |  | ||||||
|  |  | ||||||
|         <form class="form"> |  | ||||||
|           <seasoned-input text="plex username" icon="Email" |  | ||||||
|                           @inputValue="setValue('plexUsername', $event)"/> |  | ||||||
|           <seasoned-input text="plex password" icon="Keyhole" type="password" |  | ||||||
|                           @inputValue="setValue('plexPassword', $event)"/> |  | ||||||
|  |  | ||||||
|           <seasoned-button @click="authenticatePlex">link plex account</seasoned-button> |  | ||||||
|         </form> |  | ||||||
|  |  | ||||||
|         <hr class='setting__divider'> |  | ||||||
|  |  | ||||||
|         <h3 class='settings__header'>Change password</h3> |  | ||||||
|         <form class="form"> |  | ||||||
|           <seasoned-input text="new password" icon="Keyhole" type="password" |  | ||||||
|                           @inputValue="setValue('newPass', $event)"/> |  | ||||||
|           <seasoned-input text="repeat new password" icon="Keyhole" type="password" |  | ||||||
|                           @inputValue="setValue('newPassConfirm', $event)"/> |  | ||||||
|  |  | ||||||
|           <seasoned-button @click="changePassword">change password</seasoned-button> |  | ||||||
|         </form> |  | ||||||
|  |  | ||||||
|         <hr class='setting__divider'> |  | ||||||
|       </section> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <section class="not-found" v-else> |  | ||||||
|       <div class="not-found__content"> |  | ||||||
|         <h2 class="not-found__title">Authentication Request Failed</h2> |  | ||||||
|         <router-link :to="{name: 'signin'}" exact title="Sign in here"> |  | ||||||
|           <button class="not-found__button button">Sign In</button> |  | ||||||
|         </router-link> |  | ||||||
|       </div> |  | ||||||
|     </section> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '@/storage.js' |  | ||||||
| import SeasonedInput from '@/components/ui/SeasonedInput.vue' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton.vue' |  | ||||||
|  |  | ||||||
| import { plexAuthenticate } from '@/api.js' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { SeasonedInput, SeasonedButton }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       userLoggedIn: '', |  | ||||||
|       plexUsername: undefined, |  | ||||||
|       plexPassword: undefined, |  | ||||||
|       newPass: undefined, |  | ||||||
|       newPassConfirm: undefined |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setValue(l, t) { |  | ||||||
|       console.log('l, t', l, t) |  | ||||||
|       this[l] = t |  | ||||||
|     }, |  | ||||||
|     changePassword() { |  | ||||||
|       return |  | ||||||
|     }, |  | ||||||
|     authenticatePlex() { |  | ||||||
|       let username = this.plexUsername |  | ||||||
|       let password = this.plexPassword |  | ||||||
|  |  | ||||||
|       plexAuthenticate(username, password) |  | ||||||
|       .then((resp) => { |  | ||||||
|          let data = resp.data; |  | ||||||
|          console.log('response from plex:', data.user) |  | ||||||
|       }) |  | ||||||
|       .catch((error) => { |  | ||||||
|          console.log('error: ', error) |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     document.title = 'Settings' + storage.pageTitlePostfix; |  | ||||||
|     storage.backTitle = document.title; |  | ||||||
|     if (localStorage.getItem('token')){ |  | ||||||
|       this.userLoggedIn = true |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| a { |  | ||||||
|    text-decoration: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DUPLICATE CODE |  | ||||||
| .form { |  | ||||||
|   > div:last-child { |  | ||||||
|     margin-top: 1rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__group{ |  | ||||||
|      justify-content: unset; |  | ||||||
|      &__input-icon { |  | ||||||
|         margin-top: 8px; |  | ||||||
|         height: 22px; |  | ||||||
|         width: 22px; |  | ||||||
|      } |  | ||||||
|      &-input { |  | ||||||
|         padding: 10px 5px 10px 45px; |  | ||||||
|         height: 40px; |  | ||||||
|         font-size: 17px; |  | ||||||
|         width: 75%; |  | ||||||
|         @include desktop-min { |  | ||||||
|            width: 400px; |  | ||||||
|         } |  | ||||||
|      } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .settings { |  | ||||||
|    padding: 35px; |  | ||||||
|  |  | ||||||
|    &__header { |  | ||||||
|       margin: 0; |  | ||||||
|       line-height: 16px; |  | ||||||
|       color: $c-dark; |  | ||||||
|       font-weight: 300; |  | ||||||
|       margin-bottom: 20px; |  | ||||||
|       text-transform: uppercase; |  | ||||||
|    } |  | ||||||
|    &__info { |  | ||||||
|       display: block; |  | ||||||
|       margin-bottom: 25px; |  | ||||||
|    } |  | ||||||
|    hr { |  | ||||||
|       display: block; |  | ||||||
|       height: 1px; |  | ||||||
|       border: 0; |  | ||||||
|       border-bottom: 1px solid rgba(8, 28, 36, 0.05); |  | ||||||
|       margin-top: 30px; |  | ||||||
|       margin-bottom: 70px; |  | ||||||
|       margin-left: 20px; |  | ||||||
|       width: 96%; |  | ||||||
|       text-align: left; |  | ||||||
|    } |  | ||||||
|    span { |  | ||||||
|       font-weight: 200; |  | ||||||
|       size: 16px; |  | ||||||
|    } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,158 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section class="profile"> |  | ||||||
|     <div class="profile__content"> |  | ||||||
|       <h2 class='settings__header'>Sign in</h2> |  | ||||||
|  |  | ||||||
|       <form class="form"> |  | ||||||
|         <div class="form__buffer"></div> |  | ||||||
|  |  | ||||||
|           <seasoned-input text="username" icon="Email" type="username" |  | ||||||
|                           @inputValue="setValue('username', $event)" /> |  | ||||||
|           <seasoned-input text="username" icon="Keyhole" type="password" |  | ||||||
|                           @inputValue="setValue('password', $event)" /> |  | ||||||
|  |  | ||||||
|           <seasoned-button @click="signin">sign in</seasoned-button> |  | ||||||
|            |  | ||||||
|           <transition name="message-fade"> |  | ||||||
|               <div class="message" :class="messageClass" v-if="showMessage"> |  | ||||||
|                 <span class="message-text">{{ messageText }}</span> |  | ||||||
|                 <span class="message-dismiss" @click="showMessage=false">X</span> |  | ||||||
|               </div> |  | ||||||
|             </transition> |  | ||||||
|       </form> |  | ||||||
|        |  | ||||||
|       <div class="form__group"> |  | ||||||
|         <router-link class="form__group-link" :to="{name: 'register'}" exact title="Sign in here"> |  | ||||||
|           <span class="form__group-signin">Don't have a user? Register here</span> |  | ||||||
|         </router-link> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|     </div> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import axios from 'axios' |  | ||||||
| import storage from '../storage.js' |  | ||||||
| import SeasonedInput from '@/components/ui/SeasonedInput.vue' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton.vue' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { SeasonedInput, SeasonedButton }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       userLoggedIn: '', |  | ||||||
|       showMessage: false, |  | ||||||
|       messageClass: 'message-success', |  | ||||||
|       messageText: 'hello world', |  | ||||||
|       username: undefined, |  | ||||||
|       password: undefined |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setValue(l, t) { |  | ||||||
|       this[l] = t |  | ||||||
|     }, |  | ||||||
|     signin(){ |  | ||||||
|       let username = this.username; |  | ||||||
|       let password = this.password; |  | ||||||
|  |  | ||||||
|       axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, { |  | ||||||
|         username: username, |  | ||||||
|         password: password |  | ||||||
|       }) |  | ||||||
|       .then(function (resp){ |  | ||||||
|         let data = resp.data; |  | ||||||
|         if (data.success){ |  | ||||||
|           localStorage.setItem('token', data.token); |  | ||||||
|           localStorage.setItem('username', username); |  | ||||||
|           localStorage.setItem('admin', data.admin); |  | ||||||
|           this.userLoggedIn = true; |  | ||||||
|            |  | ||||||
|           eventHub.$emit('setUserStatus'); |  | ||||||
|           this.$router.push({ name: 'profile' }) |  | ||||||
|         } |  | ||||||
|       }.bind(this)) |  | ||||||
|       .catch(function (error){ |  | ||||||
|         if (error.message.endsWith('401')) |  | ||||||
|           this.msg('Incorrect username or password ', 'warning') |  | ||||||
|         else |  | ||||||
|           this.msg(error.message, 'warning') |  | ||||||
|       }.bind(this)); |  | ||||||
|     }, |  | ||||||
|     msg(text, status){ |  | ||||||
|       if (status === 'warning') |  | ||||||
|         this.messageClass = 'message-warning'; |  | ||||||
|       else if (status === 'success') |  | ||||||
|         this.messageClass = 'message-success'; |  | ||||||
|       else |  | ||||||
|         this.messageClass = 'message-info'; |  | ||||||
|       this.messageText = text; |  | ||||||
|       this.showMessage = true; |  | ||||||
|       // setTimeout(() => this.showMessage = false, 3500); |  | ||||||
|     }, |  | ||||||
|     toggleView(){ |  | ||||||
|       this.register = false; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     document.title = 'Sign in' + storage.pageTitlePostfix; |  | ||||||
|     storage.backTitle = document.title; |  | ||||||
|     if (this.userLoggedIn == true) { |  | ||||||
|       this.$router.push({ name: 'profile' }) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted(){ |  | ||||||
|     // this.$refs.email.focus(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/message"; |  | ||||||
|  |  | ||||||
| // DUPLICATE CODE |  | ||||||
| .settings { |  | ||||||
|   padding: 35px; |  | ||||||
|  |  | ||||||
|   &__header { |  | ||||||
|     margin: 0; |  | ||||||
|     line-height: 16px; |  | ||||||
|     color: $c-dark; |  | ||||||
|     font-weight: 300; |  | ||||||
|     margin-bottom: 20px; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .profile__content { |  | ||||||
|   padding: 35px; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   flex-direction: column; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .form { |  | ||||||
|   > div:last-child { |  | ||||||
|     margin-top: 1rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__group{ |  | ||||||
|      justify-content: unset; |  | ||||||
|      &__input-icon { |  | ||||||
|         margin-top: 8px; |  | ||||||
|         height: 22px; |  | ||||||
|         width: 22px; |  | ||||||
|      } |  | ||||||
|      &-input { |  | ||||||
|         padding: 10px 5px 10px 45px; |  | ||||||
|         height: 40px; |  | ||||||
|         font-size: 17px; |  | ||||||
|         width: 75%; |  | ||||||
|         // @include desktop-min { |  | ||||||
|         //    width: 400px; |  | ||||||
|         // } |  | ||||||
|      } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,386 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div v-if="show"> |  | ||||||
|     <h2 class="title">torrents: {{ query }}</h2> |  | ||||||
|  |  | ||||||
|     <div v-if="listLoaded"> |  | ||||||
|       <ul class="filter"> |  | ||||||
|         <li class="filter-item" v-for="(item, index) in release_types" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item }}</li> |  | ||||||
|       </ul> |  | ||||||
|  |  | ||||||
|  |  | ||||||
|       <table> |  | ||||||
|         <tr class="table__header noselect"> |  | ||||||
|           <th @click="sortTable('name')"> |  | ||||||
|             <span>Name</span> |  | ||||||
|             <span v-if="prevCol === 'name' && direction">↑</span> |  | ||||||
|             <span v-if="prevCol === 'name' && !direction">↓</span> |  | ||||||
|           </th> |  | ||||||
|           <th @click="sortTable('seed')"> |  | ||||||
|             <span>Seed</span> |  | ||||||
|             <span v-if="prevCol === 'seed' && direction">↑</span> |  | ||||||
|             <span v-if="prevCol === 'seed' && !direction">↓</span> |  | ||||||
|           </th> |  | ||||||
|           <th @click="sortTable('size')"> |  | ||||||
|             <span>Size</span> |  | ||||||
|             <span v-if="prevCol === 'size' && direction">↑</span> |  | ||||||
|             <span v-if="prevCol === 'size' && !direction">↓</span> |  | ||||||
|           <th> |  | ||||||
|             <span>Magnet</span> |  | ||||||
|           </th> |  | ||||||
|         </tr> |  | ||||||
|         <tr v-for="torrent in torrents" class="table__content"> |  | ||||||
|           <td @click="expand($event, torrent.name)">{{ torrent.name }}</td> |  | ||||||
|           <td @click="expand($event, torrent.name)">{{ torrent.seed }}</td> |  | ||||||
|           <td @click="expand($event, torrent.name)">{{ torrent.size }}</td> |  | ||||||
|           <td @click="sendTorrent(torrent.magnet, torrent.name, $event)" class="download"> |  | ||||||
|             <svg class="download__icon"><use xlink:href="#iconUnmatched"></use></svg> |  | ||||||
|           </td> |  | ||||||
|         </tr> |  | ||||||
|       </table> |  | ||||||
|     </div> |  | ||||||
|     <i v-else class="torrentloader"></i> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import storage from '@/storage.js' |  | ||||||
| import { sortableSize } from '@/utils.js' |  | ||||||
| import { searchTorrents, addMagnet } from '@/api.js' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     query: { |  | ||||||
|       type: String, |  | ||||||
|       require: true |  | ||||||
|     }, |  | ||||||
|     tmdb_id: { |  | ||||||
|       type: Number, |  | ||||||
|       require: true |  | ||||||
|     }, |  | ||||||
|     tmdb_type: String, |  | ||||||
|     admin: String, |  | ||||||
|     show: Boolean |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       listLoaded: false, |  | ||||||
|       torrents: undefined, |  | ||||||
|       torrentResponse: undefined, |  | ||||||
|       currentPage: 0, |  | ||||||
|       prevCol: '', |  | ||||||
|       direction: false, |  | ||||||
|       release_types: ['all'], |  | ||||||
|       selectedRelaseType: 'all' |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     if (localStorage.getItem('admin')) { |  | ||||||
|       this.fetchTorrents() |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     expand(event, name) { |  | ||||||
|       const existingExpandedElement = document.getElementsByClassName('expanded')[0] |  | ||||||
|  |  | ||||||
|       if (existingExpandedElement) { |  | ||||||
|         console.log('exists') |  | ||||||
|         const expandedSibling = event.target.parentNode.nextSibling.className === 'expanded' |  | ||||||
|  |  | ||||||
|         existingExpandedElement.remove() |  | ||||||
|  |  | ||||||
|         if (expandedSibling) { |  | ||||||
|           console.log('sibling is here') |  | ||||||
|           return |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       console.log('expand event', event) |  | ||||||
|       const nameRow = document.createElement('tr') |  | ||||||
|       const nameCol = document.createElement('td') |  | ||||||
|       nameRow.className = 'expanded' |  | ||||||
|       nameCol.innerText = name |  | ||||||
|  |  | ||||||
|       nameRow.appendChild(nameCol) |  | ||||||
|  |  | ||||||
|       event.target.parentNode.insertAdjacentElement('afterend', nameRow) |  | ||||||
|     }, |  | ||||||
|     sendTorrent(magnet, name, event){ |  | ||||||
|       this.$notifications.info({ |  | ||||||
|         title: 'Adding torrent 🦜', |  | ||||||
|         description: this.query, |  | ||||||
|         timeout: 3000 |  | ||||||
|       }) |  | ||||||
|  |  | ||||||
|       event.target.parentNode.classList.add('active') |  | ||||||
|  |  | ||||||
|       addMagnet(magnet, name, this.tmdb_id) |  | ||||||
|       .catch((resp) => { console.log('error:', resp.data) }) |  | ||||||
|       .then((resp) => { |  | ||||||
|         console.log('addTorrent resp: ', resp) |  | ||||||
|         this.$notifications.success({ |  | ||||||
|           title: 'Torrent added 🎉', |  | ||||||
|           description: this.query, |  | ||||||
|           timeout: 3000 |  | ||||||
|         }) |  | ||||||
|       }) |  | ||||||
|     }, |  | ||||||
|     sortTable(col, sameDirection=false) { |  | ||||||
|       if (this.prevCol === col && sameDirection === false) { |  | ||||||
|         this.direction = !this.direction |  | ||||||
|       } |  | ||||||
|       console.log('col and more', col, sameDirection) |  | ||||||
|  |  | ||||||
|       switch (col) { |  | ||||||
|         case 'name': |  | ||||||
|           this.sortName() |  | ||||||
|           break |  | ||||||
|         case 'seed': |  | ||||||
|           this.sortSeed() |  | ||||||
|           break |  | ||||||
|         case 'size': |  | ||||||
|           this.sortSize() |  | ||||||
|           break |  | ||||||
|       } |  | ||||||
|       this.prevCol = col |  | ||||||
|     }, |  | ||||||
|     sortName() { |  | ||||||
|       const torrentsCopy = [...this.torrents] |  | ||||||
|       if (this.direction) { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => (a.name < b.name) ? 1 : -1) |  | ||||||
|       } else { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => (a.name > b.name) ? 1 : -1) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     sortSeed() { |  | ||||||
|       const torrentsCopy = [...this.torrents] |  | ||||||
|       if (this.direction) { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(a.seed) - parseInt(b.seed)); |  | ||||||
|       } else { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(b.seed) - parseInt(a.seed)); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     sortSize() { |  | ||||||
|       const torrentsCopy = [...this.torrents] |  | ||||||
|       if (this.direction) { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(sortableSize(a.size)) - parseInt(sortableSize(b.size))); |  | ||||||
|       } else { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(sortableSize(b.size)) - parseInt(sortableSize(a.size))); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     findRelaseTypes() { |  | ||||||
|       this.torrents.forEach(item => this.release_types.push(...item.release_type)) |  | ||||||
|       this.release_types = [...new Set(this.release_types)] |  | ||||||
|     }, |  | ||||||
|     applyFilter(item, index) { |  | ||||||
|       this.selectedRelaseType = item; |  | ||||||
|       const torrents = [...this.torrentResponse] |  | ||||||
|  |  | ||||||
|       if (item === 'all') { |  | ||||||
|         this.torrents = torrents |  | ||||||
|         this.sortTable(this.prevCol, true) |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.torrents = torrents.filter(torrent => torrent.release_type.includes(item)) |  | ||||||
|       this.sortTable(this.prevCol, true) |  | ||||||
|     }, |  | ||||||
|     fetchTorrents(){ |  | ||||||
|       searchTorrents(this.query, 'all', this.currentPage, storage.token) |  | ||||||
|       .then(resp => { |  | ||||||
|           let data = resp.data; |  | ||||||
|           console.log('data results', data.results); |  | ||||||
|           this.torrentResponse = data.results; |  | ||||||
|           this.torrents = data.results; |  | ||||||
|           this.listLoaded = true; |  | ||||||
|       }) |  | ||||||
|       .then(this.findRelaseTypes) |  | ||||||
|       .catch(e => { |  | ||||||
|         const error = e.toString() |  | ||||||
|         this.errorMessage = error.indexOf('401') != -1 ? 'Permission denied' : 'Nothing found'; |  | ||||||
|         this.listLoaded = true; |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss"> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| .expanded { |  | ||||||
|   display: flex; |  | ||||||
|   margin: 0 1rem; |  | ||||||
|   max-width: 100%; |  | ||||||
|   border-left: 1px solid rgba($c-dark, 0.5); |  | ||||||
|   border-right: 1px solid rgba($c-dark, 0.5); |  | ||||||
|   border-bottom: 1px solid rgba($c-dark, 0.5); |  | ||||||
|  |  | ||||||
|   td { |  | ||||||
|     // border-left: 1px solid $c-dark; |  | ||||||
|     word-break: break-all; |  | ||||||
|     padding: 0.5rem 0.15rem; |  | ||||||
|     width: 100%; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| @import "./src/scss/elements"; |  | ||||||
|  |  | ||||||
| .title { |  | ||||||
|   margin: 0; |  | ||||||
|   font-weight: 400; |  | ||||||
|   text-transform: uppercase; |  | ||||||
|   text-align: center; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: $c-green; |  | ||||||
|   padding-bottom: 20px; |  | ||||||
|   @include tablet-min{ |  | ||||||
|     font-size: 16px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table { |  | ||||||
|   border-collapse: collapse; |  | ||||||
|   width: 100%; |  | ||||||
|   table-layout: fixed; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .table__content, .table__header { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 0; |  | ||||||
|   margin: 0 1rem; |  | ||||||
|   border-left: 1px solid rgba($c-dark, 0.8); |  | ||||||
|   border-right: 1px solid rgba($c-dark, 0.8); |  | ||||||
|   border-bottom: 1px solid rgba($c-dark, 0.8); |  | ||||||
|  |  | ||||||
|   th, td { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: column; |  | ||||||
|     flex-basis: 100%; |  | ||||||
|  |  | ||||||
|     padding: 0.4rem; |  | ||||||
|  |  | ||||||
|     white-space: nowrap; |  | ||||||
|     text-overflow: ellipsis; |  | ||||||
|     overflow: hidden; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   th:first-child, td:first-child { |  | ||||||
|     flex: 1; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   th:not(:first-child), td:not(:first-child) { |  | ||||||
|     flex: 0.2; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   th:nth-child(2), td:nth-child(2) { |  | ||||||
|     flex: 0.1; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile-only { |  | ||||||
|     th:first-child, td:first-child { |  | ||||||
|       display: none; |  | ||||||
|  |  | ||||||
|       &.show { |  | ||||||
|         display: block; |  | ||||||
|         align: flex-end; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     th:not(:first-child), td:not(:first-child) { |  | ||||||
|       flex: 1; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .table__content { |  | ||||||
|   td:not(:last-child) { |  | ||||||
|     border-right: 1px solid rgba($c-dark, 0.8); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .table__content:last-child { |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
|  |  | ||||||
|   border-bottom-left-radius: 3px; |  | ||||||
|   border-bottom-right-radius: 3px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .table__header { |  | ||||||
|   background-color: white; |  | ||||||
|   color: $c-dark; |  | ||||||
|   text-transform: uppercase; |  | ||||||
|   cursor: pointer; |  | ||||||
|  |  | ||||||
|   border-top: 1px solid rgba($c-dark, 0.8); |  | ||||||
|   border-top-left-radius: 3px; |  | ||||||
|   border-top-right-radius: 3px; |  | ||||||
|  |  | ||||||
|   th { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: row; |  | ||||||
|     font-weight: 400; |  | ||||||
|     letter-spacing: 0.7px; |  | ||||||
|     // font-size: 1.08rem; |  | ||||||
|     font-size: 15px; |  | ||||||
|  |  | ||||||
|     &::before { |  | ||||||
|       content: ''; |  | ||||||
|       min-width: 0.2rem; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     span:first-child { |  | ||||||
|       margin-right: 0.6rem; |  | ||||||
|     } |  | ||||||
|     span:nth-child(2) { |  | ||||||
|       margin-right: 0.1rem; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   th:not(:last-child) { |  | ||||||
|     border-right: 1px solid rgba($c-dark, 0.8); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .download { |  | ||||||
|  |  | ||||||
|   &__icon { |  | ||||||
|     fill: rgba($c-dark, 0.6); |  | ||||||
|     height: 1.2rem; |  | ||||||
|  |  | ||||||
|     &:hover { |  | ||||||
|       fill: $c-dark; |  | ||||||
|       cursor: pointer; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.active &__icon { |  | ||||||
|     fill: $c-green; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .torrentloader{ |  | ||||||
|   animation: load 1s linear infinite; |  | ||||||
|   border: 2px solid $c-dark; |  | ||||||
|   border-radius: 50%; |  | ||||||
|   display: block; |  | ||||||
|   height: 30px; |  | ||||||
|   left: 50%; |  | ||||||
|   margin: 2rem auto; |  | ||||||
|   width: 30px; |  | ||||||
|   &:after { |  | ||||||
|     border: 5px solid $c-green; |  | ||||||
|     border-radius: 50%; |  | ||||||
|     content: ''; |  | ||||||
|     left: 10px; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 16px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @keyframes load { |  | ||||||
|   100% { transform: rotate(360deg); } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										265
									
								
								src/components/header/AutocompleteDropdown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,265 @@ | |||||||
|  | <template> | ||||||
|  |   <transition name="shut"> | ||||||
|  |     <ul class="dropdown"> | ||||||
|  |       <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events --> | ||||||
|  |       <li | ||||||
|  |         v-for="(result, _index) in searchResults" | ||||||
|  |         :key="`${_index}-${result.title}-${result.type}`" | ||||||
|  |         :class="`result di-${_index} ${_index === index ? 'active' : ''}`" | ||||||
|  |         @click="openPopup(result)" | ||||||
|  |       > | ||||||
|  |         <IconMovie v-if="result.type == 'movie'" class="type-icon" /> | ||||||
|  |         <IconShow v-if="result.type == 'show'" class="type-icon" /> | ||||||
|  |         <span class="title">{{ result.title }}</span> | ||||||
|  |       </li> | ||||||
|  |  | ||||||
|  |       <li | ||||||
|  |         v-if="searchResults.length" | ||||||
|  |         :class="`info di-${searchResults.length}`" | ||||||
|  |       > | ||||||
|  |         <span> Select from list or press enter to search </span> | ||||||
|  |       </li> | ||||||
|  |     </ul> | ||||||
|  |   </transition> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, watch, defineProps } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import IconMovie from "@/icons/IconMovie.vue"; | ||||||
|  |   import IconShow from "@/icons/IconShow.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { elasticSearchMoviesAndShows } from "../../api"; | ||||||
|  |   import { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |   import { Index } from "../../interfaces/IAutocompleteSearch"; | ||||||
|  |   import type { | ||||||
|  |     IAutocompleteResult, | ||||||
|  |     IAutocompleteSearchResults | ||||||
|  |   } from "../../interfaces/IAutocompleteSearch"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     query?: string; | ||||||
|  |     index?: number; | ||||||
|  |     results?: Array<IAutocompleteResult>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   interface Emit { | ||||||
|  |     (e: "update:results", value: Array<IAutocompleteResult>); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const numberOfResults = 10; | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const emit = defineEmits<Emit>(); | ||||||
|  |   const store = useStore(); | ||||||
|  |  | ||||||
|  |   const searchResults: Ref<Array<IAutocompleteResult>> = ref([]); | ||||||
|  |   const keyboardNavigationIndex: Ref<number> = ref(0); | ||||||
|  |  | ||||||
|  |   watch( | ||||||
|  |     () => props.query, | ||||||
|  |     newQuery => { | ||||||
|  |       if (newQuery?.length > 0) | ||||||
|  |         fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */ | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   function openPopup(result) { | ||||||
|  |     if (!result.id || !result.type) return; | ||||||
|  |  | ||||||
|  |     store.dispatch("popup/open", { ...result }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function removeDuplicates(_searchResults) { | ||||||
|  |     const filteredResults = []; | ||||||
|  |     _searchResults.forEach(result => { | ||||||
|  |       if (result === undefined) return; | ||||||
|  |       const numberOfDuplicates = filteredResults.filter( | ||||||
|  |         filterItem => filterItem.id === result.id | ||||||
|  |       ); | ||||||
|  |       if (numberOfDuplicates.length >= 1) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       filteredResults.push(result); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return filteredResults; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function elasticIndexToMediaType(index: Index): MediaTypes { | ||||||
|  |     if (index === Index.Movies) return MediaTypes.Movie; | ||||||
|  |     if (index === Index.Shows) return MediaTypes.Show; | ||||||
|  |  | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function parseElasticResponse(elasticResponse: IAutocompleteSearchResults) { | ||||||
|  |     const data = elasticResponse.hits.hits; | ||||||
|  |  | ||||||
|  |     const results: Array<IAutocompleteResult> = []; | ||||||
|  |  | ||||||
|  |     data.forEach(item => { | ||||||
|  |       if (!Object.values(Index).includes(item._index)) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       results.push({ | ||||||
|  |         title: item._source?.original_name || item._source.original_title, | ||||||
|  |         id: item._source.id, | ||||||
|  |         adult: item._source.adult, | ||||||
|  |         type: elasticIndexToMediaType(item._index) | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return removeDuplicates(results).map((el, index) => { | ||||||
|  |       return { ...el, index }; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fetchAutocompleteResults() { | ||||||
|  |     keyboardNavigationIndex.value = 0; | ||||||
|  |     searchResults.value = []; | ||||||
|  |  | ||||||
|  |     elasticSearchMoviesAndShows(props.query, numberOfResults) | ||||||
|  |       .then(elasticResponse => parseElasticResponse(elasticResponse)) | ||||||
|  |       .then(_searchResults => { | ||||||
|  |         emit("update:results", _searchResults); | ||||||
|  |         searchResults.value = _searchResults; | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // on load functions | ||||||
|  |   fetchAutocompleteResults(); | ||||||
|  |   // end on load functions | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |   $sizes: 22; | ||||||
|  |  | ||||||
|  |   @for $i from 0 through $sizes { | ||||||
|  |     .dropdown .di-#{$i} { | ||||||
|  |       visibility: visible; | ||||||
|  |       transform-origin: top center; | ||||||
|  |       animation: scaleZ 200ms calc(50ms * #{$i}) ease-in forwards; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @keyframes scaleZ { | ||||||
|  |     0% { | ||||||
|  |       opacity: 0; | ||||||
|  |       transform: scale(0); | ||||||
|  |     } | ||||||
|  |     80% { | ||||||
|  |       transform: scale(1.07); | ||||||
|  |     } | ||||||
|  |     100% { | ||||||
|  |       opacity: 1; | ||||||
|  |       transform: scale(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .dropdown { | ||||||
|  |     top: var(--header-size); | ||||||
|  |     position: relative; | ||||||
|  |     height: 100%; | ||||||
|  |     width: 100%; | ||||||
|  |     max-width: 720px; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     z-index: 5; | ||||||
|  |  | ||||||
|  |     margin-top: -1px; | ||||||
|  |     border-top: none; | ||||||
|  |     padding: 0; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       position: fixed; | ||||||
|  |       left: 0; | ||||||
|  |       max-width: 100vw; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       top: unset; | ||||||
|  |       --gutter: 1.5rem; | ||||||
|  |       max-width: calc(100% - (2 * var(--gutter))); | ||||||
|  |       margin: -1px var(--gutter) 0 var(--gutter); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include desktop { | ||||||
|  |       max-width: 720px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   li.result { | ||||||
|  |     background-color: var(--background-95); | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     padding: 0.5rem 2rem; | ||||||
|  |     list-style: none; | ||||||
|  |  | ||||||
|  |     opacity: 0; | ||||||
|  |     height: 56px; | ||||||
|  |     width: 100%; | ||||||
|  |     visibility: hidden; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     padding: 0.5rem 2rem; | ||||||
|  |  | ||||||
|  |     font-size: 1.4rem; | ||||||
|  |     text-transform: capitalize; | ||||||
|  |     list-style: none; | ||||||
|  |     cursor: pointer; | ||||||
|  |     white-space: nowrap; | ||||||
|  |  | ||||||
|  |     transition: color 0.1s ease, fill 0.4s ease; | ||||||
|  |  | ||||||
|  |     span { | ||||||
|  |       overflow-x: hidden; | ||||||
|  |       text-overflow: ellipsis; | ||||||
|  |       transition: inherit; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.active, | ||||||
|  |     &:hover, | ||||||
|  |     &:active { | ||||||
|  |       color: var(--text-color); | ||||||
|  |       border-bottom: 2px solid var(--color-green); | ||||||
|  |  | ||||||
|  |       .type-icon { | ||||||
|  |         fill: var(--text-color); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .type-icon { | ||||||
|  |       width: 28px; | ||||||
|  |       height: 28px; | ||||||
|  |       margin-right: 1rem; | ||||||
|  |       transition: inherit; | ||||||
|  |       fill: var(--text-color-50); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   li.info { | ||||||
|  |     visibility: hidden; | ||||||
|  |     opacity: 0; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     padding: 0 1rem; | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     background-color: var(--background-95); | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     font-size: 0.6rem; | ||||||
|  |     height: 16px; | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .shut-leave-to { | ||||||
|  |     height: 0px; | ||||||
|  |     transition: height 0.4s ease; | ||||||
|  |     flex-wrap: no-wrap; | ||||||
|  |     overflow: hidden; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										125
									
								
								src/components/header/NavigationHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,125 @@ | |||||||
|  | <template> | ||||||
|  |   <nav> | ||||||
|  |     <!-- eslint-disable-next-line vuejs-accessibility/anchor-has-content --> | ||||||
|  |     <a v-if="isHome" class="nav__logo" href="/"> | ||||||
|  |       <TmdbLogo class="logo" /> | ||||||
|  |     </a> | ||||||
|  |  | ||||||
|  |     <router-link v-else class="nav__logo" to="/" exact> | ||||||
|  |       <TmdbLogo class="logo" /> | ||||||
|  |     </router-link> | ||||||
|  |  | ||||||
|  |     <SearchInput /> | ||||||
|  |  | ||||||
|  |     <Hamburger class="mobile-only" /> | ||||||
|  |     <NavigationIcon class="desktop-only" :route="profileRoute" /> | ||||||
|  |  | ||||||
|  |     <!-- <div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }"> --> | ||||||
|  |     <div v-if="isOpen" class="navigation-icons-grid mobile-only"> | ||||||
|  |       <NavigationIcons> | ||||||
|  |         <NavigationIcon :route="profileRoute" /> | ||||||
|  |       </NavigationIcons> | ||||||
|  |     </div> | ||||||
|  |   </nav> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { computed } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import { useRoute } from "vue-router"; | ||||||
|  |   import SearchInput from "@/components/header/SearchInput.vue"; | ||||||
|  |   import Hamburger from "@/components/ui/Hamburger.vue"; | ||||||
|  |   import NavigationIcons from "@/components/header/NavigationIcons.vue"; | ||||||
|  |   import NavigationIcon from "@/components/header/NavigationIcon.vue"; | ||||||
|  |   import TmdbLogo from "@/icons/tmdb-logo.vue"; | ||||||
|  |   import IconProfile from "@/icons/IconProfile.vue"; | ||||||
|  |   import IconProfileLock from "@/icons/IconProfileLock.vue"; | ||||||
|  |   import type INavigationIcon from "../../interfaces/INavigationIcon"; | ||||||
|  |  | ||||||
|  |   const route = useRoute(); | ||||||
|  |   const store = useStore(); | ||||||
|  |  | ||||||
|  |   const signinNavigationIcon: INavigationIcon = { | ||||||
|  |     title: "Signin", | ||||||
|  |     route: "/signin", | ||||||
|  |     icon: IconProfileLock | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const profileNavigationIcon: INavigationIcon = { | ||||||
|  |     title: "Profile", | ||||||
|  |     route: "/profile", | ||||||
|  |     icon: IconProfile | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const isHome = computed(() => route.path === "/"); | ||||||
|  |   const isOpen = computed(() => store.getters["hamburger/isOpen"]); | ||||||
|  |   const loggedIn = computed(() => store.getters["user/loggedIn"]); | ||||||
|  |  | ||||||
|  |   const profileRoute = computed(() => | ||||||
|  |     !loggedIn.value ? signinNavigationIcon : profileNavigationIcon | ||||||
|  |   ); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   .spacer { | ||||||
|  |     @include mobile-only { | ||||||
|  |       width: 100%; | ||||||
|  |       height: $header-size; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   nav { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: var(--header-size) 1fr var(--header-size); | ||||||
|  |  | ||||||
|  |     > * { | ||||||
|  |       z-index: 10; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .nav__logo { | ||||||
|  |     overflow: hidden; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .logo { | ||||||
|  |     padding: 1rem; | ||||||
|  |     fill: var(--color-green); | ||||||
|  |     width: var(--header-size); | ||||||
|  |     height: var(--header-size); | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     background: $background-nav-logo; | ||||||
|  |     transition: transform 0.3s ease; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       transform: scale(1.08); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       padding: 0.5rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .navigation-icons-grid { | ||||||
|  |     display: flex; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     position: fixed; | ||||||
|  |     top: var(--header-size); | ||||||
|  |     left: 0; | ||||||
|  |     width: 100%; | ||||||
|  |     background-color: $background-95; | ||||||
|  |     visibility: hidden; | ||||||
|  |     opacity: 0; | ||||||
|  |     transition: opacity 0.4s ease; | ||||||
|  |  | ||||||
|  |     opacity: 1; | ||||||
|  |     visibility: visible; | ||||||
|  |  | ||||||
|  |     &.open { | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										97
									
								
								src/components/header/NavigationIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,97 @@ | |||||||
|  | <template> | ||||||
|  |   <router-link | ||||||
|  |     v-if="route?.requiresAuth == undefined || (route?.requiresAuth && loggedIn)" | ||||||
|  |     :key="route?.title" | ||||||
|  |     :to="{ path: route?.route }" | ||||||
|  |   > | ||||||
|  |     <li | ||||||
|  |       class="navigation-link" | ||||||
|  |       :class="{ active: matchesActiveRoute(), 'nofill-stroke': useStroke }" | ||||||
|  |     > | ||||||
|  |       <component :is="route.icon" class="navigation-icon"></component> | ||||||
|  |       <span>{{ route.title }}</span> | ||||||
|  |     </li> | ||||||
|  |   </router-link> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import { computed, defineProps } from "vue"; | ||||||
|  |   import type INavigationIcon from "../../interfaces/INavigationIcon"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     route: INavigationIcon; | ||||||
|  |     active?: string; | ||||||
|  |     useStroke?: boolean; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |  | ||||||
|  |   const store = useStore(); | ||||||
|  |   const loggedIn = computed(() => store.getters["user/loggedIn"]); | ||||||
|  |  | ||||||
|  |   function matchesActiveRoute() { | ||||||
|  |     const currentRoute = props.route.title.toLowerCase(); | ||||||
|  |     return props?.active?.includes(currentRoute); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   .navigation-link { | ||||||
|  |     display: grid; | ||||||
|  |     place-items: center; | ||||||
|  |     min-height: var(--header-size); | ||||||
|  |     list-style: none; | ||||||
|  |     padding: 1rem 0.15rem; | ||||||
|  |     text-align: center; | ||||||
|  |     background-color: var(--background-color-secondary); | ||||||
|  |     transition: transform 0.3s ease, color 0.3s ease, stoke 0.3s ease, | ||||||
|  |       fill 0.3s ease, background-color 0.5s ease; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       transform: scale(1.05); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover, | ||||||
|  |     &.active { | ||||||
|  |       background-color: var(--background-color); | ||||||
|  |  | ||||||
|  |       span, | ||||||
|  |       .navigation-icon { | ||||||
|  |         color: var(--text-color); | ||||||
|  |         fill: var(--text-color); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     span { | ||||||
|  |       text-transform: uppercase; | ||||||
|  |       font-size: 11px; | ||||||
|  |       margin-top: 0.25rem; | ||||||
|  |       color: var(--text-color-70); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.nofill-stroke { | ||||||
|  |       .navigation-icon { | ||||||
|  |         stroke: var(--text-color-70); | ||||||
|  |         fill: none !important; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &:hover .navigation-icon, | ||||||
|  |       &.active .navigation-icon { | ||||||
|  |         stroke: var(--text-color); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   a { | ||||||
|  |     text-decoration: none; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .navigation-icon { | ||||||
|  |     width: 28px; | ||||||
|  |     fill: var(--text-color-70); | ||||||
|  |     transition: inherit; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										99
									
								
								src/components/header/NavigationIcons.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,99 @@ | |||||||
|  | <template> | ||||||
|  |   <ul class="navigation-icons"> | ||||||
|  |     <NavigationIcon | ||||||
|  |       v-for="_route in routes" | ||||||
|  |       :key="_route.route" | ||||||
|  |       :route="_route" | ||||||
|  |       :active="activeRoute" | ||||||
|  |       :use-stroke="_route?.useStroke" | ||||||
|  |     /> | ||||||
|  |     <slot></slot> | ||||||
|  |   </ul> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, watch } from "vue"; | ||||||
|  |   import { useRoute } from "vue-router"; | ||||||
|  |   import NavigationIcon from "@/components/header/NavigationIcon.vue"; | ||||||
|  |   import IconInbox from "@/icons/IconInbox.vue"; | ||||||
|  |   import IconNowPlaying from "@/icons/IconNowPlaying.vue"; | ||||||
|  |   import IconPopular from "@/icons/IconPopular.vue"; | ||||||
|  |   import IconUpcoming from "@/icons/IconUpcoming.vue"; | ||||||
|  |   import IconSettings from "@/icons/IconSettings.vue"; | ||||||
|  |   import IconActivity from "@/icons/IconActivity.vue"; | ||||||
|  |   import IconBinoculars from "@/icons/IconBinoculars.vue"; | ||||||
|  |   import type INavigationIcon from "../../interfaces/INavigationIcon"; | ||||||
|  |  | ||||||
|  |   const route = useRoute(); | ||||||
|  |   const activeRoute = ref(window?.location?.pathname); | ||||||
|  |   const routes: INavigationIcon[] = [ | ||||||
|  |     { | ||||||
|  |       title: "Requests", | ||||||
|  |       route: "/list/requests", | ||||||
|  |       icon: IconInbox | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: "Now Playing", | ||||||
|  |       route: "/list/now_playing", | ||||||
|  |       icon: IconNowPlaying | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: "Popular", | ||||||
|  |       route: "/list/popular", | ||||||
|  |       icon: IconPopular | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: "Upcoming", | ||||||
|  |       route: "/list/upcoming", | ||||||
|  |       icon: IconUpcoming | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: "Activity", | ||||||
|  |       route: "/activity", | ||||||
|  |       requiresAuth: true, | ||||||
|  |       useStroke: true, | ||||||
|  |       icon: IconActivity | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: "Torrents", | ||||||
|  |       route: "/torrents", | ||||||
|  |       requiresAuth: true, | ||||||
|  |       icon: IconBinoculars | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: "Settings", | ||||||
|  |       route: "/settings", | ||||||
|  |       requiresAuth: true, | ||||||
|  |       useStroke: true, | ||||||
|  |       icon: IconSettings | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   function setActiveRoute(_route: string) { | ||||||
|  |     activeRoute.value = _route; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   watch(route, () => setActiveRoute(window?.location?.pathname || "")); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   .navigation-icons { | ||||||
|  |     display: grid; | ||||||
|  |     grid-column: 1fr; | ||||||
|  |     padding-left: 0; | ||||||
|  |     margin: 0; | ||||||
|  |     background-color: var(--background-color-secondary); | ||||||
|  |     z-index: 15; | ||||||
|  |     width: 100%; | ||||||
|  |  | ||||||
|  |     @include desktop { | ||||||
|  |       grid-template-rows: var(--header-size); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       grid-template-columns: 1fr 1fr; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										290
									
								
								src/components/header/SearchInput.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,290 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div class="search" :class="{ active: inputIsActive }"> | ||||||
|  |       <IconSearch class="search-icon" tabindex="-1" /> | ||||||
|  |  | ||||||
|  |       <!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label --> | ||||||
|  |       <input | ||||||
|  |         ref="inputElement" | ||||||
|  |         v-model="query" | ||||||
|  |         type="text" | ||||||
|  |         placeholder="Search for movie or show" | ||||||
|  |         aria-label="Search input for finding a movie or show" | ||||||
|  |         autocorrect="off" | ||||||
|  |         autocapitalize="off" | ||||||
|  |         tabindex="0" | ||||||
|  |         @input="handleInput" | ||||||
|  |         @click="focus" | ||||||
|  |         @keydown.escape="handleEscape" | ||||||
|  |         @keyup.enter="handleSubmit" | ||||||
|  |         @keydown.up="navigateUp" | ||||||
|  |         @keydown.down="navigateDown" | ||||||
|  |         @focus="focus" | ||||||
|  |         @blur="blur" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <IconClose | ||||||
|  |         v-if="query && query.length" | ||||||
|  |         tabindex="0" | ||||||
|  |         aria-label="button" | ||||||
|  |         class="close-icon" | ||||||
|  |         @click="clearInput" | ||||||
|  |         @keydown.enter.stop="clearInput" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <AutocompleteDropdown | ||||||
|  |       v-if="showAutocompleteResults" | ||||||
|  |       v-model:results="dropdownResults" | ||||||
|  |       :query="query" | ||||||
|  |       :index="dropdownIndex" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, computed } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import { useRouter, useRoute } from "vue-router"; | ||||||
|  |   import AutocompleteDropdown from "@/components/header/AutocompleteDropdown.vue"; | ||||||
|  |   import IconSearch from "@/icons/IconSearch.vue"; | ||||||
|  |   import IconClose from "@/icons/IconClose.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import type { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   interface ISearchResult { | ||||||
|  |     title: string; | ||||||
|  |     id: number; | ||||||
|  |     adult: boolean; | ||||||
|  |     type: MediaTypes; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const store = useStore(); | ||||||
|  |   const router = useRouter(); | ||||||
|  |   const route = useRoute(); | ||||||
|  |  | ||||||
|  |   const query: Ref<string> = ref(null); | ||||||
|  |   const disabled: Ref<boolean> = ref(false); | ||||||
|  |   const dropdownIndex: Ref<number> = ref(-1); | ||||||
|  |   const dropdownResults: Ref<ISearchResult[]> = ref([]); | ||||||
|  |   const inputIsActive: Ref<boolean> = ref(false); | ||||||
|  |   const inputElement: Ref<HTMLInputElement> = ref(null); | ||||||
|  |  | ||||||
|  |   const isOpen = computed(() => store.getters["popup/isOpen"]); | ||||||
|  |   const showAutocompleteResults = computed(() => { | ||||||
|  |     return ( | ||||||
|  |       !disabled.value && | ||||||
|  |       inputIsActive.value && | ||||||
|  |       query.value && | ||||||
|  |       query.value.length > 0 | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const params = new URLSearchParams(window.location.search); | ||||||
|  |   if (params && params.has("query")) { | ||||||
|  |     query.value = decodeURIComponent(params.get("query")); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const { ELASTIC } = process.env; | ||||||
|  |   if (ELASTIC === undefined || ELASTIC === "") { | ||||||
|  |     disabled.value = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function navigateDown() { | ||||||
|  |     if (dropdownIndex.value < dropdownResults.value.length - 1) { | ||||||
|  |       dropdownIndex.value += 1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function navigateUp() { | ||||||
|  |     if (dropdownIndex.value > -1) dropdownIndex.value -= 1; | ||||||
|  |  | ||||||
|  |     const textLength = inputElement.value.value.length; | ||||||
|  |  | ||||||
|  |     setTimeout(() => { | ||||||
|  |       inputElement.value.focus(); | ||||||
|  |       inputElement.value.setSelectionRange(textLength, textLength + 1); | ||||||
|  |     }, 1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function search() { | ||||||
|  |     const encodedQuery = encodeURI(query.value.replace("/ /g", "+")); | ||||||
|  |  | ||||||
|  |     router.push({ | ||||||
|  |       name: "search", | ||||||
|  |       query: { | ||||||
|  |         ...route.query, | ||||||
|  |         query: encodedQuery | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleInput() { | ||||||
|  |     dropdownIndex.value = -1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function focus() { | ||||||
|  |     inputIsActive.value = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function reset() { | ||||||
|  |     inputElement.value.blur(); | ||||||
|  |     dropdownIndex.value = -1; | ||||||
|  |     inputIsActive.value = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function blur() { | ||||||
|  |     return setTimeout(reset, 150); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function clearInput() { | ||||||
|  |     query.value = ""; | ||||||
|  |     inputElement.value.focus(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleSubmit() { | ||||||
|  |     if (!query.value || query.value.length === 0) return; | ||||||
|  |  | ||||||
|  |     if (dropdownIndex.value >= 0) { | ||||||
|  |       const resultItem = dropdownResults.value[dropdownIndex.value]; | ||||||
|  |  | ||||||
|  |       store.dispatch("popup/open", { | ||||||
|  |         id: resultItem?.id, | ||||||
|  |         type: resultItem?.type | ||||||
|  |       }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     search(); | ||||||
|  |     reset(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleEscape() { | ||||||
|  |     if (!isOpen.value) reset(); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |  | ||||||
|  |   .close-icon { | ||||||
|  |     position: absolute; | ||||||
|  |     top: calc(50% - 12px); | ||||||
|  |     right: 0; | ||||||
|  |     cursor: pointer; | ||||||
|  |     fill: var(--text-color); | ||||||
|  |     height: 24px; | ||||||
|  |     width: 24px; | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       right: 6px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .filter { | ||||||
|  |     width: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     margin: 1rem 2rem; | ||||||
|  |  | ||||||
|  |     h2 { | ||||||
|  |       margin-top: 0.5rem; | ||||||
|  |       margin-bottom: 0.5rem; | ||||||
|  |       font-weight: 400; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &-items { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: row; | ||||||
|  |       align-items: center; | ||||||
|  |  | ||||||
|  |       > :not(:first-child) { | ||||||
|  |         margin-left: 1rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   hr { | ||||||
|  |     display: block; | ||||||
|  |     height: 1px; | ||||||
|  |     border: 0; | ||||||
|  |     border-bottom: 1px solid $text-color-50; | ||||||
|  |     margin-top: 10px; | ||||||
|  |     margin-bottom: 10px; | ||||||
|  |     width: 90%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .search.active { | ||||||
|  |     input { | ||||||
|  |       border-color: var(--color-green); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .search-icon { | ||||||
|  |       fill: var(--color-green); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .search { | ||||||
|  |     height: $header-size; | ||||||
|  |     display: flex; | ||||||
|  |     position: fixed; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     z-index: 5; | ||||||
|  |     border: 0; | ||||||
|  |     background-color: $background-color-secondary; | ||||||
|  |  | ||||||
|  |     // TODO check if this is for mobile | ||||||
|  |     width: calc(100% - 110px); | ||||||
|  |     top: 0; | ||||||
|  |     right: 55px; | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       position: relative; | ||||||
|  |       width: 100%; | ||||||
|  |       right: 0px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     input { | ||||||
|  |       display: block; | ||||||
|  |       width: 100%; | ||||||
|  |       padding: 13px 28px 13px 45px; | ||||||
|  |       outline: none; | ||||||
|  |       margin: 0; | ||||||
|  |       border: 0; | ||||||
|  |       background-color: $background-color-secondary; | ||||||
|  |       font-weight: 300; | ||||||
|  |       font-size: 18px; | ||||||
|  |       color: $text-color; | ||||||
|  |       border-bottom: 1px solid transparent; | ||||||
|  |  | ||||||
|  |       &:focus { | ||||||
|  |         // border-bottom: 1px solid var(--color-green); | ||||||
|  |         border-color: var(--color-green); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         font-size: 24px; | ||||||
|  |         padding: 13px 40px 13px 60px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &-icon { | ||||||
|  |       width: 20px; | ||||||
|  |       height: 20px; | ||||||
|  |       fill: var(--text-color-50); | ||||||
|  |       pointer-events: none; | ||||||
|  |       position: absolute; | ||||||
|  |       left: 15px; | ||||||
|  |       top: calc(50% - 10px); | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         width: 24px; | ||||||
|  |         height: 24px; | ||||||
|  |         top: calc(50% - 12px); | ||||||
|  |         left: 22px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
| @@ -1,95 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="action"> |  | ||||||
|     <a class="action-link" :class="{'active': active}" @click="$emit('click')"> |  | ||||||
|       <svg class="action-icon"> |  | ||||||
|         <use v-if="active && iconRefActive" :xlink:href="iconRefActive"></use> |  | ||||||
|         <use v-else :xlink:href="iconRef"></use> |  | ||||||
|       </svg> |  | ||||||
|       <span class="action-text">{{ active && textActive ? textActive : text }}</span>  |  | ||||||
|     </a> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     iconRef: { |  | ||||||
|       type: String, |  | ||||||
|       required: true |  | ||||||
|     }, |  | ||||||
|     iconRefActive: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     active: { |  | ||||||
|       type: Boolean, |  | ||||||
|       default: false, |  | ||||||
|     }, |  | ||||||
|     text: { |  | ||||||
|       type: String, |  | ||||||
|       required: true |  | ||||||
|     }, |  | ||||||
|     textActive: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/loading-placeholder"; |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| .action { |  | ||||||
|   &-link { |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     text-decoration: none; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|     color: rgba($c-dark, 0.5); |  | ||||||
|     transition: color 0.5s ease; |  | ||||||
|     font-size: 11px; |  | ||||||
|     padding: 5px 0; |  | ||||||
|     border-bottom: 1px solid rgba($c-dark, 0.05); |  | ||||||
|     &:hover { |  | ||||||
|       color: rgba($c-dark, 0.75); |  | ||||||
|     } |  | ||||||
|     &.active { |  | ||||||
|       color: $c-dark; |  | ||||||
|     } |  | ||||||
|     &.pending { |  | ||||||
|       color: #f8bd2d; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &-icon { |  | ||||||
|     width: 18px; |  | ||||||
|     height: 18px; |  | ||||||
|     margin: 0 10px 0 0; |  | ||||||
|     fill: rgba($c-dark, 0.5); |  | ||||||
|     transition: fill 0.5s ease, transform 0.5s ease; |  | ||||||
|     &.waiting { |  | ||||||
|       transform: scale(0.8, 0.8); |  | ||||||
|     } |  | ||||||
|     &.pending { |  | ||||||
|       fill: #f8bd2d; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &-link:hover &-icon { |  | ||||||
|     fill: rgba($c-dark, 0.75); |  | ||||||
|     cursor: pointer; |  | ||||||
|   } |  | ||||||
|   &-link.active &-icon { |  | ||||||
|     fill: $c-green; |  | ||||||
|   } |  | ||||||
|   &-text { |  | ||||||
|     display: block; |  | ||||||
|     padding-top: 2px; |  | ||||||
|     cursor: pointer; |  | ||||||
|     margin:4.4px; |  | ||||||
|     margin-left: -3px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
							
								
								
									
										86
									
								
								src/components/popup/ActionButton.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,86 @@ | |||||||
|  | <template> | ||||||
|  |   <li | ||||||
|  |     class="sidebar-list-element" | ||||||
|  |     :class="{ active, disabled }" | ||||||
|  |     @click="emit('click')" | ||||||
|  |     @keydown.enter="emit('click')" | ||||||
|  |   > | ||||||
|  |     <slot></slot> | ||||||
|  |   </li> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps, defineEmits } from "vue"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     active?: boolean; | ||||||
|  |     disabled?: boolean; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   interface Emit { | ||||||
|  |     (e: "click"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const emit = defineEmits<Emit>(); | ||||||
|  |   defineProps<Props>(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   li.sidebar-list-element { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     text-decoration: none; | ||||||
|  |     text-transform: uppercase; | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     font-size: 12px; | ||||||
|  |     padding: 10px 0; | ||||||
|  |     border-bottom: 1px solid var(--text-color-5); | ||||||
|  |     cursor: pointer; | ||||||
|  |     user-select: none; | ||||||
|  |     -webkit-user-select: none; | ||||||
|  |  | ||||||
|  |     &:first-of-type { | ||||||
|  |       padding-top: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     div > svg, | ||||||
|  |     svg { | ||||||
|  |       width: 26px; | ||||||
|  |       height: 26px; | ||||||
|  |       margin-right: 1rem; | ||||||
|  |       transition: all 0.3s ease; | ||||||
|  |       fill: var(--text-color-70); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover, | ||||||
|  |     &.active { | ||||||
|  |       color: var(--text-color); | ||||||
|  |  | ||||||
|  |       div > svg, | ||||||
|  |       svg { | ||||||
|  |         fill: var(--text-color); | ||||||
|  |         transform: scale(1.1, 1.1); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.active > div > svg, | ||||||
|  |     &.active > svg { | ||||||
|  |       fill: var(--color-green); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.disabled { | ||||||
|  |       cursor: default; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .pending { | ||||||
|  |       color: #f8bd2d; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .meta { | ||||||
|  |       margin-left: auto; | ||||||
|  |       text-align: right; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										127
									
								
								src/components/popup/Description.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,127 @@ | |||||||
|  | <template> | ||||||
|  |   <div | ||||||
|  |     ref="descriptionElement" | ||||||
|  |     class="movie-description noselect" | ||||||
|  |     @click="overflow ? (truncated = !truncated) : null" | ||||||
|  |     @keydown.enter="overflow ? (truncated = !truncated) : null" | ||||||
|  |   > | ||||||
|  |     <span :class="{ truncated }">{{ description }}</span> | ||||||
|  |  | ||||||
|  |     <button v-if="description && overflow" class="truncate-toggle"> | ||||||
|  |       <IconArrowDown :class="{ rotate: !truncated }" /> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, defineProps, onMounted } from "vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import IconArrowDown from "../../icons/IconArrowDown.vue"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     description: string; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const truncated: Ref<boolean> = ref(true); | ||||||
|  |   const overflow: Ref<boolean> = ref(false); | ||||||
|  |   const descriptionElement: Ref<HTMLElement> = ref(null); | ||||||
|  |  | ||||||
|  |   // eslint-disable-next-line no-undef | ||||||
|  |   function removeElements(elems: NodeListOf<Element>) { | ||||||
|  |     elems.forEach(el => el.remove()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // The description element overflows text after 4 rows with css | ||||||
|  |   // line-clamp this takes the same text and adds to a temporary | ||||||
|  |   // element without css overflow. If the temp element is | ||||||
|  |   // higher then description element, we display expand button | ||||||
|  |   function checkDescriptionOverflowing() { | ||||||
|  |     const element = descriptionElement?.value; | ||||||
|  |     if (!element) return; | ||||||
|  |  | ||||||
|  |     const { height, width } = element.getBoundingClientRect(); | ||||||
|  |     const { fontSize, lineHeight } = getComputedStyle(element); | ||||||
|  |  | ||||||
|  |     const descriptionComparisonElement = document.createElement("div"); | ||||||
|  |     descriptionComparisonElement.setAttribute( | ||||||
|  |       "style", | ||||||
|  |       `max-width: ${Math.ceil( | ||||||
|  |         width + 10 | ||||||
|  |       )}px; display: block; font-size: ${fontSize}; line-height: ${lineHeight};` | ||||||
|  |     ); | ||||||
|  |     // Don't know why need to add 10px to width, but works out perfectly | ||||||
|  |  | ||||||
|  |     descriptionComparisonElement.classList.add("dummy-non-overflow"); | ||||||
|  |     descriptionComparisonElement.innerText = props.description; | ||||||
|  |  | ||||||
|  |     document.body.appendChild(descriptionComparisonElement); | ||||||
|  |     const elemWithoutOverflowHeight = | ||||||
|  |       descriptionComparisonElement.getBoundingClientRect().height; | ||||||
|  |  | ||||||
|  |     overflow.value = elemWithoutOverflowHeight > height; | ||||||
|  |     removeElements(document.querySelectorAll(".dummy-non-overflow")); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onMounted(checkDescriptionOverflowing); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   .movie-description { | ||||||
|  |     font-weight: 300; | ||||||
|  |     font-size: 13px; | ||||||
|  |     line-height: 1.8; | ||||||
|  |     margin-bottom: 20px; | ||||||
|  |     transition: all 1s ease; | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       margin-bottom: 30px; | ||||||
|  |       font-size: 14px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   span.truncated { | ||||||
|  |     display: -webkit-box; | ||||||
|  |     overflow: hidden; | ||||||
|  |     -webkit-line-clamp: 4; | ||||||
|  |     -webkit-box-orient: vertical; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .truncate-toggle { | ||||||
|  |     border: none; | ||||||
|  |     background: none; | ||||||
|  |     width: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     text-align: center; | ||||||
|  |     color: var(--text-color); | ||||||
|  |     margin-top: 1rem; | ||||||
|  |     cursor: pointer; | ||||||
|  |  | ||||||
|  |     svg { | ||||||
|  |       transition: 0.4s ease all; | ||||||
|  |       height: 22px; | ||||||
|  |       width: 22px; | ||||||
|  |       fill: var(--text-color); | ||||||
|  |  | ||||||
|  |       &.rotate { | ||||||
|  |         transform: rotateX(180deg); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &::before, | ||||||
|  |     &::after { | ||||||
|  |       content: ""; | ||||||
|  |       flex: 1; | ||||||
|  |       border-bottom: 1px solid var(--text-color-50); | ||||||
|  |     } | ||||||
|  |     &::before { | ||||||
|  |       margin-right: 1rem; | ||||||
|  |     } | ||||||
|  |     &::after { | ||||||
|  |       margin-left: 1rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										54
									
								
								src/components/popup/Detail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="movie-detail"> | ||||||
|  |     <h2 class="title">{{ title }}</h2> | ||||||
|  |     <span v-if="detail" class="info">{{ detail }}</span> | ||||||
|  |  | ||||||
|  |     <slot></slot> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps } from "vue"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     title: string; | ||||||
|  |     detail?: string | number; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   defineProps<Props>(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  |   .movie-detail { | ||||||
|  |     margin-bottom: 20px; | ||||||
|  |  | ||||||
|  |     &:last-of-type { | ||||||
|  |       margin-bottom: 0px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       margin-bottom: 30px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     h2.title { | ||||||
|  |       margin: 0; | ||||||
|  |       font-weight: 400; | ||||||
|  |       text-transform: uppercase; | ||||||
|  |       font-size: 1.2rem; | ||||||
|  |       color: var(--color-green); | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         font-size: 1.1rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     span.info { | ||||||
|  |       font-weight: 300; | ||||||
|  |       font-size: 1rem; | ||||||
|  |       letter-spacing: 0.8px; | ||||||
|  |       margin-top: 5px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										553
									
								
								src/components/popup/Movie.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,553 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="movie"> | ||||||
|  |     <!-- HEADER w/ POSTER --> | ||||||
|  |     <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events --> | ||||||
|  |     <header | ||||||
|  |       ref="backdropElement" | ||||||
|  |       :class="compact ? 'compact' : ''" | ||||||
|  |       @click="compact = !compact" | ||||||
|  |     > | ||||||
|  |       <figure class="movie__poster"> | ||||||
|  |         <img | ||||||
|  |           ref="poster-image" | ||||||
|  |           class="movie-item__img is-loaded" | ||||||
|  |           alt="Movie poster" | ||||||
|  |           :src="poster" | ||||||
|  |         /> | ||||||
|  |       </figure> | ||||||
|  |  | ||||||
|  |       <div v-if="media" class="movie__title"> | ||||||
|  |         <h1>{{ media.title }}</h1> | ||||||
|  |         <i>{{ media.tagline }}</i> | ||||||
|  |       </div> | ||||||
|  |       <loading-placeholder v-else :count="2" /> | ||||||
|  |     </header> | ||||||
|  |  | ||||||
|  |     <!-- Siderbar and movie info --> | ||||||
|  |     <div class="movie__main"> | ||||||
|  |       <div class="movie__wrap movie__wrap--main"> | ||||||
|  |         <!-- SIDEBAR ACTIONS --> | ||||||
|  |         <div v-if="media" class="movie__actions"> | ||||||
|  |           <action-button :active="media?.exists_in_plex" :disabled="true"> | ||||||
|  |             <IconThumbsUp v-if="media?.exists_in_plex" /> | ||||||
|  |             <IconThumbsDown v-else /> | ||||||
|  |             {{ | ||||||
|  |               !media?.exists_in_plex | ||||||
|  |                 ? "Not yet available" | ||||||
|  |                 : "Already available 🎉" | ||||||
|  |             }} | ||||||
|  |           </action-button> | ||||||
|  |  | ||||||
|  |           <action-button :active="requested" @click="sendRequest"> | ||||||
|  |             <transition name="fade" mode="out-in"> | ||||||
|  |               <div v-if="!requested" key="request"><IconRequest /></div> | ||||||
|  |               <div v-else key="requested"><IconRequested /></div> | ||||||
|  |             </transition> | ||||||
|  |             {{ !requested ? `Request ${type}?` : "Already requested" }} | ||||||
|  |           </action-button> | ||||||
|  |  | ||||||
|  |           <action-button | ||||||
|  |             v-if="plexUserId && media?.exists_in_plex" | ||||||
|  |             @click="openInPlex" | ||||||
|  |           > | ||||||
|  |             <IconPlay /> | ||||||
|  |             Open and watch in plex now! | ||||||
|  |           </action-button> | ||||||
|  |  | ||||||
|  |           <action-button | ||||||
|  |             v-if="cast?.length" | ||||||
|  |             :active="showCast" | ||||||
|  |             @click="() => (showCast = !showCast)" | ||||||
|  |           > | ||||||
|  |             <IconProfile class="icon" /> | ||||||
|  |             {{ showCast ? "Hide cast" : "Show cast" }} | ||||||
|  |           </action-button> | ||||||
|  |  | ||||||
|  |           <action-button | ||||||
|  |             v-if="admin === true" | ||||||
|  |             :active="showTorrents" | ||||||
|  |             @click="showTorrents = !showTorrents" | ||||||
|  |           > | ||||||
|  |             <IconBinoculars /> | ||||||
|  |             Search for torrents | ||||||
|  |             <span v-if="numberOfTorrentResults" class="meta">{{ | ||||||
|  |               numberOfTorrentResults | ||||||
|  |             }}</span> | ||||||
|  |           </action-button> | ||||||
|  |  | ||||||
|  |           <action-button @click="openTmdb"> | ||||||
|  |             <IconInfo /> | ||||||
|  |             See more info | ||||||
|  |           </action-button> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <!-- Loading placeholder --> | ||||||
|  |         <div v-else class="movie__actions text-input__loading"> | ||||||
|  |           <div | ||||||
|  |             v-for="index in admin ? Array(4) : Array(3)" | ||||||
|  |             :key="index" | ||||||
|  |             class="movie__actions-link" | ||||||
|  |           > | ||||||
|  |             <div | ||||||
|  |               class="movie__actions-text text-input__loading--line" | ||||||
|  |               style="margin: 9px; margin-left: -3px" | ||||||
|  |             ></div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <!-- MOVIE INFO --> | ||||||
|  |         <div class="movie__info"> | ||||||
|  |           <!-- Loading placeholder --> | ||||||
|  |           <div v-if="loading"> | ||||||
|  |             <loading-placeholder :count="5" /> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <Description | ||||||
|  |             v-if="!loading && media && media.overview" | ||||||
|  |             :description="media.overview" | ||||||
|  |           /> | ||||||
|  |  | ||||||
|  |           <div v-if="media" class="movie__details"> | ||||||
|  |             <Detail | ||||||
|  |               v-if="media.year" | ||||||
|  |               title="Release date" | ||||||
|  |               :detail="media.year" | ||||||
|  |             /> | ||||||
|  |             <Detail | ||||||
|  |               v-if="media.type === MediaTypes.Movie && media.rating" | ||||||
|  |               title="Rating" | ||||||
|  |               :detail="media.rating" | ||||||
|  |             /> | ||||||
|  |             <Detail | ||||||
|  |               v-if="media.type == MediaTypes.Show" | ||||||
|  |               title="Seasons" | ||||||
|  |               :detail="media.seasons" | ||||||
|  |             /> | ||||||
|  |             <Detail | ||||||
|  |               v-if="media.genres && media.genres.length" | ||||||
|  |               title="Genres" | ||||||
|  |               :detail="media.genres.join(', ')" | ||||||
|  |             /> | ||||||
|  |             <Detail | ||||||
|  |               v-if=" | ||||||
|  |                 media.production_status && | ||||||
|  |                 media.production_status !== 'Released' | ||||||
|  |               " | ||||||
|  |               title="Production status" | ||||||
|  |               :detail="media.production_status" | ||||||
|  |             /> | ||||||
|  |             <Detail | ||||||
|  |               v-if="media.runtime" | ||||||
|  |               title="Runtime" | ||||||
|  |               :detail="humanMinutes(media.runtime)" | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <!-- TODO: change this classname, this is general  --> | ||||||
|  |  | ||||||
|  |         <div v-if="showCast && cast?.length" class="movie__admin"> | ||||||
|  |           <Detail title="cast"> | ||||||
|  |             <CastList :cast="cast" /> | ||||||
|  |           </Detail> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <!-- TORRENT LIST --> | ||||||
|  |       <TorrentList | ||||||
|  |         v-if="media && admin && showTorrents" | ||||||
|  |         class="torrents" | ||||||
|  |         :query="media?.title" | ||||||
|  |         :tmdb-id="id" | ||||||
|  |       ></TorrentList> | ||||||
|  |     </div> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, computed, defineProps, onMounted } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |  | ||||||
|  |   // import img from "@/directives/v-image"; | ||||||
|  |   import IconProfile from "@/icons/IconProfile.vue"; | ||||||
|  |   import IconThumbsUp from "@/icons/IconThumbsUp.vue"; | ||||||
|  |   import IconThumbsDown from "@/icons/IconThumbsDown.vue"; | ||||||
|  |   import IconInfo from "@/icons/IconInfo.vue"; | ||||||
|  |   import IconRequest from "@/icons/IconRequest.vue"; | ||||||
|  |   import IconRequested from "@/icons/IconRequested.vue"; | ||||||
|  |   import IconBinoculars from "@/icons/IconBinoculars.vue"; | ||||||
|  |   import IconPlay from "@/icons/IconPlay.vue"; | ||||||
|  |   import TorrentList from "@/components/torrent/TruncatedTorrentResults.vue"; | ||||||
|  |   import CastList from "@/components/CastList.vue"; | ||||||
|  |   import Detail from "@/components/popup/Detail.vue"; | ||||||
|  |   import ActionButton from "@/components/popup/ActionButton.vue"; | ||||||
|  |   import Description from "@/components/popup/Description.vue"; | ||||||
|  |   import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import type { | ||||||
|  |     IMovie, | ||||||
|  |     IShow, | ||||||
|  |     IMediaCredits, | ||||||
|  |     ICast | ||||||
|  |   } from "../../interfaces/IList"; | ||||||
|  |   import type { | ||||||
|  |     IRequestStatusResponse, | ||||||
|  |     IRequestSubmitResponse | ||||||
|  |   } from "../../interfaces/IRequestResponse"; | ||||||
|  |   import { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   import { humanMinutes } from "../../utils"; | ||||||
|  |   import { | ||||||
|  |     getMovie, | ||||||
|  |     getShow, | ||||||
|  |     getMovieCredits, | ||||||
|  |     getShowCredits, | ||||||
|  |     request, | ||||||
|  |     getRequestStatus | ||||||
|  |     // watchLink | ||||||
|  |   } from "../../api"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     id: number; | ||||||
|  |     type: MediaTypes.Movie | MediaTypes.Show; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const ASSET_URL = "https://image.tmdb.org/t/p/"; | ||||||
|  |   const ASSET_SIZES = ["w500", "w780", "original"]; | ||||||
|  |  | ||||||
|  |   const media: Ref<IMovie | IShow> = ref(); | ||||||
|  |   const requested: Ref<boolean> = ref(); | ||||||
|  |   const showTorrents: Ref<boolean> = ref(); | ||||||
|  |   const showCast: Ref<boolean> = ref(); | ||||||
|  |   const cast: Ref<ICast[]> = ref([]); | ||||||
|  |   const compact: Ref<boolean> = ref(); | ||||||
|  |   const loading: Ref<boolean> = ref(); | ||||||
|  |   const backdropElement: Ref<HTMLElement> = ref(); | ||||||
|  |  | ||||||
|  |   const store = useStore(); | ||||||
|  |  | ||||||
|  |   const admin = computed(() => store.getters["user/admin"]); | ||||||
|  |   const plexUserId = computed(() => store.getters["user/plexUserId"]); | ||||||
|  |  | ||||||
|  |   const poster = computed(() => { | ||||||
|  |     if (!media.value) return "/assets/placeholder.png"; | ||||||
|  |     if (!media.value?.poster) return "/assets/no-image.svg"; | ||||||
|  |  | ||||||
|  |     return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const numberOfTorrentResults = computed(() => { | ||||||
|  |     const count = store.getters["torrentModule/resultCount"]; | ||||||
|  |     return count ? `${count} results` : null; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function setCast(_cast: ICast[]) { | ||||||
|  |     cast.value = _cast; | ||||||
|  |   } | ||||||
|  |   function setRequested( | ||||||
|  |     requestResponse: IRequestStatusResponse | IRequestSubmitResponse | ||||||
|  |   ) { | ||||||
|  |     if (requestResponse?.success) { | ||||||
|  |       requested.value = requestResponse?.success; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     requested.value = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setBackdrop(): void { | ||||||
|  |     if ( | ||||||
|  |       !media.value?.backdrop || | ||||||
|  |       !backdropElement.value?.style || | ||||||
|  |       backdropElement.value?.style?.backgroundImage !== "" | ||||||
|  |     ) | ||||||
|  |       return; | ||||||
|  |  | ||||||
|  |     const backdropURL = `${ASSET_URL}${ASSET_SIZES[1]}${media.value.backdrop}`; | ||||||
|  |     backdropElement.value.style.backgroundImage = `url(${backdropURL})`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getCredits( | ||||||
|  |     type: MediaTypes.Movie | MediaTypes.Show | ||||||
|  |   ): Promise<IMediaCredits> { | ||||||
|  |     if (type === MediaTypes.Movie) { | ||||||
|  |       return getMovieCredits(props.id); | ||||||
|  |     } | ||||||
|  |     if (type === MediaTypes.Show) { | ||||||
|  |       return getShowCredits(props.id); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return Promise.reject(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setAndReturnMedia(_media: IMovie | IShow) { | ||||||
|  |     media.value = _media; | ||||||
|  |     return _media; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fetchMedia() { | ||||||
|  |     if (!props.id || !props.type) { | ||||||
|  |       console.error("Unable to fetch media, requires id & type"); // eslint-disable-line no-console | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let apiFunction: typeof getMovie; | ||||||
|  |     let parameters: { | ||||||
|  |       checkExistance: boolean; | ||||||
|  |       credits: boolean; | ||||||
|  |       releaseDates?: boolean; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (props.type === MediaTypes.Movie) { | ||||||
|  |       apiFunction = getMovie; | ||||||
|  |       parameters = { checkExistance: true, credits: false }; | ||||||
|  |     } else if (props.type === MediaTypes.Show) { | ||||||
|  |       apiFunction = getShow; | ||||||
|  |       parameters = { checkExistance: true, credits: false }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     apiFunction(props.id, { ...parameters }) | ||||||
|  |       .then(setAndReturnMedia) | ||||||
|  |       .then(() => getCredits(props.type)) | ||||||
|  |       .then(credits => setCast(credits?.cast || [])) | ||||||
|  |       .then(() => getRequestStatus(props.id, props.type)) | ||||||
|  |       .then(requestResponse => setRequested(requestResponse)) | ||||||
|  |       .then(setBackdrop); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sendRequest() { | ||||||
|  |     request(props.id, props.type).then(requestResponse => | ||||||
|  |       setRequested(requestResponse) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function openInPlex(): boolean { | ||||||
|  |     // watchLink() | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function openTmdb() { | ||||||
|  |     const tmdbType = props.type === MediaTypes.Show ? "tv" : props.type; | ||||||
|  |     const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`; | ||||||
|  |     window.location.href = tmdbURL; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // On created functions | ||||||
|  |   fetchMedia(); | ||||||
|  |   store.dispatch("torrentModule/setResultCount", null); | ||||||
|  |   // End on create functions | ||||||
|  |  | ||||||
|  |   onMounted(setBackdrop); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/loading-placeholder"; | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |  | ||||||
|  |   header { | ||||||
|  |     $duration: 0.2s; | ||||||
|  |     transform: scaleY(1); | ||||||
|  |     transition: height $duration ease; | ||||||
|  |     transform-origin: top; | ||||||
|  |     position: relative; | ||||||
|  |     background-size: cover; | ||||||
|  |     background-repeat: no-repeat; | ||||||
|  |     background-position: 50% 50%; | ||||||
|  |     background-color: $background-color; | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 1fr 1fr; | ||||||
|  |     height: 350px; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       grid-template-columns: 1fr; | ||||||
|  |       height: 250px; | ||||||
|  |       place-items: center; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     * { | ||||||
|  |       z-index: 2; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &::before { | ||||||
|  |       content: ""; | ||||||
|  |       display: block; | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|  |       left: 0; | ||||||
|  |       z-index: 1; | ||||||
|  |       width: 100%; | ||||||
|  |       height: 100%; | ||||||
|  |       background: $background-dark-85; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       &.compact { | ||||||
|  |         height: 100px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .movie__poster { | ||||||
|  |     display: none; | ||||||
|  |  | ||||||
|  |     @include desktop { | ||||||
|  |       background: var(--background-color); | ||||||
|  |       height: auto; | ||||||
|  |       display: block; | ||||||
|  |       width: calc(100% - 80px); | ||||||
|  |       margin: 40px; | ||||||
|  |  | ||||||
|  |       > img { | ||||||
|  |         width: 100%; | ||||||
|  |         border-radius: 10px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .movie { | ||||||
|  |     &__wrap { | ||||||
|  |       &--header { | ||||||
|  |         align-items: center; | ||||||
|  |         height: 100%; | ||||||
|  |       } | ||||||
|  |       &--main { | ||||||
|  |         display: flex; | ||||||
|  |         flex-wrap: wrap; | ||||||
|  |         flex-direction: column; | ||||||
|  |         @include tablet-min { | ||||||
|  |           flex-direction: row; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         background-color: $background-color; | ||||||
|  |         color: $text-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__img { | ||||||
|  |       display: block; | ||||||
|  |       width: 100%; | ||||||
|  |       opacity: 0; | ||||||
|  |       transform: scale(0.97) translateZ(0); | ||||||
|  |       transition: opacity 0.5s ease, transform 0.5s ease; | ||||||
|  |  | ||||||
|  |       &.is-loaded { | ||||||
|  |         opacity: 1; | ||||||
|  |         transform: scale(1); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__title { | ||||||
|  |       position: relative; | ||||||
|  |       padding: 20px; | ||||||
|  |       text-align: center; | ||||||
|  |       width: 100%; | ||||||
|  |       height: fit-content; | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         text-align: left; | ||||||
|  |         padding: 140px 30px 0 40px; | ||||||
|  |       } | ||||||
|  |       h1 { | ||||||
|  |         color: var(--color-green); | ||||||
|  |         font-weight: 500; | ||||||
|  |         line-height: 1.4; | ||||||
|  |         font-size: 24px; | ||||||
|  |         margin-bottom: 0; | ||||||
|  |  | ||||||
|  |         @include tablet-min { | ||||||
|  |           font-size: 30px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       i { | ||||||
|  |         display: block; | ||||||
|  |         color: rgba(255, 255, 255, 0.8); | ||||||
|  |         margin-top: 1rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__actions { | ||||||
|  |       text-align: center; | ||||||
|  |       width: 100%; | ||||||
|  |       order: 2; | ||||||
|  |       padding: 20px; | ||||||
|  |       border-top: 1px solid $text-color-5; | ||||||
|  |       @include tablet-min { | ||||||
|  |         order: 1; | ||||||
|  |         width: 45%; | ||||||
|  |         padding: 185px 0 40px 40px; | ||||||
|  |         border-top: 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &__info { | ||||||
|  |       width: 100%; | ||||||
|  |       padding: 20px; | ||||||
|  |       order: 1; | ||||||
|  |       @include tablet-min { | ||||||
|  |         order: 2; | ||||||
|  |         padding: 40px; | ||||||
|  |         width: 55%; | ||||||
|  |         margin-left: 45%; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &__info { | ||||||
|  |       margin-left: 0; | ||||||
|  |     } | ||||||
|  |     &__details { | ||||||
|  |       display: flex; | ||||||
|  |       flex-wrap: wrap; | ||||||
|  |  | ||||||
|  |       > * { | ||||||
|  |         margin-right: 30px; | ||||||
|  |  | ||||||
|  |         @include mobile { | ||||||
|  |           margin-right: 20px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &__admin { | ||||||
|  |       width: 100%; | ||||||
|  |       padding: 20px; | ||||||
|  |       order: 2; | ||||||
|  |       @include tablet-min { | ||||||
|  |         order: 3; | ||||||
|  |         padding: 40px; | ||||||
|  |         padding-top: 0px; | ||||||
|  |         width: 100%; | ||||||
|  |       } | ||||||
|  |       &-title { | ||||||
|  |         margin: 0; | ||||||
|  |         font-weight: 400; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         text-align: center; | ||||||
|  |         font-size: 14px; | ||||||
|  |         color: $green; | ||||||
|  |         padding-bottom: 20px; | ||||||
|  |         @include tablet-min { | ||||||
|  |           font-size: 16px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .torrents { | ||||||
|  |       background-color: var(--background-color); | ||||||
|  |       padding: 0 1rem; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         padding: 0 0.5rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .fade-enter-active, | ||||||
|  |   .fade-leave-active { | ||||||
|  |     transition: opacity 0.4s; | ||||||
|  |   } | ||||||
|  |   .fade-enter, | ||||||
|  |   .fade-leave-to { | ||||||
|  |     opacity: 0; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										278
									
								
								src/components/popup/Person.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,278 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="person"> | ||||||
|  |     <header ref="header"> | ||||||
|  |       <div class="info"> | ||||||
|  |         <h1 v-if="person"> | ||||||
|  |           {{ person.name }} | ||||||
|  |         </h1> | ||||||
|  |         <div v-else> | ||||||
|  |           <loading-placeholder :count="1" /> | ||||||
|  |           <loading-placeholder :count="1" line-class="short" :top="3.5" /> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <span v-if="person && person['known_for_department']" class="known-for"> | ||||||
|  |           {{ | ||||||
|  |             person.known_for_department === "Acting" | ||||||
|  |               ? "Actor" | ||||||
|  |               : person.known_for_department | ||||||
|  |           }} | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <figure class="person__poster"> | ||||||
|  |         <img | ||||||
|  |           ref="poster-image" | ||||||
|  |           class="person-item__img is-loaded" | ||||||
|  |           alt="Image of person" | ||||||
|  |           :src="poster" | ||||||
|  |         /> | ||||||
|  |       </figure> | ||||||
|  |     </header> | ||||||
|  |  | ||||||
|  |     <div v-if="loading"> | ||||||
|  |       <loading-placeholder :count="6" /> | ||||||
|  |       <loading-placeholder line-class="short" :top="3" /> | ||||||
|  |       <loading-placeholder :count="6" line-class="fullwidth" /> | ||||||
|  |       <loading-placeholder line-class="short" :top="4.5" /> | ||||||
|  |       <loading-placeholder /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="person"> | ||||||
|  |       <Detail v-if="age" title="Age" :detail="age" /> | ||||||
|  |  | ||||||
|  |       <Detail | ||||||
|  |         v-if="person" | ||||||
|  |         title="Born" | ||||||
|  |         :detail="person.place_of_birth ? person.place_of_birth : '(Not found)'" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <Detail v-if="person.biography" title="Biography"> | ||||||
|  |         <Description :description="person.biography" /> | ||||||
|  |       </Detail> | ||||||
|  |  | ||||||
|  |       <Detail | ||||||
|  |         v-if="creditedShows.length" | ||||||
|  |         title="movies" | ||||||
|  |         :detail="`Credited in ${creditedMovies.length} movies`" | ||||||
|  |       > | ||||||
|  |         <CastList :cast="creditedMovies" /> | ||||||
|  |       </Detail> | ||||||
|  |  | ||||||
|  |       <Detail | ||||||
|  |         v-if="creditedShows.length" | ||||||
|  |         title="shows" | ||||||
|  |         :detail="`Credited in ${creditedShows.length} shows`" | ||||||
|  |       > | ||||||
|  |         <CastList :cast="creditedShows" /> | ||||||
|  |       </Detail> | ||||||
|  |     </div> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, computed, defineProps } from "vue"; | ||||||
|  |   import CastList from "@/components/CastList.vue"; | ||||||
|  |   import Detail from "@/components/popup/Detail.vue"; | ||||||
|  |   import Description from "@/components/popup/Description.vue"; | ||||||
|  |   import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue"; | ||||||
|  |   import type { Ref, ComputedRef } from "vue"; | ||||||
|  |   import { getPerson, getPersonCredits } from "../../api"; | ||||||
|  |   import type { | ||||||
|  |     IPerson, | ||||||
|  |     IPersonCredits, | ||||||
|  |     IMovie, | ||||||
|  |     IShow | ||||||
|  |   } from "../../interfaces/IList"; | ||||||
|  |   import { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     id: number; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const ASSET_URL = "https://image.tmdb.org/t/p/"; | ||||||
|  |   const ASSET_SIZES = ["w500", "w780", "original"]; | ||||||
|  |  | ||||||
|  |   const person: Ref<IPerson> = ref(); | ||||||
|  |   const credits: Ref<IPersonCredits> = ref(); | ||||||
|  |   const loading: Ref<boolean> = ref(false); | ||||||
|  |   const creditedMovies: Ref<Array<IMovie | IShow>> = ref([]); | ||||||
|  |   const creditedShows: Ref<Array<IMovie | IShow>> = ref([]); | ||||||
|  |  | ||||||
|  |   const poster: ComputedRef<string> = computed(() => { | ||||||
|  |     if (!person.value) return "/assets/placeholder.png"; | ||||||
|  |     if (!person.value?.poster) return "/assets/no-image.svg"; | ||||||
|  |  | ||||||
|  |     return `${ASSET_URL}${ASSET_SIZES[0]}${person.value.poster}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const age: ComputedRef<string> = computed(() => { | ||||||
|  |     if (!person.value?.birthday) return ""; | ||||||
|  |  | ||||||
|  |     const today = new Date().getFullYear(); | ||||||
|  |     const birthYear = new Date(person.value.birthday).getFullYear(); | ||||||
|  |     return `${today - birthYear} years old`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function setCredits(_credits: IPersonCredits) { | ||||||
|  |     credits.value = _credits; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setPerson(_person: IPerson) { | ||||||
|  |     person.value = _person; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sortPopularity(a: IMovie | IShow, b: IMovie | IShow): number { | ||||||
|  |     return a.popularity < b.popularity ? 1 : -1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function alreadyExists( | ||||||
|  |     item: IMovie | IShow, | ||||||
|  |     pos: number, | ||||||
|  |     self: Array<IMovie | IShow> | ||||||
|  |   ) { | ||||||
|  |     const names = self.map(_item => _item.title); | ||||||
|  |     return names.indexOf(item.title) === pos; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function personCreditedFrom(cast: Array<IMovie | IShow>): void { | ||||||
|  |     creditedMovies.value = cast | ||||||
|  |       .filter(credit => credit.type === MediaTypes.Movie) | ||||||
|  |       .filter(alreadyExists) | ||||||
|  |       .sort(sortPopularity); | ||||||
|  |  | ||||||
|  |     creditedShows.value = cast | ||||||
|  |       .filter(credit => credit.type === MediaTypes.Show) | ||||||
|  |       .filter(alreadyExists) | ||||||
|  |       .sort(sortPopularity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fetchPerson() { | ||||||
|  |     if (!props.id) { | ||||||
|  |       console.error("Unable to fetch person, missing id!"); // eslint-disable-line no-console | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getPerson(props.id) | ||||||
|  |       .then(setPerson) | ||||||
|  |       .then(() => getPersonCredits(person.value?.id)) | ||||||
|  |       .then(setCredits) | ||||||
|  |       .then(() => personCreditedFrom(credits.value?.cast)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // On create functions | ||||||
|  |   fetchPerson(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/loading-placeholder"; | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |  | ||||||
|  |   section.person { | ||||||
|  |     overflow: hidden; | ||||||
|  |     position: relative; | ||||||
|  |     padding: 40px; | ||||||
|  |     background-color: var(--background-color); | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       padding: 50px 20px 10px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:before { | ||||||
|  |       content: ""; | ||||||
|  |       display: block; | ||||||
|  |       position: absolute; | ||||||
|  |       top: -130px; | ||||||
|  |       left: -100px; | ||||||
|  |       z-index: 1; | ||||||
|  |       width: 1000px; | ||||||
|  |       height: 500px; | ||||||
|  |       transform: rotate(21deg); | ||||||
|  |       background-color: #062541; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         // top: -52vw; | ||||||
|  |         top: -215px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   header { | ||||||
|  |     $duration: 0.2s; | ||||||
|  |     transition: height $duration ease; | ||||||
|  |     position: relative; | ||||||
|  |     background-color: transparent; | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 1fr 1fr; | ||||||
|  |     height: 350px; | ||||||
|  |     z-index: 2; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       height: 180px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .info { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       padding: 30px; | ||||||
|  |       padding-left: 0; | ||||||
|  |       text-align: left; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         padding: 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     h1 { | ||||||
|  |       color: $green; | ||||||
|  |       width: 100%; | ||||||
|  |       font-weight: 500; | ||||||
|  |       line-height: 1.4; | ||||||
|  |       font-size: 30px; | ||||||
|  |       margin-top: 0; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         font-size: 24px; | ||||||
|  |         margin: 10px 0; | ||||||
|  |         // padding: 30px 30px 30px 40px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .known-for { | ||||||
|  |       color: rgba(255, 255, 255, 0.8); | ||||||
|  |       font-size: 1.2rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .person__poster { | ||||||
|  |     display: block; | ||||||
|  |     margin: auto; | ||||||
|  |     width: fit-content; | ||||||
|  |     border-radius: 10px; | ||||||
|  |     background-color: grey; | ||||||
|  |     animation: pulse 1s infinite ease-in-out; | ||||||
|  |  | ||||||
|  |     @keyframes pulse { | ||||||
|  |       0% { | ||||||
|  |         background-color: rgba(165, 165, 165, 0.1); | ||||||
|  |       } | ||||||
|  |       50% { | ||||||
|  |         background-color: rgba(165, 165, 165, 0.3); | ||||||
|  |       } | ||||||
|  |       100% { | ||||||
|  |         background-color: rgba(165, 165, 165, 0.1); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     > img { | ||||||
|  |       border-radius: 10px; | ||||||
|  |       width: 100%; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         max-width: 225px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										98
									
								
								src/components/profile/ChangePassword.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,98 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <h3 class="settings__header">Change password</h3> | ||||||
|  |     <form class="form"> | ||||||
|  |       <seasoned-input | ||||||
|  |         v-model="oldPassword" | ||||||
|  |         placeholder="old password" | ||||||
|  |         icon="Keyhole" | ||||||
|  |         type="password" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <seasoned-input | ||||||
|  |         v-model="newPassword" | ||||||
|  |         placeholder="new password" | ||||||
|  |         icon="Keyhole" | ||||||
|  |         type="password" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <seasoned-input | ||||||
|  |         v-model="newPasswordRepeat" | ||||||
|  |         placeholder="repeat new password" | ||||||
|  |         icon="Keyhole" | ||||||
|  |         type="password" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <seasoned-button @click="changePassword">change password</seasoned-button> | ||||||
|  |     </form> | ||||||
|  |     <seasoned-messages v-model:messages="messages" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref } from "vue"; | ||||||
|  |   import SeasonedInput from "@/components/ui/SeasonedInput.vue"; | ||||||
|  |   import SeasonedButton from "@/components/ui/SeasonedButton.vue"; | ||||||
|  |   import SeasonedMessages from "@/components/ui/SeasonedMessages.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { ErrorMessageTypes } from "../../interfaces/IErrorMessage"; | ||||||
|  |   import type { IErrorMessage } from "../../interfaces/IErrorMessage"; | ||||||
|  |  | ||||||
|  |   // interface ResetPasswordPayload { | ||||||
|  |   //   old_password: string; | ||||||
|  |   //   new_password: string; | ||||||
|  |   // } | ||||||
|  |  | ||||||
|  |   const oldPassword: Ref<string> = ref(""); | ||||||
|  |   const newPassword: Ref<string> = ref(""); | ||||||
|  |   const newPasswordRepeat: Ref<string> = ref(""); | ||||||
|  |   const messages: Ref<IErrorMessage[]> = ref([]); | ||||||
|  |  | ||||||
|  |   function addWarningMessage(message: string, title?: string) { | ||||||
|  |     messages.value.push({ | ||||||
|  |       message, | ||||||
|  |       title, | ||||||
|  |       type: ErrorMessageTypes.Warning | ||||||
|  |     } as IErrorMessage); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function validate() { | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       if (!oldPassword.value || oldPassword?.value?.length === 0) { | ||||||
|  |         addWarningMessage("Missing old password!", "Validation error"); | ||||||
|  |         reject(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!newPassword.value || newPassword?.value?.length === 0) { | ||||||
|  |         addWarningMessage("Missing new password!", "Validation error"); | ||||||
|  |         reject(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (newPassword.value !== newPasswordRepeat.value) { | ||||||
|  |         addWarningMessage( | ||||||
|  |           "Password and password repeat do not match!", | ||||||
|  |           "Validation error" | ||||||
|  |         ); | ||||||
|  |         reject(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       resolve(true); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // TODO seasoned-api /user/password-reset | ||||||
|  |   async function changePassword() { | ||||||
|  |     try { | ||||||
|  |       validate(); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.log("not valid!"); // eslint-disable-line no-console | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // const body: ResetPasswordPayload = { | ||||||
|  |     //   old_password: oldPassword.value, | ||||||
|  |     //   new_password: newPassword.value | ||||||
|  |     // }; | ||||||
|  |     // const options = {}; | ||||||
|  |     // fetch() | ||||||
|  |   } | ||||||
|  | </script> | ||||||
							
								
								
									
										114
									
								
								src/components/profile/LinkPlexAccount.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,114 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <h3 class="settings__header">Plex account</h3> | ||||||
|  |  | ||||||
|  |     <div v-if="!plexUserId"> | ||||||
|  |       <span class="info" | ||||||
|  |         >Sign in to your plex account to get information about recently added | ||||||
|  |         movies and to see your watch history</span | ||||||
|  |       > | ||||||
|  |  | ||||||
|  |       <form class="form"> | ||||||
|  |         <seasoned-input | ||||||
|  |           v-model="username" | ||||||
|  |           placeholder="plex username" | ||||||
|  |           type="email" | ||||||
|  |         /> | ||||||
|  |         <seasoned-input | ||||||
|  |           v-model="password" | ||||||
|  |           placeholder="plex password" | ||||||
|  |           type="password" | ||||||
|  |           @enter="authenticatePlex" | ||||||
|  |         > | ||||||
|  |         </seasoned-input> | ||||||
|  |  | ||||||
|  |         <seasoned-button @click="authenticatePlex" | ||||||
|  |           >link plex account</seasoned-button | ||||||
|  |         > | ||||||
|  |       </form> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-else> | ||||||
|  |       <span class="info" | ||||||
|  |         >Awesome, your account is already authenticated with plex! Enjoy viewing | ||||||
|  |         your seasoned search history, plex watch history and real-time torrent | ||||||
|  |         download progress.</span | ||||||
|  |       > | ||||||
|  |       <seasoned-button @click="unauthenticatePlex" | ||||||
|  |         >un-link plex account</seasoned-button | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |     <seasoned-messages v-model:messages="messages" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, computed, defineEmits } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import seasonedInput from "@/components/ui/SeasonedInput.vue"; | ||||||
|  |   import SeasonedButton from "@/components/ui/SeasonedButton.vue"; | ||||||
|  |   import SeasonedMessages from "@/components/ui/SeasonedMessages.vue"; | ||||||
|  |   import type { Ref, ComputedRef } from "vue"; | ||||||
|  |   import { linkPlexAccount, unlinkPlexAccount } from "../../api"; | ||||||
|  |   import { ErrorMessageTypes } from "../../interfaces/IErrorMessage"; | ||||||
|  |   import type { IErrorMessage } from "../../interfaces/IErrorMessage"; | ||||||
|  |  | ||||||
|  |   interface Emit { | ||||||
|  |     (e: "reload"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const username: Ref<string> = ref(""); | ||||||
|  |   const password: Ref<string> = ref(""); | ||||||
|  |   const messages: Ref<IErrorMessage[]> = ref([]); | ||||||
|  |  | ||||||
|  |   const store = useStore(); | ||||||
|  |   const emit = defineEmits<Emit>(); | ||||||
|  |  | ||||||
|  |   const plexUserId: ComputedRef<boolean> = computed( | ||||||
|  |     () => store.getters["user/plexUserId"] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   async function authenticatePlex() { | ||||||
|  |     const { success, message } = await linkPlexAccount( | ||||||
|  |       username.value, | ||||||
|  |       password.value | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (success) { | ||||||
|  |       username.value = ""; | ||||||
|  |       password.value = ""; | ||||||
|  |       emit("reload"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     messages.value.push({ | ||||||
|  |       type: success ? ErrorMessageTypes.Success : ErrorMessageTypes.Error, | ||||||
|  |       title: success ? "Authenticated with plex" : "Something went wrong", | ||||||
|  |       message | ||||||
|  |     } as IErrorMessage); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async function unauthenticatePlex() { | ||||||
|  |     const response = await unlinkPlexAccount(); | ||||||
|  |  | ||||||
|  |     if (response?.success) { | ||||||
|  |       emit("reload"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     messages.value.push({ | ||||||
|  |       type: response.success | ||||||
|  |         ? ErrorMessageTypes.Success | ||||||
|  |         : ErrorMessageTypes.Error, | ||||||
|  |       title: response.success | ||||||
|  |         ? "Unlinked plex account " | ||||||
|  |         : "Something went wrong", | ||||||
|  |       message: response.message | ||||||
|  |     } as IErrorMessage); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   .info { | ||||||
|  |     display: block; | ||||||
|  |     margin-bottom: 25px; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										6
									
								
								src/components/torrent/ActiveTorrents.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | <template> | ||||||
|  |   <code | ||||||
|  |     >Monitor active torrents requested. Requires authentication with owners plex | ||||||
|  |     library!</code | ||||||
|  |   > | ||||||
|  | </template> | ||||||