128
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										58
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,45 +1,45 @@ | |||||||
| { | { | ||||||
|   "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": "cross-env NODE_ENV=development webpack server", | ||||||
|     "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", |     "build": "cross-env 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", | ||||||
|     "start": "node server.js", |     "start": "node server.js", | ||||||
|     "docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md" |     "docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "axios": "^0.18.1", |  | ||||||
|     "babel-plugin-transform-object-rest-spread": "^6.26.0", |  | ||||||
|     "chart.js": "^2.9.2", |     "chart.js": "^2.9.2", | ||||||
|     "connect-history-api-fallback": "^1.3.0", |     "connect-history-api-fallback": "1.6.0", | ||||||
|     "express": "^4.16.1", |     "cross-env": "6.0.0", | ||||||
|     "vue": "^2.5.2", |     "express": "4.17.3", | ||||||
|     "vue-axios": "^1.2.2", |     "vue": "^3.2.37", | ||||||
|     "vue-data-tablee": "^0.12.1", |     "vue-router": "4.1.2", | ||||||
|     "vue-js-modal": "^1.3.16", |     "vuex": "3.6.2" | ||||||
|     "vue-router": "^3.0.1", |  | ||||||
|     "vuex": "^3.1.0" |  | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/core": "^7.4.5", |     "@babel/core": "7.17.2", | ||||||
|     "@babel/plugin-transform-runtime": "^7.4.4", |     "@babel/plugin-transform-runtime": "7.17.0", | ||||||
|     "@babel/preset-env": "^7.4.5", |     "@babel/preset-env": "7.16.11", | ||||||
|     "@babel/runtime": "^7.4.5", |     "@babel/runtime": "7.17.2", | ||||||
|     "babel-loader": "^8.0.6", |     "@types/node": "^18.6.1", | ||||||
|     "cross-env": "^3.0.0", |     "babel-loader": "8.2.3", | ||||||
|     "css-loader": "^3.4.2", |     "css-loader": "6.7.0", | ||||||
|     "documentation": "^11.0.0", |     "documentation": "^11.0.0", | ||||||
|     "file-loader": "^0.9.0", |     "file-loader": "6.2.0", | ||||||
|     "node-sass": "^4.5.0", |     "html-webpack-plugin": "^5.5.0", | ||||||
|     "sass-loader": "^5.0.1", |     "sass": "1.49.9", | ||||||
|     "schema-utils": "^2.4.1", |     "sass-loader": "12.6.0", | ||||||
|     "vue-loader": "^10.0.0", |     "terser-webpack-plugin": "5.3.1", | ||||||
|     "vue-svg-inline-loader": "^1.3.1", |     "ts-loader": "^9.3.1", | ||||||
|     "vue-template-compiler": "2.6.10", |     "typescript": "^4.7.4", | ||||||
|     "webpack": "^2.2.0", |     "vue-loader": "17.0.0", | ||||||
|     "webpack-dev-server": "^2.2.0" |     "webpack": "5.70.0", | ||||||
|  |     "webpack-cli": "4.9.2", | ||||||
|  |     "webpack-dev-server": "4.7.4" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| 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 | 
							
								
								
									
										27
									
								
								server.js
									
									
									
									
									
								
							
							
						
						| @@ -1,23 +1,18 @@ | |||||||
| var express = require('express'); | const express = require("express"); | ||||||
| var path = require('path'); | const path = require("path"); | ||||||
| const compression = require('compression') | const history = require("connect-history-api-fallback"); | ||||||
| var history = require('connect-history-api-fallback'); |  | ||||||
|  | const publicPath = path.join(__dirname, "public"); | ||||||
|  |  | ||||||
| app = express(); | app = express(); | ||||||
|  | app.use("/", express.static(publicPath)); | ||||||
|  | app.use(history({ index: "/" })); | ||||||
|  |  | ||||||
| app.use(compression()) | app.get("/", function (req, res) { | ||||||
| app.use('/dist', express.static(path.join(__dirname + "/dist"))); |   res.sendFile(`${publicPath}/index.html`); | ||||||
| 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')); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | const port = process.env.PORT || 5001; | ||||||
|  | console.log("Server runnning at port:", port); | ||||||
|  |  | ||||||
| app.listen(port); | app.listen(port); | ||||||
|   | |||||||
							
								
								
									
										180
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						| @@ -1,159 +1,77 @@ | |||||||
| <template> | <template> | ||||||
|   <div id="app"> |   <div id="app"> | ||||||
|  |  | ||||||
|     <!-- Header and hamburger navigation --> |     <!-- Header and hamburger navigation --> | ||||||
|     <navigation></navigation> |     <NavigationHeader class="header"></NavigationHeader> | ||||||
|  |  | ||||||
|     <!-- Header with search field --> |     <div class="navigation-icons-gutter desktop-only"> | ||||||
|  |       <NavigationIcons /> | ||||||
|     <!-- TODO move this to the navigation component --> |     </div> | ||||||
|     <header class="header"> |  | ||||||
|       <search-input v-model="query"></search-input> |  | ||||||
|     </header> |  | ||||||
|  |  | ||||||
|     <!-- Movie popup that will show above existing rendered content --> |  | ||||||
|     <movie-popup v-if="moviePopupIsVisible" :id="popupID" :type="popupType"></movie-popup> |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     <darkmode-toggle /> |  | ||||||
|  |  | ||||||
|     <!-- Display the component assigned to the given route (default: home) --> |     <!-- Display the component assigned to the given route (default: home) --> | ||||||
|     <router-view class="content" :key="$route.fullPath"></router-view> |     <router-view class="content" :key="$route.fullPath"></router-view> | ||||||
|  |  | ||||||
|  |     <!-- Popup that will show above existing rendered content --> | ||||||
|  |     <popup /> | ||||||
|  |  | ||||||
|  |     <darkmode-toggle /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import Vue from 'vue' | import NavigationHeader from "@/components/header/NavigationHeader"; | ||||||
| import Navigation from '@/components/Navigation' | import NavigationIcons from "@/components/header/NavigationIcons"; | ||||||
| import MoviePopup from '@/components/MoviePopup' | import Popup from "@/components/Popup"; | ||||||
| import SearchInput from '@/components/SearchInput' | import DarkmodeToggle from "@/components/ui/DarkmodeToggle"; | ||||||
| import DarkmodeToggle from '@/components/ui/darkmodeToggle' |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'app', |   name: "app", | ||||||
|   components: { |   components: { | ||||||
|     Navigation, |     NavigationHeader, | ||||||
|     MoviePopup, |     NavigationIcons, | ||||||
|     SearchInput, |     Popup, | ||||||
|     DarkmodeToggle |     DarkmodeToggle | ||||||
|   }, |  | ||||||
|   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); |  | ||||||
|     margin-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"; |  | ||||||
|  |  | ||||||
| *{ | #app { | ||||||
|   box-sizing: border-box; |   display: grid; | ||||||
| } |   grid-template-rows: var(--header-size); | ||||||
| html { |   grid-template-columns: var(--header-size) 1fr; | ||||||
|   height: 100%; |  | ||||||
| } |   @include mobile { | ||||||
| body{ |     grid-template-columns: 1fr; | ||||||
|   margin: 0; |  | ||||||
|   padding: 0; |  | ||||||
|   font-family: 'Roboto', sans-serif; |  | ||||||
|   line-height: 1.6; |  | ||||||
|   background: $background-color; |  | ||||||
|   color: $text-color; |  | ||||||
|   transition: background-color .5s ease, color .5s ease; |  | ||||||
|   &.hidden{ |  | ||||||
|     overflow: hidden; |  | ||||||
|   } |   } | ||||||
| } |  | ||||||
| h1,h2,h3 { |  | ||||||
|   transition: color .5s ease; |  | ||||||
| } |  | ||||||
| a:any-link { |  | ||||||
|   color: inherit; |  | ||||||
| } |  | ||||||
| input, textarea, button{ |  | ||||||
|   font-family: 'Roboto', sans-serif; |  | ||||||
| } |  | ||||||
| figure{ |  | ||||||
|   padding: 0; |  | ||||||
|   margin: 0; |  | ||||||
| } |  | ||||||
| img{ |  | ||||||
|   display: block; |  | ||||||
|   // max-width: 100%; |  | ||||||
|   height: auto; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .no-scroll { |   .header { | ||||||
|   overflow: hidden; |     position: fixed; | ||||||
| } |  | ||||||
|  |  | ||||||
| .wrapper{ |  | ||||||
|   position: relative; |  | ||||||
| } |  | ||||||
| .header{ |  | ||||||
|   position: fixed; |  | ||||||
|   z-index: 15; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|  |  | ||||||
|   @include tablet-min{ |  | ||||||
|     width: calc(100% - 170px); |  | ||||||
|     margin-left: 95px; |  | ||||||
|     border-top: 0; |  | ||||||
|     border-bottom: 0; |  | ||||||
|     top: 0; |     top: 0; | ||||||
|  |     width: 100%; | ||||||
|  |     z-index: 15; | ||||||
|   } |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| // router view transition |   .navigation-icons-gutter { | ||||||
| .fade-enter-active, .fade-leave-active { |     position: fixed; | ||||||
|   transition-property: opacity; |     height: 100vh; | ||||||
|   transition-duration: 0.25s; |     margin: 0; | ||||||
| } |     top: var(--header-size); | ||||||
| .fade-enter-active { |     width: var(--header-size); | ||||||
|   transition-delay: 0.25s; |     background-color: var(--background-color-secondary); | ||||||
| } |   } | ||||||
| .fade-enter, .fade-leave-active { |  | ||||||
|   opacity: 0 |   .content { | ||||||
|  |     display: grid; | ||||||
|  |     grid-column: 2 / 3; | ||||||
|  |     grid-row: 2; | ||||||
|  |     z-index: 5; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       grid-column: 1 / 3; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										490
									
								
								src/api.js
									
									
									
									
									
								
							
							
						
						| @@ -1,490 +0,0 @@ | |||||||
| import axios from 'axios' |  | ||||||
| import storage from '@/storage' |  | ||||||
| import config from '@/config.json' |  | ||||||
| import path from 'path' |  | ||||||
| import store from '@/store' |  | ||||||
|  |  | ||||||
| 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? |  | ||||||
|  |  | ||||||
| const checkStatusAndReturnJson = (response) => { |  | ||||||
|   if (!response.ok) { |  | ||||||
|     throw resp |  | ||||||
|   } |  | ||||||
|   return response.json() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // - - - 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, checkExistance=false, credits=false, release_dates=false) => { |  | ||||||
|   const url = new URL('v2/movie', SEASONED_URL) |  | ||||||
|   url.pathname = path.join(url.pathname, id.toString()) |  | ||||||
|   if (checkExistance) { |  | ||||||
|     url.searchParams.append('check_existance', true) |  | ||||||
|   } |  | ||||||
|   if (credits) { |  | ||||||
|     url.searchParams.append('credits', true) |  | ||||||
|   } |  | ||||||
|   if(release_dates) { |  | ||||||
|     url.searchParams.append('release_dates', true) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .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, checkExistance=false, credits=false) => { |  | ||||||
|   const url = new URL('v2/show', SEASONED_URL) |  | ||||||
|   url.pathname = path.join(url.pathname, id.toString()) |  | ||||||
|   if (checkExistance) { |  | ||||||
|     url.searchParams.append('check_existance', true) |  | ||||||
|   } |  | ||||||
|   if (credits) { |  | ||||||
|     url.searchParams.append('credits', true) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .catch(error => { console.error(`api error getting show: ${id}`); 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('v2/person', SEASONED_URL) |  | ||||||
|   url.pathname = path.join(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}`); 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, page=1) => { |  | ||||||
|   const url = new URL('v2/movie/' + name, SEASONED_URL) |  | ||||||
|   url.searchParams.append('page', page) |  | ||||||
|   const headers = { authorization: storage.token } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers: headers }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Fetches requested items. |  | ||||||
|  * @param {number} [page=1] |  | ||||||
|  * @returns {object} Request response |  | ||||||
|  */ |  | ||||||
| const getRequests = (page=1) => { |  | ||||||
|   const url = new URL('v2/request', SEASONED_URL) |  | ||||||
|   url.searchParams.append('page', page) |  | ||||||
|   const headers = { authorization: storage.token } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers: headers }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| const getUserRequests = (page=1) => { |  | ||||||
|   const url = new URL('v1/user/requests', SEASONED_URL) |  | ||||||
|   url.searchParams.append('page', page) |  | ||||||
|  |  | ||||||
|   const headers = { authorization: localStorage.getItem('token') } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers }) |  | ||||||
|     .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('v2/search', SEASONED_URL) |  | ||||||
|   if (mediaType != null && ['movie', 'show', 'person'].includes(mediaType)) { |  | ||||||
|     url.pathname += `/${mediaType}` |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   url.searchParams.append('query', query) |  | ||||||
|   url.searchParams.append('page', page) |  | ||||||
|   url.searchParams.append('adult', adult) |  | ||||||
|  |  | ||||||
|   const headers = { authorization: localStorage.getItem('token') } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .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('/api/v1/pirate/search', SEASONED_URL) |  | ||||||
|   url.searchParams.append('query', query) |  | ||||||
|  |  | ||||||
|   const headers = { authorization: storage.token } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers: headers }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .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 = JSON.stringify({ |  | ||||||
|     magnet: magnet, |  | ||||||
|     name: name, |  | ||||||
|     tmdb_id: tmdb_id |  | ||||||
|   }) |  | ||||||
|   const headers = { |  | ||||||
|     'Content-Type': 'application/json', |  | ||||||
|     authorization: storage.token |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { |  | ||||||
|       method: 'POST', |  | ||||||
|       headers, |  | ||||||
|       body |  | ||||||
|     }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .catch(error => { console.error(`api error adding magnet: ${name} ${error}`); 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)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const watchLink = (title, year, authorization_token=undefined) => { |  | ||||||
|   const url = new URL('v1/plex/watch-link', SEASONED_URL) |  | ||||||
|   url.searchParams.append('title', title) |  | ||||||
|   url.searchParams.append('year', year) |  | ||||||
|  |  | ||||||
|   const headers = { |  | ||||||
|     'Authorization': authorization_token, |  | ||||||
|     'Content-Type': 'application/json' |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .then(response => response.link) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // - - - Seasoned user endpoints - - - |  | ||||||
|  |  | ||||||
| const register = (username, password) => { |  | ||||||
|   const url = new URL('v1/user', SEASONED_URL) |  | ||||||
|   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 => { |  | ||||||
|       console.error('Unexpected error occured before receiving response. Error:', error) |  | ||||||
|       // TODO log to sentry the issue here |  | ||||||
|       throw error |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const login = (username, password, throwError=false) => { |  | ||||||
|   const url = new URL('v1/user/login', SEASONED_URL) |  | ||||||
|   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; |  | ||||||
|       else |  | ||||||
|         console.error("Error occured when trying to sign in.\nError:", resp); |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const getSettings = () => { |  | ||||||
|   const settingsExists = (value) => { |  | ||||||
|     if (value instanceof Object && value.hasOwnProperty('settings')) |  | ||||||
|       return value; |  | ||||||
|     throw "Settings does not exist in response object."; |  | ||||||
|   } |  | ||||||
|   const commitSettingsToStore = (response) => { |  | ||||||
|     store.dispatch('userModule/setSettings', response.settings) |  | ||||||
|     return response |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const url = new URL('v1/user/settings', SEASONED_URL) |  | ||||||
|  |  | ||||||
|   const authorization_token = localStorage.getItem('token') |  | ||||||
|   const headers = authorization_token ? { |  | ||||||
|     'Authorization': authorization_token, |  | ||||||
|     'Content-Type': 'application/json' |  | ||||||
|   } : {} |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .then(settingsExists) |  | ||||||
|     .then(commitSettingsToStore) |  | ||||||
|     .then(response => response.settings) |  | ||||||
|     .catch(error => { console.log('api error getting user settings'); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const updateSettings = (settings) => { |  | ||||||
|   const url = new URL('v1/user/settings', SEASONED_URL) |  | ||||||
|  |  | ||||||
|   const authorization_token = localStorage.getItem('token') |  | ||||||
|   const headers = authorization_token ? { |  | ||||||
|     'Authorization': authorization_token, |  | ||||||
|     'Content-Type': 'application/json' |  | ||||||
|   } : {} |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { |  | ||||||
|       method: 'PUT', |  | ||||||
|       headers, |  | ||||||
|       body: JSON.stringify(settings) |  | ||||||
|     }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .catch(error => { console.log('api error updating user settings'); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // - - - Authenticate with plex - - - |  | ||||||
|  |  | ||||||
| const linkPlexAccount = (username, password) => { |  | ||||||
|   const url = new URL('v1/user/link_plex', SEASONED_URL) |  | ||||||
|   const body = { username, password } |  | ||||||
|   const headers = { |  | ||||||
|     'Content-Type': 'application/json', |  | ||||||
|     authorization: storage.token |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { |  | ||||||
|     method: 'POST', |  | ||||||
|     headers, |  | ||||||
|     body: JSON.stringify(body) |  | ||||||
|   }) |  | ||||||
|   .then(resp => resp.json()) |  | ||||||
|   .catch(error => { console.error(`api error linking plex account: ${username}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const unlinkPlexAccount = (username, password) => { |  | ||||||
|   const url = new URL('v1/user/unlink_plex', SEASONED_URL) |  | ||||||
|   const headers = { |  | ||||||
|     'Content-Type': 'application/json', |  | ||||||
|     authorization: storage.token |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { |  | ||||||
|     method: 'POST', |  | ||||||
|     headers |  | ||||||
|   }) |  | ||||||
|   .then(resp => resp.json()) |  | ||||||
|   .catch(error => { console.error(`api error unlinking plex account: ${username}`); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // - - - User graphs - - - |  | ||||||
|  |  | ||||||
| const fetchChart = (urlPath, days, chartType) => { |  | ||||||
|   const url = new URL('v1/user' + urlPath, SEASONED_URL) |  | ||||||
|   url.searchParams.append('days', days) |  | ||||||
|   url.searchParams.append('y_axis', chartType) |  | ||||||
|  |  | ||||||
|   const authorization_token = localStorage.getItem('token') |  | ||||||
|   const headers = authorization_token ? { |  | ||||||
|     'Authorization': authorization_token, |  | ||||||
|     'Content-Type': 'application/json' |  | ||||||
|   } : {} |  | ||||||
|  |  | ||||||
|   return fetch(url.href, { headers }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .catch(error => { console.log('api error fetching chart'); throw error }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // - - - Random emoji - - - |  | ||||||
|  |  | ||||||
| const getEmoji = () => { |  | ||||||
|   const url = new URL('v1/emoji', SEASONED_URL) |  | ||||||
|  |  | ||||||
|   return fetch(url.href) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .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, |  | ||||||
|   getPerson, |  | ||||||
|   getTmdbMovieListByName, |  | ||||||
|   searchTmdb, |  | ||||||
|   getUserRequests, |  | ||||||
|   getRequests, |  | ||||||
|   searchTorrents, |  | ||||||
|   addMagnet, |  | ||||||
|   request, |  | ||||||
|   watchLink, |  | ||||||
|   getRequestStatus, |  | ||||||
|   linkPlexAccount, |  | ||||||
|   unlinkPlexAccount, |  | ||||||
|   register, |  | ||||||
|   login, |  | ||||||
|   getSettings, |  | ||||||
|   updateSettings, |  | ||||||
|   fetchChart, |  | ||||||
|   getEmoji, |  | ||||||
|   elasticSearchMoviesAndShows |  | ||||||
| } |  | ||||||
							
								
								
									
										563
									
								
								src/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,563 @@ | |||||||
|  | import config from "./config"; | ||||||
|  | import { IList } from "./interfaces/IList"; | ||||||
|  |  | ||||||
|  | let { SEASONED_URL, ELASTIC_URL, ELASTIC_INDEX } = config; | ||||||
|  | if (!SEASONED_URL) { | ||||||
|  |   SEASONED_URL = window.location.origin; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TODO | ||||||
|  | //  - Move autorization token and errors here? | ||||||
|  |  | ||||||
|  | const checkStatusAndReturnJson = response => { | ||||||
|  |   if (!response.ok) { | ||||||
|  |     throw response; | ||||||
|  |   } | ||||||
|  |   return response.json(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - 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, | ||||||
|  |   checkExistance = false, | ||||||
|  |   credits = false, | ||||||
|  |   release_dates = false | ||||||
|  | ) => { | ||||||
|  |   const url = new URL("/api/v2/movie", SEASONED_URL); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|  |   if (checkExistance) { | ||||||
|  |     url.searchParams.append("check_existance", "true"); | ||||||
|  |   } | ||||||
|  |   if (credits) { | ||||||
|  |     url.searchParams.append("credits", "true"); | ||||||
|  |   } | ||||||
|  |   if (release_dates) { | ||||||
|  |     url.searchParams.append("release_dates", "true"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .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, checkExistance = false, credits = false) => { | ||||||
|  |   const url = new URL("/api/v2/show", SEASONED_URL); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|  |   if (checkExistance) { | ||||||
|  |     url.searchParams.append("check_existance", "true"); | ||||||
|  |   } | ||||||
|  |   if (credits) { | ||||||
|  |     url.searchParams.append("credits", "true"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting show: ${id}`); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function delay(ms) { | ||||||
|  |   return new Promise(resolve => setTimeout(resolve, ms)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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", SEASONED_URL); | ||||||
|  |   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}`); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getCredits = (type, id) => { | ||||||
|  |   if (type === "movie") { | ||||||
|  |     return getMovieCredits(id); | ||||||
|  |   } else if (type === "show") { | ||||||
|  |     return getShowCredits(id); | ||||||
|  |   } else if (type === "person") { | ||||||
|  |     return getPersonCredits(id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return []; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb movie credits by id. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getMovieCredits = id => { | ||||||
|  |   const url = new URL("/api/v2/movie", SEASONED_URL); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting movie: ${id}`); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb show credits by id. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getShowCredits = id => { | ||||||
|  |   const url = new URL("/api/v2/show", SEASONED_URL); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting show: ${id}`); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches tmdb person credits by id. | ||||||
|  |  * @param {number} id | ||||||
|  |  * @returns {object} Tmdb response | ||||||
|  |  */ | ||||||
|  | const getPersonCredits = id => { | ||||||
|  |   const url = new URL("/api/v2/person", SEASONED_URL); | ||||||
|  |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error getting person: ${id}`); | ||||||
|  |       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: number = 1 | ||||||
|  | ): Promise<IList> => { | ||||||
|  |   const url = new URL("/api/v2/movie/" + name, SEASONED_URL); | ||||||
|  |   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 }) | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fetches requested items. | ||||||
|  |  * @param {number} [page=1] | ||||||
|  |  * @returns {object} Request response | ||||||
|  |  */ | ||||||
|  | const getRequests = (page: number = 1) => { | ||||||
|  |   const url = new URL("/api/v2/request", SEASONED_URL); | ||||||
|  |   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 }) | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getUserRequests = (page = 1) => { | ||||||
|  |   const url = new URL("/api/v1/user/requests", SEASONED_URL); | ||||||
|  |   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", SEASONED_URL); | ||||||
|  |   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}`); | ||||||
|  |       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", SEASONED_URL); | ||||||
|  |   url.searchParams.append("query", query); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .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("/api/v1/pirate/add", SEASONED_URL); | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       magnet: magnet, | ||||||
|  |       name: name, | ||||||
|  |       tmdb_id: tmdb_id | ||||||
|  |     }) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.error(`api error adding magnet: ${name} ${error}`); | ||||||
|  |       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) => { | ||||||
|  |   const url = new URL("/api/v2/request", SEASONED_URL); | ||||||
|  |  | ||||||
|  |   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}`); | ||||||
|  |       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 = undefined) => { | ||||||
|  |   const url = new URL("/api/v2/request", SEASONED_URL); | ||||||
|  |   url.pathname = `${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)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const watchLink = (title, year) => { | ||||||
|  |   const url = new URL("/api/v1/plex/watch-link", SEASONED_URL); | ||||||
|  |   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`, SEASONED_URL); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(resp => resp.json()); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Seasoned user endpoints - - - | ||||||
|  |  | ||||||
|  | const register = (username, password) => { | ||||||
|  |   const url = new URL("/api/v1/user", SEASONED_URL); | ||||||
|  |   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 => { | ||||||
|  |       console.error( | ||||||
|  |         "Unexpected error occured before receiving response. Error:", | ||||||
|  |         error | ||||||
|  |       ); | ||||||
|  |       // TODO log to sentry the issue here | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const login = (username, password, throwError = false) => { | ||||||
|  |   const url = new URL("/api/v1/user/login", SEASONED_URL); | ||||||
|  |   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; | ||||||
|  |     else console.error("Error occured when trying to sign in.\nError:", resp); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const logout = (throwError = false) => { | ||||||
|  |   const url = new URL("/api/v1/user/logout", SEASONED_URL); | ||||||
|  |   const options = { method: "POST" }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options).then(resp => { | ||||||
|  |     if (resp.status == 200) return resp.json(); | ||||||
|  |  | ||||||
|  |     if (throwError) throw resp; | ||||||
|  |     else console.error("Error occured when trying to log out.\nError:", resp); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getSettings = () => { | ||||||
|  |   const url = new URL("/api/v1/user/settings", SEASONED_URL); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(error => { | ||||||
|  |       console.log("api error getting user settings"); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateSettings = settings => { | ||||||
|  |   const url = new URL("/api/v1/user/settings", SEASONED_URL); | ||||||
|  |  | ||||||
|  |   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"); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Authenticate with plex - - - | ||||||
|  |  | ||||||
|  | const linkPlexAccount = (username, password) => { | ||||||
|  |   const url = new URL("/api/v1/user/link_plex", SEASONED_URL); | ||||||
|  |   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}`); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const unlinkPlexAccount = (username, password) => { | ||||||
|  |   const url = new URL("/api/v1/user/unlink_plex", SEASONED_URL); | ||||||
|  |  | ||||||
|  |   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 plex account: ${username}`); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - User graphs - - - | ||||||
|  |  | ||||||
|  | const fetchChart = (urlPath, days, chartType) => { | ||||||
|  |   const url = new URL("/api/v1/user" + urlPath, SEASONED_URL); | ||||||
|  |   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); | ||||||
|  |       throw Error(resp.statusText); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return resp.json(); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // - - - Random emoji - - - | ||||||
|  |  | ||||||
|  | const getEmoji = () => { | ||||||
|  |   const url = new URL("/api/v1/emoji", SEASONED_URL); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .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, count = 22) => { | ||||||
|  |   const url = new URL(`${ELASTIC_INDEX}/_search`, ELASTIC_URL); | ||||||
|  |  | ||||||
|  |   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}`); | ||||||
|  |       throw error; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |   getMovie, | ||||||
|  |   getShow, | ||||||
|  |   getPerson, | ||||||
|  |   getMovieCredits, | ||||||
|  |   getShowCredits, | ||||||
|  |   getPersonCredits, | ||||||
|  |   getCredits, | ||||||
|  |   getTmdbMovieListByName, | ||||||
|  |   searchTmdb, | ||||||
|  |   getUserRequests, | ||||||
|  |   getRequests, | ||||||
|  |   searchTorrents, | ||||||
|  |   addMagnet, | ||||||
|  |   request, | ||||||
|  |   watchLink, | ||||||
|  |   movieImages, | ||||||
|  |   getRequestStatus, | ||||||
|  |   linkPlexAccount, | ||||||
|  |   unlinkPlexAccount, | ||||||
|  |   register, | ||||||
|  |   login, | ||||||
|  |   logout, | ||||||
|  |   getSettings, | ||||||
|  |   updateSettings, | ||||||
|  |   fetchChart, | ||||||
|  |   getEmoji, | ||||||
|  |   elasticSearchMoviesAndShows | ||||||
|  | }; | ||||||
| Before Width: | Height: | Size: 1.9 KiB | 
| @@ -1,75 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <section class="not-found"> |  | ||||||
|       <h1 class="not-found__title">Page Not Found</h1> |  | ||||||
|     </section> |  | ||||||
|     <seasoned-button class="button" @click="goBack">go back to previous page</seasoned-button> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import store from '@/store' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { SeasonedButton }, |  | ||||||
|   methods: { |  | ||||||
|     goBack() { |  | ||||||
|       this.$router.go(-1) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     if (this.$popup.isOpen == true) |  | ||||||
|       this.$popup.close() |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| .button { |  | ||||||
|   font-size: 1.2rem; |  | ||||||
|   position: fixed; |  | ||||||
|   top: 50%; |  | ||||||
|   left: calc(50% + 46px); |  | ||||||
|   transform: translate(-50%, -50%); |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     top: 60%; |  | ||||||
|     left: 50%; |  | ||||||
|     font-size: 1rem; |  | ||||||
|     width: content; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .not-found { |  | ||||||
|   display: flex; |  | ||||||
|   height: calc(100vh - var(--header-size)); |  | ||||||
|   background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%; |  | ||||||
|   background-size: cover; |  | ||||||
|   align-items: center; |  | ||||||
|   flex-direction: column; |  | ||||||
|  |  | ||||||
|   &::before { |  | ||||||
|     content: ""; |  | ||||||
|     position: absolute; |  | ||||||
|     height: calc(100vh - var(--header-size)); |  | ||||||
|     width: 100%; |  | ||||||
|     pointer-events: none; |  | ||||||
|     background: $background-40; |  | ||||||
|   } |  | ||||||
|   &__title { |  | ||||||
|     margin-top: 30vh; |  | ||||||
|     font-size: 2.5rem; |  | ||||||
|     font-weight: 500; |  | ||||||
|     color: $text-color; |  | ||||||
|     position: relative; |  | ||||||
|  |  | ||||||
|     @include tablet-min { |  | ||||||
|       font-size: 3.5rem; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,316 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="wrapper" v-if="hasPlexUser"> |  | ||||||
|     <h1>Your watch activity</h1> |  | ||||||
|  |  | ||||||
|     <div class="filter"> |  | ||||||
|       <h2>Filter</h2> |  | ||||||
|  |  | ||||||
|       <div class="filter-item"> |  | ||||||
|         <label class="desktop-only">Days:</label> |  | ||||||
|         <input class="dayinput" |  | ||||||
|                v-model="days" |  | ||||||
|                placeholder="number of days" |  | ||||||
|                type="number" |  | ||||||
|                pattern="[0-9]*" |  | ||||||
|                :style="{maxWidth: `${3 + (0.5 * days.length)}rem`}"/> |  | ||||||
| <!--         <datalist id="days"> |  | ||||||
|           <option v-for="index in 1500" :value="index" :key="index"></option> |  | ||||||
|         </datalist> --> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <toggle-button class="filter-item" :options="chartTypes" :selected.sync="selectedChartDataType" /> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div class="chart-section"> |  | ||||||
|       <h3 class="chart-header">Activity per day:</h3> |  | ||||||
|       <div class="chart"> |  | ||||||
|         <canvas ref="activityCanvas"></canvas> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <h3 class="chart-header">Activity per day of week:</h3> |  | ||||||
|       <div class="chart"> |  | ||||||
|         <canvas ref="playsByDayOfWeekCanvas"></canvas> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
|   <div v-else> |  | ||||||
|     <h1>Must be authenticated</h1> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import store from '@/store' |  | ||||||
| import ToggleButton from '@/components/ui/ToggleButton'; |  | ||||||
| import { fetchChart } from '@/api' |  | ||||||
|  |  | ||||||
| var Chart = require('chart.js'); |  | ||||||
| Chart.defaults.global.elements.point.radius = 0 |  | ||||||
| Chart.defaults.global.elements.point.hitRadius = 10 |  | ||||||
| Chart.defaults.global.elements.point.pointHoverRadius = 10 |  | ||||||
| Chart.defaults.global.elements.point.hoverBorderWidth = 4 |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { ToggleButton }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       days: 30, |  | ||||||
|       selectedChartDataType: 'plays', |  | ||||||
|       charts: [{ |  | ||||||
|         name: 'Watch activity', |  | ||||||
|         ref: 'activityCanvas', |  | ||||||
|         data: null, |  | ||||||
|         urlPath: '/plays_by_day', |  | ||||||
|         graphType: 'line' |  | ||||||
|       }, { |  | ||||||
|         name: 'Plays by day of week', |  | ||||||
|         ref: 'playsByDayOfWeekCanvas', |  | ||||||
|         data: null, |  | ||||||
|         urlPath: '/plays_by_dayofweek', |  | ||||||
|         graphType: 'bar' |  | ||||||
|       }], |  | ||||||
|       chartData: [{ |  | ||||||
|         type: 'plays', |  | ||||||
|         tooltipLabel: 'Play count', |  | ||||||
|       },{ |  | ||||||
|         type: 'duration', |  | ||||||
|         tooltipLabel: 'Watched duration', |  | ||||||
|         valueConvertFunction: this.convertSecondsToHumanReadable |  | ||||||
|       }], |  | ||||||
|       gridColor: getComputedStyle(document.documentElement).getPropertyValue('--text-color-5') |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     hasPlexUser() { |  | ||||||
|       return store.getters['userModule/plex_userid'] != null ? true : false |  | ||||||
|     }, |  | ||||||
|     chartTypes() { |  | ||||||
|       return this.chartData.map(chart => chart.type) |  | ||||||
|     }, |  | ||||||
|     selectedChartType() { |  | ||||||
|       return this.chartData.filter(data => data.type == this.selectedChartDataType)[0] |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     hasPlexUser(newValue, oldValue) { |  | ||||||
|       if (newValue != oldValue && newValue == true) { |  | ||||||
|         this.fetchChartData(this.charts) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     days(newValue) { |  | ||||||
|       if (newValue !== '') { |  | ||||||
|         this.fetchChartData(this.charts) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     selectedChartDataType(selectedChartDataType) { |  | ||||||
|       this.fetchChartData(this.charts) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     if (typeof(this.days) == 'number') { |  | ||||||
|       this.days = this.days.toString() |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     fetchChartData(charts) { |  | ||||||
|       if (this.hasPlexUser == false) { |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       for (let chart of charts) { |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         fetchChart(chart.urlPath, this.days, this.selectedChartType.type) |  | ||||||
|           .then(data => { |  | ||||||
|             this.series = data.data.series.filter(group => group.name === 'TV')[0].data;      // plays pr date in groups (movie/tv/music) |  | ||||||
|             this.categories = data.data.categories;  // dates |  | ||||||
|  |  | ||||||
|             const x_labels = data.data.categories.map(date => { |  | ||||||
|               if (date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) { |  | ||||||
|                 const [year, month, day] = date.split('-') |  | ||||||
|                 return `${day}.${month}` |  | ||||||
|               } |  | ||||||
|  |  | ||||||
|               return date |  | ||||||
|             }) |  | ||||||
|             let y_activityMovies = data.data.series.filter(group => group.name === 'Movies')[0].data |  | ||||||
|             let y_activityTV = data.data.series.filter(group => group.name === 'TV')[0].data |  | ||||||
|  |  | ||||||
|             const datasets = [{ |  | ||||||
|                 label: `Movies watch last ${ this.days } days`, |  | ||||||
|                 data: y_activityMovies, |  | ||||||
|                 backgroundColor: 'rgba(54, 162, 235, 0.2)', |  | ||||||
|                 borderColor: 'rgba(54, 162, 235, 1)', |  | ||||||
|                 borderWidth: 1 |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 label: `Shows watch last ${ this.days } days`, |  | ||||||
|                 data: y_activityTV, |  | ||||||
|                 backgroundColor: 'rgba(255, 159, 64, 0.2)', |  | ||||||
|                 borderColor: 'rgba(255, 159, 64, 1)', |  | ||||||
|                 borderWidth: 1 |  | ||||||
|               } |  | ||||||
|             ] |  | ||||||
|  |  | ||||||
|             if (chart.data == null) { |  | ||||||
|               this.generateChart(chart, x_labels, datasets) |  | ||||||
|             } else { |  | ||||||
|               chart.data.clear(); |  | ||||||
|               chart.data.data.labels = x_labels; |  | ||||||
|               chart.data.data.datasets = datasets; |  | ||||||
|               chart.data.update(); |  | ||||||
|             } |  | ||||||
|           }) |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     generateChart(chart, labels, datasets) { |  | ||||||
|       const chartInstance = new Chart(this.$refs[chart.ref], { |  | ||||||
|         type: chart.graphType, |  | ||||||
|         data: { |  | ||||||
|             labels: labels, |  | ||||||
|             datasets: datasets |  | ||||||
|         }, |  | ||||||
|         options: { |  | ||||||
|           // hitRadius: 8, |  | ||||||
|           maintainAspectRatio: false, |  | ||||||
|           tooltips: { |  | ||||||
|             callbacks: { |  | ||||||
|               title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`, |  | ||||||
|               label: (tooltipItem, data) => { |  | ||||||
|                 let label = data.datasets[tooltipItem.datasetIndex].label |  | ||||||
|                 let value = tooltipItem.value; |  | ||||||
|                 let text = 'Duration watched' |  | ||||||
|  |  | ||||||
|                 const context = label.split(' ')[0] |  | ||||||
|                 if (context) { |  | ||||||
|                   text = `${context} ${this.selectedChartType.tooltipLabel.toLowerCase()}` |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (this.selectedChartType.valueConvertFunction) { |  | ||||||
|                   value = this.selectedChartType.valueConvertFunction(tooltipItem.value) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return ` ${text}: ${value}` |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }, |  | ||||||
|           scales: { |  | ||||||
|               yAxes: [{ |  | ||||||
|                 gridLines: { |  | ||||||
|                     color: this.gridColor |  | ||||||
|                 }, |  | ||||||
|                 stacked: chart.graphType === 'bar', |  | ||||||
|                 ticks: { |  | ||||||
|                   // suggestedMax: 10000, |  | ||||||
|                   callback: (value, index, values) => { |  | ||||||
|                     if (this.selectedChartType.valueConvertFunction) { |  | ||||||
|                       return this.selectedChartType.valueConvertFunction(value, values) |  | ||||||
|                     } |  | ||||||
|                     return value |  | ||||||
|                   }, |  | ||||||
|                   beginAtZero: true |  | ||||||
|                 } |  | ||||||
|               }], |  | ||||||
|               xAxes: [{ |  | ||||||
|                 stacked: chart.graphType === 'bar', |  | ||||||
|                 gridLines: { |  | ||||||
|                   display: false, |  | ||||||
|                 } |  | ||||||
|               }] |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       chart.data = chartInstance; |  | ||||||
|     }, |  | ||||||
|     convertSecondsToHumanReadable(value, values=null) { |  | ||||||
|       const highestValue = values ? values[0] : value; |  | ||||||
|  |  | ||||||
|       // minutes |  | ||||||
|       if (highestValue < 3600) { |  | ||||||
|         const minutes = Math.floor(value / 60); |  | ||||||
|  |  | ||||||
|         value = `${minutes} m` |  | ||||||
|       } |  | ||||||
|       // hours and minutes |  | ||||||
|       else if (highestValue > 3600 && highestValue < 86400) { |  | ||||||
|         const hours = Math.floor(value / 3600); |  | ||||||
|         const minutes = Math.floor(value % 3600 / 60); |  | ||||||
|  |  | ||||||
|         value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m` |  | ||||||
|       } |  | ||||||
|       // days and hours |  | ||||||
|       else if (highestValue > 86400 && highestValue < 31557600) { |  | ||||||
|         const days = Math.floor(value / 86400); |  | ||||||
|         const hours = Math.floor(value % 86400 / 3600); |  | ||||||
|  |  | ||||||
|         value = days != 0 ? `${days} d ${hours} h` : `${hours} h` |  | ||||||
|       } |  | ||||||
|       // years and days |  | ||||||
|       else if (highestValue > 31557600) { |  | ||||||
|         const years = Math.floor(value / 31557600); |  | ||||||
|         const days = Math.floor(value % 31557600 / 86400); |  | ||||||
|  |  | ||||||
|         value = years != 0 ? `${years} y ${days} d` : `${days} d` |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return value |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
|  |  | ||||||
| .wrapper { |  | ||||||
|   padding: 2rem; |  | ||||||
|  |  | ||||||
|   @include mobile-only { |  | ||||||
|     padding: 0 0.8rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .filter { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   align-items: center; |  | ||||||
|   margin-bottom: 2rem; |  | ||||||
|  |  | ||||||
|   h2 { |  | ||||||
|     margin-bottom: 0.5rem; |  | ||||||
|     width: 100%; |  | ||||||
|     font-weight: 400; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   &-item:not(:first-of-type) { |  | ||||||
|     margin-left: 1rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .dayinput { |  | ||||||
|     font-size: 1.2rem; |  | ||||||
|     max-width: 3rem; |  | ||||||
|     background-color: $background-ui; |  | ||||||
|     color: $text-color; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .chart-section { |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|  |  | ||||||
|   .chart { |  | ||||||
|     position: relative; |  | ||||||
|     height: 35vh; |  | ||||||
|     width: 90vw; |  | ||||||
|     margin-bottom: 2rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .chart-header { |  | ||||||
|     font-weight: 300; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
							
								
								
									
										44
									
								
								src/components/CastList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="cast"> | ||||||
|  |     <ol class="persons"> | ||||||
|  |       <CastListItem v-for="person in cast" :person="person" :key="person.id" /> | ||||||
|  |     </ol> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import CastListItem from "src/components/CastListItem"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "CastList", | ||||||
|  |   components: { CastListItem }, | ||||||
|  |   props: { | ||||||
|  |     cast: { | ||||||
|  |       type: Array, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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> | ||||||
							
								
								
									
										121
									
								
								src/components/CastListItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,121 @@ | |||||||
|  | <template> | ||||||
|  |   <li class="card"> | ||||||
|  |     <a @click="openCastItem"> | ||||||
|  |       <img class="persons--image" :src="pictureUrl" /> | ||||||
|  |       <p class="name">{{ person.name || person.title }}</p> | ||||||
|  |       <p class="meta">{{ person.character || person.year }}</p> | ||||||
|  |     </a> | ||||||
|  |   </li> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { mapActions } from "vuex"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "CastListItem", | ||||||
|  |   props: { | ||||||
|  |     person: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapActions("popup", ["open"]), | ||||||
|  |     openCastItem() { | ||||||
|  |       let { id, type } = this.person; | ||||||
|  |  | ||||||
|  |       if (type) { | ||||||
|  |         this.open({ id, type }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     pictureUrl() { | ||||||
|  |       const { profile_path, poster_path, poster } = this.person; | ||||||
|  |       if (profile_path) return "https://image.tmdb.org/t/p/w185" + profile_path; | ||||||
|  |       else if (poster_path) | ||||||
|  |         return "https://image.tmdb.org/t/p/w185" + poster_path; | ||||||
|  |       else if (poster) return "https://image.tmdb.org/t/p/w185" + poster; | ||||||
|  |  | ||||||
|  |       return "/assets/no-image_small.svg"; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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; | ||||||
|  |  | ||||||
|  |   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> | ||||||
| @@ -1,87 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section> |  | ||||||
|     <LandingBanner /> |  | ||||||
|  |  | ||||||
|     <div v-for="list in lists"> |  | ||||||
|       <list-header :title="list.title" :link="'/list/' + list.route" /> |  | ||||||
|  |  | ||||||
|       <results-list :results="list.data" :shortList="true" /> |  | ||||||
|       <loader v-if="!list.data.length" /> |  | ||||||
|     </div> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import LandingBanner from "@/components/LandingBanner"; |  | ||||||
| import ListHeader from "@/components/ListHeader"; |  | ||||||
| import ResultsList from "@/components/ResultsList"; |  | ||||||
| import Loader from "@/components/ui/Loader"; |  | ||||||
|  |  | ||||||
| import { getTmdbMovieListByName, getRequests } from "@/api"; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   name: "home", |  | ||||||
|   components: { LandingBanner, ResultsList, ListHeader, Loader }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       imageFile: "/pulp-fiction.jpg", |  | ||||||
|       requests: [], |  | ||||||
|       nowplaying: [], |  | ||||||
|       upcoming: [], |  | ||||||
|       popular: [] |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     lists() { |  | ||||||
|       return [ |  | ||||||
|         { |  | ||||||
|           title: "Requests", |  | ||||||
|           route: "request", |  | ||||||
|           data: this.requests |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           title: "Now playing", |  | ||||||
|           route: "now_playing", |  | ||||||
|           data: this.nowplaying |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           title: "Upcoming", |  | ||||||
|           route: "upcoming", |  | ||||||
|           data: this.upcoming |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           title: "Popular", |  | ||||||
|           route: "popular", |  | ||||||
|           data: this.popular |  | ||||||
|         } |  | ||||||
|       ]; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     fetchRequests() { |  | ||||||
|       getRequests().then(results => (this.requests = results.results)); |  | ||||||
|     }, |  | ||||||
|     fetchNowPlaying() { |  | ||||||
|       getTmdbMovieListByName("now_playing").then( |  | ||||||
|         results => (this.nowplaying = results.results) |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     fetchUpcoming() { |  | ||||||
|       getTmdbMovieListByName("upcoming").then( |  | ||||||
|         results => (this.upcoming = results.results) |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     fetchPopular() { |  | ||||||
|       getTmdbMovieListByName("popular").then( |  | ||||||
|         results => (this.popular = results.results) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     this.fetchRequests(); |  | ||||||
|     this.fetchNowPlaying(); |  | ||||||
|     this.fetchUpcoming(); |  | ||||||
|     this.fetchPopular(); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
| @@ -1,14 +1,28 @@ | |||||||
| <template> | <template> | ||||||
|   <header v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }"> |   <header | ||||||
|  |     :class="{ expanded, noselect: true }" | ||||||
|  |     v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }" | ||||||
|  |   > | ||||||
|     <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="expanded = !expanded"> | ||||||
|  |       <IconExpand v-if="!expanded" /> | ||||||
|  |       <IconShrink v-else /> | ||||||
|     </div> |     </div> | ||||||
|   </header> |   </header> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import IconExpand from "../icons/IconExpand.vue"; | ||||||
|  | import IconShrink from "../icons/IconShrink.vue"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|  |   components: { IconExpand, IconShrink }, | ||||||
|   props: { |   props: { | ||||||
|     image: { |     image: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -17,24 +31,35 @@ export default { | |||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       imageFile: "/pulp-fiction.jpg" |       images: [ | ||||||
|  |         "pulp-fiction.jpg", | ||||||
|  |         "arrival.jpg", | ||||||
|  |         "dune.jpg", | ||||||
|  |         "mandalorian.jpg" | ||||||
|  |       ], | ||||||
|  |       imageFile: undefined, | ||||||
|  |       expanded: false | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   beforeMount() { |   beforeMount() { | ||||||
|     if (this.image && this.image.length > 0) { |     if (this.image && this.image.length > 0) { | ||||||
|       this.imageFile = this.image; |       this.imageFile = this.image; | ||||||
|  |     } else { | ||||||
|  |       this.imageFile = `/assets/${ | ||||||
|  |         this.images[Math.floor(Math.random() * this.images.length)] | ||||||
|  |       }`; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </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%; |   width: 100%; | ||||||
|   height: 200px; |   height: 25vh; | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
| @@ -43,8 +68,48 @@ header { | |||||||
|   background-position: 50% 50%; |   background-position: 50% 50%; | ||||||
|   position: relative; |   position: relative; | ||||||
|  |  | ||||||
|   @include tablet-min { |   &.expanded { | ||||||
|     height: 284px; |     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); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &:hover { | ||||||
|  |     .expand-icon { | ||||||
|  |       visibility: visible; | ||||||
|  |       opacity: 1; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:before { |   &:before { | ||||||
| @@ -54,8 +119,8 @@ header { | |||||||
|     left: 0; |     left: 0; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     background-color: $background-70; |     background-color: var(--background-70); | ||||||
|     transition: background-color 0.5s ease; |     transition: inherit; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .container { |   .container { | ||||||
| @@ -71,9 +136,10 @@ header { | |||||||
|     letter-spacing: 0.5px; |     letter-spacing: 0.5px; | ||||||
|     color: $text-color; |     color: $text-color; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|  |     opacity: 1; | ||||||
|  |  | ||||||
|     @include tablet-min { |     @include tablet-min { | ||||||
|       font-size: 28px; |       font-size: 2.5rem; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -83,9 +149,10 @@ header { | |||||||
|     font-weight: 300; |     font-weight: 300; | ||||||
|     color: $text-color-70; |     color: $text-color-70; | ||||||
|     margin: 5px 0; |     margin: 5px 0; | ||||||
|  |     opacity: 1; | ||||||
|  |  | ||||||
|     @include tablet-min { |     @include tablet-min { | ||||||
|       font-size: 16px; |       font-size: 1.3rem; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,25 @@ | |||||||
| <template> | <template> | ||||||
|   <header :class="{ 'sticky': sticky }"> |   <header> | ||||||
|     <h2>{{ title }}</h2> |     <h2>{{ prettify }}</h2> | ||||||
|  |     <h3>{{ subtitle }}</h3> | ||||||
|  |  | ||||||
|     <div v-if="info instanceof Array" class="flex flex-direction-column"> |     <router-link | ||||||
|       <span v-for="item in info" class="info">{{ item }}</span> |       v-if="shortList" | ||||||
|     </div> |       :to="urlify" | ||||||
|     <span v-else class="info">{{ info }}</span> |       class="view-more" | ||||||
|     <router-link v-if="link" :to="link" class='view-more' :aria-label="`View all ${title}`"> |       :aria-label="`View all ${title}`" | ||||||
|  |     > | ||||||
|       View All |       View All | ||||||
|     </router-link> |     </router-link> | ||||||
|   </header>   |  | ||||||
|  |     <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> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| @@ -19,10 +29,10 @@ export default { | |||||||
|       type: String, |       type: String, | ||||||
|       required: true |       required: true | ||||||
|     }, |     }, | ||||||
|     sticky: { |     subtitle: { | ||||||
|       type: Boolean, |       type: String, | ||||||
|       required: false, |       required: false, | ||||||
|       default: true |       default: null | ||||||
|     }, |     }, | ||||||
|     info: { |     info: { | ||||||
|       type: [String, Array], |       type: [String, Array], | ||||||
| @@ -31,34 +41,43 @@ export default { | |||||||
|     link: { |     link: { | ||||||
|       type: String, |       type: String, | ||||||
|       required: false |       required: false | ||||||
|  |     }, | ||||||
|  |     shortList: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     urlify: function () { | ||||||
|  |       return `/list/${this.title.toLowerCase().replace(" ", "_")}`; | ||||||
|  |     }, | ||||||
|  |     prettify: function () { | ||||||
|  |       return this.title.includes("_") | ||||||
|  |         ? this.title.split("_").join(" ") | ||||||
|  |         : this.title; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </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"; | ||||||
| @import './src/scss/main'; | @import "src/scss/main"; | ||||||
|  |  | ||||||
| header { | header { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   min-height: 80px; |  | ||||||
|   display: flex; |   display: flex; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   padding-left: 0.75rem; |   padding: 0.5rem 0.75rem; | ||||||
|   padding-right: 0.75rem; |   background-color: $background-color; | ||||||
|  |  | ||||||
|   &.sticky { |   position: sticky; | ||||||
|     background-color: $background-color; |   position: -webkit-sticky; | ||||||
|  |   top: $header-size; | ||||||
|     position: sticky; |   z-index: 1; | ||||||
|     position: -webkit-sticky; |  | ||||||
|     top: $header-size; |  | ||||||
|     z-index: 4; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   h2 { |   h2 { | ||||||
|     font-size: 1.4rem; |     font-size: 1.4rem; | ||||||
| @@ -72,16 +91,16 @@ header { | |||||||
|   .view-more { |   .view-more { | ||||||
|     font-size: 0.9rem; |     font-size: 0.9rem; | ||||||
|     font-weight: 300; |     font-weight: 300; | ||||||
|     letter-spacing: .5px; |     letter-spacing: 0.5px; | ||||||
|     color: $text-color-70; |     color: $text-color-70; | ||||||
|     text-decoration: none; |     text-decoration: none; | ||||||
|     transition: color .5s ease; |     transition: color 0.5s ease; | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |  | ||||||
|     &:after{ |     &:after { | ||||||
|       content: " →"; |       content: " →"; | ||||||
|     } |     } | ||||||
|     &:hover{ |     &:hover { | ||||||
|       color: $text-color; |       color: $text-color; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -89,18 +108,18 @@ header { | |||||||
|   .info { |   .info { | ||||||
|     font-size: 13px; |     font-size: 13px; | ||||||
|     font-weight: 300; |     font-weight: 300; | ||||||
|     letter-spacing: .5px; |     letter-spacing: 0.5px; | ||||||
|     color: $text-color; |     color: $text-color; | ||||||
|     text-decoration: none; |     text-decoration: none; | ||||||
|     text-align: right; |     text-align: right; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @include tablet-min { |   @include tablet-min { | ||||||
|     padding-left: 1.25rem;; |     padding-left: 1.25rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @include desktop-lg-min { |   @include desktop-lg-min { | ||||||
|     padding-left: 1.75rem; |     padding-left: 1.75rem; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | </style> | ||||||
| </style> |  | ||||||
|   | |||||||
| @@ -1,113 +0,0 @@ | |||||||
|  |  | ||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <list-header :title="listTitle" :info="info" :sticky="true" /> |  | ||||||
|  |  | ||||||
|     <results-list :results="results" v-if="results" /> |  | ||||||
|  |  | ||||||
|     <loader v-if="!results.length" /> |  | ||||||
|  |  | ||||||
|     <div v-if="page < totalPages" class="fullwidth-button"> |  | ||||||
|       <seasoned-button @click="loadMore">load more</seasoned-button> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import ListHeader from '@/components/ListHeader' |  | ||||||
| import ResultsList from '@/components/ResultsList' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' |  | ||||||
| import Loader from '@/components/ui/Loader' |  | ||||||
| import { getTmdbMovieListByName, getRequests } from '@/api' |  | ||||||
| import store from '@/store' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { ListHeader, ResultsList, SeasonedButton, Loader }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       legalTmdbLists: [ 'now_playing', 'upcoming', 'popular' ], |  | ||||||
|       results: [], |  | ||||||
|       page: 1, |  | ||||||
|       totalPages: 0, |  | ||||||
|       totalResults: 0, |  | ||||||
|       loading: true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     listTitle() { |  | ||||||
|       if (this.results.length === 0) |  | ||||||
|         return '' |  | ||||||
|  |  | ||||||
|       const routeListName = this.$route.params.name |  | ||||||
|       console.log('routelistname', routeListName) |  | ||||||
|       return routeListName.includes('_') ? routeListName.split('_').join(' ') : routeListName |  | ||||||
|     }, |  | ||||||
|     info() { |  | ||||||
|       if (this.results.length === 0) |  | ||||||
|         return [null, null] |  | ||||||
|       return [this.pageCount, this.resultCount] |  | ||||||
|     }, |  | ||||||
|     resultCount() { |  | ||||||
|       const loadedResults = this.results.length |  | ||||||
|       const totalResults = this.totalResults < 10000 ? this.totalResults : '∞' |  | ||||||
|       return `${loadedResults} of ${totalResults} results` |  | ||||||
|     }, |  | ||||||
|     pageCount() { |  | ||||||
|       return `Page ${this.page} of ${this.totalPages}` |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     loadMore() { |  | ||||||
|       console.log(this.$route) |  | ||||||
|       this.loading = true; |  | ||||||
|       this.page++ |  | ||||||
|  |  | ||||||
|       window.history.replaceState({}, 'search', `/#/${this.$route.fullPath}?page=${this.page}`) |  | ||||||
|       this.init() |  | ||||||
|     }, |  | ||||||
|     init() { |  | ||||||
|       const routeListName = this.$route.params.name |  | ||||||
|  |  | ||||||
|       if (routeListName === 'request') { |  | ||||||
|         getRequests(this.page) |  | ||||||
|           .then(results => { |  | ||||||
|             this.results = this.results.concat(...results.results) |  | ||||||
|             this.page = results.page |  | ||||||
|             this.totalPages = results.total_pages |  | ||||||
|             this.totalResults = results.total_results |  | ||||||
|           }) |  | ||||||
|       } else if (this.legalTmdbLists.includes(routeListName)) { |  | ||||||
|         getTmdbMovieListByName(routeListName, this.page) |  | ||||||
|           .then(results => { |  | ||||||
|             this.results = this.results.concat(...results.results) |  | ||||||
|             this.page = results.page |  | ||||||
|             this.totalPages = results.total_pages |  | ||||||
|             this.totalResults = results.total_results |  | ||||||
|           }) |  | ||||||
|       } else { |  | ||||||
|         // TODO handle if list is not found |  | ||||||
|         console.log('404 this is not a tmdb list') |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.loading = false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     if (this.results.length === 0) |  | ||||||
|       this.init() |  | ||||||
|  |  | ||||||
|     store.dispatch('documentTitle/updateTitle', `${this.$router.history.current.name} ${this.$route.params.name}`) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .fullwidth-button { |  | ||||||
|   width: 100%; |  | ||||||
|   margin: 1rem 0; |  | ||||||
|   padding-bottom: 2rem; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
| } |  | ||||||
| </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'; |  | ||||||
| export default { |  | ||||||
|   components: { Movie } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| @@ -1,104 +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'; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     id: { |  | ||||||
|       type: Number, |  | ||||||
|       required: true |  | ||||||
|     }, |  | ||||||
|     type: { |  | ||||||
|       type: String, |  | ||||||
|       required: true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   components: { Movie }, |  | ||||||
|   methods: { |  | ||||||
|     checkEventForEscapeKey(event) { |  | ||||||
|       if (event.keyCode == 27) { |  | ||||||
|         this.$popup.close() |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     window.addEventListener('keyup', this.checkEventForEscapeKey) |  | ||||||
|     document.getElementsByTagName("body")[0].classList += " no-scroll"; |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     window.removeEventListener('keyup', this.checkEventForEscapeKey) |  | ||||||
|     document.getElementsByTagName("body")[0].classList.remove("no-scroll"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| </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($dark, 0.93); |  | ||||||
|   -webkit-overflow-scrolling: touch; |  | ||||||
|   overflow: auto; |  | ||||||
|   &__box{ |  | ||||||
|     width: 100%; |  | ||||||
|     max-width: 768px; |  | ||||||
|     position: relative; |  | ||||||
|     z-index: 5; |  | ||||||
|     background: $background-color-secondary; |  | ||||||
|     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: $white; |  | ||||||
|     } |  | ||||||
|     &:before{ |  | ||||||
|       transform: rotate(45deg); |  | ||||||
|     } |  | ||||||
|     &:after{ |  | ||||||
|       transform: rotate(-45deg); |  | ||||||
|     } |  | ||||||
|     &:hover{ |  | ||||||
|       background: $green; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,249 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <li class="movie-item" :class="{ shortList: shortList }"> |  | ||||||
|     <figure class="movie-item__poster"> |  | ||||||
|       <img |  | ||||||
|         class="movie-item__img" |  | ||||||
|         ref="poster-image" |  | ||||||
|         @click="openMoviePopup(movie.id, movie.type)" |  | ||||||
|         :alt="posterAltText" |  | ||||||
|         :data-src="poster" |  | ||||||
|         src="~assets/placeholder.png" |  | ||||||
|       /> |  | ||||||
|  |  | ||||||
|       <div v-if="movie.download" class="progress"> |  | ||||||
|         <progress :value="movie.download.progress" max="100"></progress> |  | ||||||
|         <span>{{ movie.download.state }}: {{ movie.download.progress }}%</span> |  | ||||||
|       </div> |  | ||||||
|     </figure> |  | ||||||
|  |  | ||||||
|     <div class="movie-item__info"> |  | ||||||
|       <p v-if="movie.title || movie.name">{{ movie.title || movie.name }}</p> |  | ||||||
|       <p v-if="movie.year">{{ movie.year }}</p> |  | ||||||
|       <p v-if="movie.type == 'person'"> |  | ||||||
|         Known for: {{ movie.known_for_department }} |  | ||||||
|       </p> |  | ||||||
|     </div> |  | ||||||
|   </li> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import img from "../directives/v-image"; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     movie: { |  | ||||||
|       type: Object, |  | ||||||
|       required: true |  | ||||||
|     }, |  | ||||||
|     shortList: { |  | ||||||
|       type: Boolean, |  | ||||||
|       required: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   directives: { |  | ||||||
|     img: img |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       poster: undefined, |  | ||||||
|       observed: false, |  | ||||||
|       posterSizes: [ |  | ||||||
|         { |  | ||||||
|           id: "w500", |  | ||||||
|           minWidth: 500 |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           id: "w342", |  | ||||||
|           minWidth: 342 |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           id: "w185", |  | ||||||
|           minWidth: 185 |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           id: "w154", |  | ||||||
|           minWidth: 0 |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     posterAltText: function () { |  | ||||||
|       const type = this.movie.type || ""; |  | ||||||
|       const title = this.movie.title || this.movie.name; |  | ||||||
|       return this.movie.poster |  | ||||||
|         ? `Poster for ${type} ${title}` |  | ||||||
|         : `Missing image for ${type} ${title}`; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     if (this.movie.poster != null) { |  | ||||||
|       this.poster = "https://image.tmdb.org/t/p/w500" + this.movie.poster; |  | ||||||
|     } else { |  | ||||||
|       this.poster = "/no-image.png"; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     const poster = this.$refs["poster-image"]; |  | ||||||
|     if (poster == null) return; |  | ||||||
|  |  | ||||||
|     const imageObserver = new IntersectionObserver((entries, imgObserver) => { |  | ||||||
|       entries.forEach(entry => { |  | ||||||
|         if (entry.isIntersecting && this.observed == false) { |  | ||||||
|           const lazyImage = entry.target; |  | ||||||
|           lazyImage.src = lazyImage.dataset.src; |  | ||||||
|           lazyImage.className = lazyImage.className + " is-loaded"; |  | ||||||
|           this.observed = true; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     imageObserver.observe(poster); |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     openMoviePopup(id, type) { |  | ||||||
|       this.$popup.open(id, type); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
| @import "./src/scss/main"; |  | ||||||
|  |  | ||||||
| .movie-item { |  | ||||||
|   padding: 10px; |  | ||||||
|   width: 50%; |  | ||||||
|   background-color: $background-color; |  | ||||||
|   transition: background-color 0.5s ease; |  | ||||||
|  |  | ||||||
|   @include tablet-min { |  | ||||||
|     padding: 15px; |  | ||||||
|     width: 33%; |  | ||||||
|   } |  | ||||||
|   @include tablet-landscape-min { |  | ||||||
|     padding: 15px; |  | ||||||
|     width: 25%; |  | ||||||
|   } |  | ||||||
|   @include desktop-min { |  | ||||||
|     padding: 15px; |  | ||||||
|     width: 20%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include desktop-lg-min { |  | ||||||
|     padding: 15px; |  | ||||||
|     width: 12.5%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &:hover &__info > p { |  | ||||||
|     color: $text-color; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__poster { |  | ||||||
|     text-decoration: none; |  | ||||||
|     color: $text-color-70; |  | ||||||
|     font-weight: 300; |  | ||||||
|  |  | ||||||
|     > img { |  | ||||||
|       width: 100%; |  | ||||||
|       opacity: 0; |  | ||||||
|       transform: scale(0.97) translateZ(0); |  | ||||||
|       transition: opacity 1s ease, transform 0.5s ease; |  | ||||||
|       &.is-loaded { |  | ||||||
|         opacity: 1; |  | ||||||
|         transform: scale(1); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       &:hover { |  | ||||||
|         transform: scale(1.03); |  | ||||||
|         box-shadow: 0 0 10px rgba($dark, 0.1); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__info { |  | ||||||
|     padding-top: 15px; |  | ||||||
|     font-weight: 300; |  | ||||||
|  |  | ||||||
|     > p { |  | ||||||
|       color: $text-color-70; |  | ||||||
|       margin: 0; |  | ||||||
|       font-size: 11px; |  | ||||||
|       letter-spacing: 0.5px; |  | ||||||
|       transition: color 0.5s ease; |  | ||||||
|       cursor: pointer; |  | ||||||
|       @include mobile-ls-min { |  | ||||||
|         font-size: 12px; |  | ||||||
|       } |  | ||||||
|       @include tablet-min { |  | ||||||
|         font-size: 14px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .no-image { |  | ||||||
|   background-color: var(--text-color); |  | ||||||
|   color: var(--background-color); |  | ||||||
|   width: 100%; |  | ||||||
|   height: 383px; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|  |  | ||||||
|   span { |  | ||||||
|     font-size: 1.5rem; |  | ||||||
|     width: 70%; |  | ||||||
|     text-align: center; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &:hover { |  | ||||||
|     transform: scale(1); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
|  |  | ||||||
| .progress { |  | ||||||
|   position: absolute; |  | ||||||
|   bottom: 0; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   align-items: center; |  | ||||||
|   width: 100%; |  | ||||||
|   margin-bottom: 0.8rem; |  | ||||||
|  |  | ||||||
|   > progress { |  | ||||||
|     width: 95%; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   > span { |  | ||||||
|     position: absolute; |  | ||||||
|     font-size: 1rem; |  | ||||||
|     line-height: 1.4rem; |  | ||||||
|     color: $white; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   progress { |  | ||||||
|     border-radius: 4px; |  | ||||||
|     height: 1.4rem; |  | ||||||
|   } |  | ||||||
|   progress::-webkit-progress-bar { |  | ||||||
|     background-color: rgba($black, 0.55); |  | ||||||
|     border-radius: 4px; |  | ||||||
|   } |  | ||||||
|   progress::-webkit-progress-value { |  | ||||||
|     background-color: $green-70; |  | ||||||
|     border-radius: 4px; |  | ||||||
|   } |  | ||||||
|   progress::-moz-progress-bar { |  | ||||||
|     /* style rules */ |  | ||||||
|     background-color: green; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,314 +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 v-for="_ in 3" 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"> |  | ||||||
|               <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' |  | ||||||
|  |  | ||||||
| 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(){ |  | ||||||
|     // TODO move this to state manager |  | ||||||
|     eventHub.$on('setUserStatus', this.setUserStatus); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| .icon { |  | ||||||
|   width: 30px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .spacer { |  | ||||||
|   @include mobile-only { |  | ||||||
|     width: 100%; |  | ||||||
|     height: $header-size; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .nav { |  | ||||||
|   transition: background .5s ease; |  | ||||||
|   position: fixed; |  | ||||||
|   top: 0; |  | ||||||
|   left: 0; |  | ||||||
|   width: 100%; |  | ||||||
|   height: 50px; |  | ||||||
|   z-index: 10; |  | ||||||
|   display: block; |  | ||||||
|   color: $text-color; |  | ||||||
|   background-color: $background-color-secondary; |  | ||||||
|  |  | ||||||
|   @include tablet-min{ |  | ||||||
|     width: 95px; |  | ||||||
|     height: 100vh; |  | ||||||
|   } |  | ||||||
|   &__logo { |  | ||||||
|     width: 55px; |  | ||||||
|     height: $header-size; |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: center; |  | ||||||
|     background: $background-nav-logo; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       width: 95px; |  | ||||||
|     } |  | ||||||
|     &-image{ |  | ||||||
|       width: 35px; |  | ||||||
|       height: 31px; |  | ||||||
|       fill: $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; |  | ||||||
|     z-index: 10; |  | ||||||
|     border-left: 1px solid $background-color; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     .bar { |  | ||||||
|       position: absolute; |  | ||||||
|       width: 23px; |  | ||||||
|       height: 1px; |  | ||||||
|       background-color: $text-color-70; |  | ||||||
|       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; |  | ||||||
|           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); |  | ||||||
|           background-color: $text-color-70; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__list { |  | ||||||
|     list-style: none; |  | ||||||
|     padding: 0; |  | ||||||
|     margin: 0; |  | ||||||
|     text-align: center; |  | ||||||
|     width: 100%; |  | ||||||
|     position: fixed; |  | ||||||
|     left: 0; |  | ||||||
|     top: 50px; |  | ||||||
|     border-top: 1px solid $background-color; |  | ||||||
|     @include mobile-only { |  | ||||||
|       display: flex; |  | ||||||
|       flex-wrap: wrap; |  | ||||||
|       font-size: 0; |  | ||||||
|       opacity: 0; |  | ||||||
|       visibility: hidden; |  | ||||||
|       background-color: $background-95; |  | ||||||
|       text-align: left; |  | ||||||
|       &--active{ |  | ||||||
|         opacity: 1; |  | ||||||
|         visibility: visible; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     @include tablet-min { |  | ||||||
|       display: flex; |  | ||||||
|       position: relative; |  | ||||||
|       display: block; |  | ||||||
|       width: 100%; |  | ||||||
|       border-top: 0; |  | ||||||
|       top: 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__item { |  | ||||||
|     transition: background .5s ease, color .5s ease, border .5s ease; |  | ||||||
|     background-color: $background-color-secondary; |  | ||||||
|     color: $text-color-70; |  | ||||||
|  |  | ||||||
|     @include mobile-only { |  | ||||||
|       flex: 0 0 50%; |  | ||||||
|       text-align: center; |  | ||||||
|       border-bottom: 1px solid $background-color; |  | ||||||
|       &:nth-child(odd){ |  | ||||||
|         border-right: 1px solid $background-color; |  | ||||||
|  |  | ||||||
|         &:last-child { |  | ||||||
|           // flex: 0 0 100%; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     @include tablet-min { |  | ||||||
|       width: 100%; |  | ||||||
|       border-bottom: 1px solid $text-color-5; |  | ||||||
|  |  | ||||||
|       &--profile { |  | ||||||
|         position: fixed; |  | ||||||
|         right: 0; |  | ||||||
|         top: 0; |  | ||||||
|         width: $header-size; |  | ||||||
|         height: $header-size; |  | ||||||
|         border-bottom: 0; |  | ||||||
|         border-left: 1px solid $background-color; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &:hover, .is-active { |  | ||||||
|       color: $text-color; |  | ||||||
|       background-color: $background-color; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__link { |  | ||||||
|     background-color: inherit; // a elements have a transparent background |  | ||||||
|     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; |  | ||||||
|     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-color: $background-color-secondary; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &-icon { |  | ||||||
|       width: 20px; |  | ||||||
|       height: 20px; |  | ||||||
|       fill: $text-color-70; |  | ||||||
|       @include tablet-min { |  | ||||||
|         width: 20px; |  | ||||||
|         height: 20px; |  | ||||||
|         margin-bottom: 5px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|     &-title { |  | ||||||
|       margin-top: 5px; |  | ||||||
|       display: block; |  | ||||||
|       width: 100%; |  | ||||||
|     } |  | ||||||
|     &:hover &-icon, &.is-active &-icon { |  | ||||||
|       fill: $text-color; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </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> |  | ||||||
							
								
								
									
										125
									
								
								src/components/Popup.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,125 @@ | |||||||
|  | <template> | ||||||
|  |   <div v-if="isOpen" class="movie-popup" @click="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> | ||||||
|  | import { mapActions, mapGetters } from "vuex"; | ||||||
|  | import Movie from "@/components/popup/Movie"; | ||||||
|  | import Person from "@/components/popup/Person"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { Movie, Person }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters("popup", ["isOpen", "id", "type"]) | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     isOpen(value) { | ||||||
|  |       value | ||||||
|  |         ? document.getElementsByTagName("body")[0].classList.add("no-scroll") | ||||||
|  |         : document | ||||||
|  |             .getElementsByTagName("body")[0] | ||||||
|  |             .classList.remove("no-scroll"); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapActions("popup", ["close", "open"]), | ||||||
|  |     checkEventForEscapeKey(event) { | ||||||
|  |       if (event.keyCode == 27) this.close(); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     const params = new URLSearchParams(window.location.search); | ||||||
|  |     let id = null; | ||||||
|  |     let type = null; | ||||||
|  |  | ||||||
|  |     if (params.has("movie")) { | ||||||
|  |       id = Number(params.get("movie")); | ||||||
|  |       type = "movie"; | ||||||
|  |     } else if (params.has("show")) { | ||||||
|  |       id = Number(params.get("show")); | ||||||
|  |       type = "show"; | ||||||
|  |     } else if (params.has("person")) { | ||||||
|  |       id = Number(params.get("person")); | ||||||
|  |       type = "person"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (id && type) { | ||||||
|  |       this.open({ id, type }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     window.addEventListener("keyup", this.checkEventForEscapeKey); | ||||||
|  |   }, | ||||||
|  |   beforeDestroy() { | ||||||
|  |     window.removeEventListener("keyup", this.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,150 +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="toggleSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button> |  | ||||||
|  |  | ||||||
|           <seasoned-button @click="logOut">Log out</seasoned-button> |  | ||||||
|         </div> |  | ||||||
|       </header> |  | ||||||
|  |  | ||||||
|       <settings v-if="showSettings"></settings> |  | ||||||
|  |  | ||||||
|       <list-header title="User requests" :info="resultCount" /> |  | ||||||
|       <results-list v-if="results" :results="results" /> |  | ||||||
|     </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' |  | ||||||
| import store from '@/store' |  | ||||||
| import ListHeader from '@/components/ListHeader' |  | ||||||
| import ResultsList from '@/components/ResultsList' |  | ||||||
| import Settings from '@/components/Settings' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' |  | ||||||
|  |  | ||||||
| import { getEmoji, getUserRequests } from '@/api' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { ListHeader, ResultsList, Settings, SeasonedButton }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       userLoggedIn: '', |  | ||||||
|       emoji: '', |  | ||||||
|       results: undefined, |  | ||||||
|       totalResults: undefined, |  | ||||||
|       showSettings: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     resultCount() { |  | ||||||
|       if (this.results === undefined) |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|       const loadedResults = this.results.length |  | ||||||
|       const totalResults = this.totalResults < 10000 ? this.totalResults : '∞' |  | ||||||
|       return `${loadedResults} of ${totalResults} results` |  | ||||||
|     }, |  | ||||||
|     username: () => store.getters['userModule/username'] |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     toggleSettings() { |  | ||||||
|       this.showSettings = this.showSettings ? false : true; |  | ||||||
|  |  | ||||||
|       if (this.showSettings) { |  | ||||||
|         this.$router.replace({ query: { settings: true} }) |  | ||||||
|       } else { |  | ||||||
|         this.$router.replace({ name: 'profile' }) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     logOut(){ |  | ||||||
|       this.$router.push('logout') |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     if(!localStorage.getItem('token')){ |  | ||||||
|       this.userLoggedIn = false; |  | ||||||
|     } else { |  | ||||||
|       this.userLoggedIn = true; |  | ||||||
|  |  | ||||||
|       this.showSettings = window.location.toString().includes('settings=true') |  | ||||||
|  |  | ||||||
|       getUserRequests() |  | ||||||
|         .then(results => { |  | ||||||
|           this.results = results.results |  | ||||||
|           this.totalResults = results.total_results |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
|       getEmoji() |  | ||||||
|         .then(resp => { |  | ||||||
|           const { emoji } = resp |  | ||||||
|           this.emoji = emoji |  | ||||||
|           store.dispatch('documentTitle/updateEmoji', emoji) |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| .button--group { |  | ||||||
|   display: flex; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DUPLICATE CODE |  | ||||||
| .profile{ |  | ||||||
|   &__header{ |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: space-between; |  | ||||||
|     padding: 20px; |  | ||||||
|     border-bottom: 1px solid $text-color-5; |  | ||||||
|  |  | ||||||
|     @include mobile-only { |  | ||||||
|       flex-direction: column; |  | ||||||
|       align-items: flex-start; |  | ||||||
|  |  | ||||||
|       .button--group { |  | ||||||
|         padding-top: 2rem; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @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: $text-color; |  | ||||||
|     font-weight: 300; |  | ||||||
|     @include tablet-min{ |  | ||||||
|       font-size: 18px; |  | ||||||
|       line-height: 18px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,110 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section> |  | ||||||
|     <h1>Register new user</h1> |  | ||||||
|  |  | ||||||
|     <seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" @enter="submit"/> |  | ||||||
|  |  | ||||||
|     <seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/> |  | ||||||
|     <seasoned-input placeholder="repeat password" icon="Keyhole" type="password" :value.sync="passwordRepeat" @enter="submit"/> |  | ||||||
|  |  | ||||||
|     <seasoned-button @click="submit">Register</seasoned-button> |  | ||||||
|     <router-link class="link" to="/signin">Have a user? Sign in here</router-link> |  | ||||||
|  |  | ||||||
|     <seasoned-messages :messages.sync="messages"></seasoned-messages> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import { register } from '@/api' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' |  | ||||||
| import SeasonedInput from '@/components/ui/SeasonedInput' |  | ||||||
| import SeasonedMessages from '@/components/ui/SeasonedMessages' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { SeasonedButton, SeasonedInput, SeasonedMessages }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       messages: [], |  | ||||||
|       username: null, |  | ||||||
|       password: null, |  | ||||||
|       passwordRepeat: null |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     submit() { |  | ||||||
|       this.messages = []; |  | ||||||
|       let { username, password, passwordRepeat } = this; |  | ||||||
|  |  | ||||||
|       if (username == null || username.length == 0) { |  | ||||||
|         this.messages.push({ type: 'error', title: 'Missing username' }) |  | ||||||
|         return |  | ||||||
|       } else if (password == null || password.length == 0) { |  | ||||||
|         this.messages.push({ type: 'error', title: 'Missing password' }) |  | ||||||
|         return |  | ||||||
|       } else if (passwordRepeat == null || passwordRepeat.length == 0) { |  | ||||||
|         this.messages.push({ type: 'error', title: 'Missing repeat password' }) |  | ||||||
|         return |  | ||||||
|       } else if (passwordRepeat != password) { |  | ||||||
|         this.messages.push({ type: 'error', title: 'Passwords do not match' }) |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.registerUser(username, password) |  | ||||||
|     }, |  | ||||||
|     registerUser(username, password) { |  | ||||||
|       register(username, password, true) |  | ||||||
|         .then(data => { |  | ||||||
|           if (data.success){ |  | ||||||
|             localStorage.setItem('token', data.token); |  | ||||||
|             const jwtData = parseJwt(data.token) |  | ||||||
|             localStorage.setItem('username', jwtData['username']); |  | ||||||
|             localStorage.setItem('admin', jwtData['admin'] || false); |  | ||||||
|  |  | ||||||
|             eventHub.$emit('setUserStatus'); |  | ||||||
|             this.$router.push({ name: 'profile' }) |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         .catch(error => { |  | ||||||
|           if (error.status === 401) { |  | ||||||
|             this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' }) |  | ||||||
|           } |  | ||||||
|           else { |  | ||||||
|             this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message }) |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|     }, |  | ||||||
|     logOut(){ |  | ||||||
|       localStorage.clear(); |  | ||||||
|       eventHub.$emit('setUserStatus'); |  | ||||||
|       this.$router.push({ name: 'home' }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
|  |  | ||||||
| section { |  | ||||||
|   padding: 1.3rem; |  | ||||||
|  |  | ||||||
|   @include tablet-min { |  | ||||||
|     padding: 4rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   h1 { |  | ||||||
|     margin: 0; |  | ||||||
|     line-height: 16px; |  | ||||||
|     color: $text-color; |  | ||||||
|     font-weight: 300; |  | ||||||
|     margin-bottom: 20px; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .link { |  | ||||||
|     display: block; |  | ||||||
|     width: max-content; |  | ||||||
|     margin-top: 1rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,15 +1,26 @@ | |||||||
| <template>  | <template> | ||||||
|   <ul class="results" :class="{'shortList': shortList}"> |   <div> | ||||||
|     <movies-list-item v-for='movie in results' :movie="movie" /> |     <ul | ||||||
|   </ul> |       v-if="results && results.length" | ||||||
|  |       class="results" | ||||||
|  |       :class="{ shortList: shortList }" | ||||||
|  |     > | ||||||
|  |       <results-list-item | ||||||
|  |         v-for="(movie, index) in results" | ||||||
|  |         :key="`${movie.type}-${movie.id}-${index}`" | ||||||
|  |         :movie="movie" | ||||||
|  |       /> | ||||||
|  |     </ul> | ||||||
|  |  | ||||||
|  |     <span v-else-if="!loading" class="no-results">No results found</span> | ||||||
|  |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import MoviesListItem from '@/components/MoviesListItem' | import ResultsListItem from "@/components/ResultsListItem"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { MoviesListItem }, |   components: { ResultsListItem }, | ||||||
|   props: { |   props: { | ||||||
|     results: { |     results: { | ||||||
|       type: Array, |       type: Array, | ||||||
| @@ -19,50 +30,54 @@ export default { | |||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       required: false, |       required: false, | ||||||
|       default: false |       default: false | ||||||
|  |     }, | ||||||
|  |     loading: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }   | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  | @import "src/scss/media-queries"; | ||||||
|  | @import "src/scss/main"; | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | .no-results { | ||||||
| @import './src/scss/media-queries'; |   width: 100%; | ||||||
|  |   display: block; | ||||||
|  |   text-align: center; | ||||||
|  |   margin: 1.5rem; | ||||||
|  |   font-size: 1.2rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .results { | .results { | ||||||
|   display: flex; |   display: grid; | ||||||
|   flex-wrap: wrap; |   grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); | ||||||
|  |   grid-auto-rows: auto; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   padding: 0; |   padding: 0; | ||||||
|   list-style: none; |   list-style: none; | ||||||
|  |  | ||||||
|   &.shortList > li { |   @include mobile { | ||||||
|     display: none; |     grid-template-columns: repeat(2, 1fr); | ||||||
|  |   } | ||||||
|  |  | ||||||
|     &:nth-child(-n+4) { |   &.shortList { | ||||||
|       display: block; |     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> | ||||||
| @include tablet-min { |  | ||||||
|   .results.shortList > li:nth-child(-n+6) { |  | ||||||
|     display: block; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @include tablet-landscape-min { |  | ||||||
|   .results.shortList > li:nth-child(-n+8) { |  | ||||||
|     display: block; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @include desktop-min { |  | ||||||
|   .results.shortList > li:nth-child(-n+10) { |  | ||||||
|     display: block; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @include desktop-lg-min { |  | ||||||
|   .results.shortList > li:nth-child(-n+16) { |  | ||||||
|     display: block; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
|   | |||||||
							
								
								
									
										180
									
								
								src/components/ResultsListItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,180 @@ | |||||||
|  | <template> | ||||||
|  |   <li class="movie-item" ref="list-item"> | ||||||
|  |     <figure ref="poster" class="movie-item__poster" @click="openMoviePopup"> | ||||||
|  |       <img | ||||||
|  |         class="movie-item__img" | ||||||
|  |         :alt="posterAltText" | ||||||
|  |         :data-src="poster" | ||||||
|  |         src="/assets/placeholder.png" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <div v-if="movie.download" class="progress"> | ||||||
|  |         <progress :value="movie.download.progress" max="100"></progress> | ||||||
|  |         <span>{{ movie.download.state }}: {{ movie.download.progress }}%</span> | ||||||
|  |       </div> | ||||||
|  |     </figure> | ||||||
|  |  | ||||||
|  |     <div class="movie-item__info"> | ||||||
|  |       <p v-if="movie.title || movie.name" class="movie-item__title"> | ||||||
|  |         {{ movie.title || movie.name }} | ||||||
|  |       </p> | ||||||
|  |       <p v-if="movie.year">{{ movie.year }}</p> | ||||||
|  |       <p v-if="movie.type == 'person'"> | ||||||
|  |         Known for: {{ movie.known_for_department }} | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </li> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { mapActions } from "vuex"; | ||||||
|  | import img from "../directives/v-image"; | ||||||
|  | import { buildImageProxyUrl } from "../utils"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     movie: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   directives: { | ||||||
|  |     img: img | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       poster: null, | ||||||
|  |       observed: false | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     posterAltText: function () { | ||||||
|  |       const type = this.movie.type || ""; | ||||||
|  |       const title = this.movie.title || this.movie.name; | ||||||
|  |       return this.movie.poster | ||||||
|  |         ? `Poster for ${type} ${title}` | ||||||
|  |         : `Missing image for ${type} ${title}`; | ||||||
|  |     }, | ||||||
|  |     imageWidth() { | ||||||
|  |       if (this.image) | ||||||
|  |         return Math.ceil(this.image.getBoundingClientRect().width); | ||||||
|  |     }, | ||||||
|  |     imageHeight() { | ||||||
|  |       if (this.image) | ||||||
|  |         return Math.ceil(this.image.getBoundingClientRect().height); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   beforeMount() { | ||||||
|  |     if (this.movie.poster == null) { | ||||||
|  |       this.poster = "/assets/no-image.svg"; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.poster = `https://image.tmdb.org/t/p/w500${this.movie.poster}`; | ||||||
|  |     // this.poster = this.buildProxyURL( | ||||||
|  |     //   this.imageWidth, | ||||||
|  |     //   this.imageHeight, | ||||||
|  |     //   assetUrl | ||||||
|  |     // ); | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     const poster = this.$refs["poster"]; | ||||||
|  |     this.image = poster.getElementsByTagName("img")[0]; | ||||||
|  |     if (this.image == null) return; | ||||||
|  |  | ||||||
|  |     const imageObserver = new IntersectionObserver((entries, imgObserver) => { | ||||||
|  |       entries.forEach(entry => { | ||||||
|  |         if (entry.isIntersecting && this.observed == false) { | ||||||
|  |           const lazyImage = entry.target; | ||||||
|  |           lazyImage.src = lazyImage.dataset.src; | ||||||
|  |           poster.className = poster.className + " is-loaded"; | ||||||
|  |           this.observed = true; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     imageObserver.observe(this.image); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapActions("popup", ["open"]), | ||||||
|  |     openMoviePopup() { | ||||||
|  |       this.open({ | ||||||
|  |         id: this.movie.id, | ||||||
|  |         type: this.movie.type | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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> | ||||||
							
								
								
									
										202
									
								
								src/components/ResultsSection.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,202 @@ | |||||||
|  | <template> | ||||||
|  |   <div ref="resultSection" class="resultSection"> | ||||||
|  |     <list-header v-bind="{ title, info, shortList }" /> | ||||||
|  |  | ||||||
|  |     <div | ||||||
|  |       v-if="!loadedPages.includes(1) && loading == false" | ||||||
|  |       class="button-container" | ||||||
|  |     > | ||||||
|  |       <seasoned-button @click="loadLess" class="load-button" :fullWidth="true" | ||||||
|  |         >load previous</seasoned-button | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <results-list v-bind="{ results, shortList, loading }" /> | ||||||
|  |     <loader v-if="loading" /> | ||||||
|  |  | ||||||
|  |     <div ref="loadMoreButton" class="button-container"> | ||||||
|  |       <seasoned-button | ||||||
|  |         class="load-button" | ||||||
|  |         v-if="!loading && !shortList && page != totalPages && results.length" | ||||||
|  |         @click="loadMore" | ||||||
|  |         :fullWidth="true" | ||||||
|  |         >load more</seasoned-button | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import ListHeader from "@/components/ListHeader"; | ||||||
|  | import ResultsList from "@/components/ResultsList"; | ||||||
|  | import SeasonedButton from "@/components/ui/SeasonedButton"; | ||||||
|  | import store from "@/store"; | ||||||
|  | import { getTmdbMovieListByName } from "@/api"; | ||||||
|  | import Loader from "@/components/ui/Loader"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     apiFunction: { | ||||||
|  |       type: Function, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     title: { | ||||||
|  |       type: String, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     shortList: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   components: { ListHeader, ResultsList, SeasonedButton, Loader }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       results: [], | ||||||
|  |       page: 1, | ||||||
|  |       loadedPages: [], | ||||||
|  |       totalPages: -1, | ||||||
|  |       totalResults: 0, | ||||||
|  |       loading: true, | ||||||
|  |       autoLoad: false, | ||||||
|  |       observer: undefined | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     info() { | ||||||
|  |       if (this.results.length === 0) return [null, null]; | ||||||
|  |       return [this.pageCount, this.resultCount]; | ||||||
|  |     }, | ||||||
|  |     resultCount() { | ||||||
|  |       const loadedResults = this.results.length; | ||||||
|  |       const totalResults = this.totalResults < 10000 ? this.totalResults : "∞"; | ||||||
|  |       return `${loadedResults} of ${totalResults} results`; | ||||||
|  |     }, | ||||||
|  |     pageCount() { | ||||||
|  |       return `Page ${this.page} of ${this.totalPages}`; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     loadMore() { | ||||||
|  |       if (!this.autoLoad) { | ||||||
|  |         this.autoLoad = true; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.loading = true; | ||||||
|  |       let maxPage = [...this.loadedPages].slice(-1)[0]; | ||||||
|  |  | ||||||
|  |       if (maxPage == NaN) return; | ||||||
|  |       this.page = maxPage + 1; | ||||||
|  |       this.getListResults(); | ||||||
|  |     }, | ||||||
|  |     loadLess() { | ||||||
|  |       this.loading = true; | ||||||
|  |       const minPage = this.loadedPages[0]; | ||||||
|  |       if (minPage === 1) return; | ||||||
|  |  | ||||||
|  |       this.page = minPage - 1; | ||||||
|  |       this.getListResults(true); | ||||||
|  |     }, | ||||||
|  |     updateQueryParams() { | ||||||
|  |       let params = new URLSearchParams(window.location.search); | ||||||
|  |       if (params.has("page")) { | ||||||
|  |         params.set("page", this.page); | ||||||
|  |       } else if (this.page > 1) { | ||||||
|  |         params.append("page", this.page); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       window.history.replaceState( | ||||||
|  |         {}, | ||||||
|  |         "search", | ||||||
|  |         `${window.location.protocol}//${window.location.hostname}${ | ||||||
|  |           window.location.port ? `:${window.location.port}` : "" | ||||||
|  |         }${window.location.pathname}${ | ||||||
|  |           params.toString().length ? `?${params}` : "" | ||||||
|  |         }` | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |     getPageFromUrl() { | ||||||
|  |       return new URLSearchParams(window.location.search).get("page"); | ||||||
|  |     }, | ||||||
|  |     getListResults(front = false) { | ||||||
|  |       this.apiFunction(this.page) | ||||||
|  |         .then(results => { | ||||||
|  |           if (!front) this.results = this.results.concat(...results.results); | ||||||
|  |           else this.results = results.results.concat(...this.results); | ||||||
|  |           this.page = results.page; | ||||||
|  |           this.loadedPages.push(this.page); | ||||||
|  |           this.loadedPages = this.loadedPages.sort((a, b) => a - b); | ||||||
|  |           this.totalPages = results.total_pages; | ||||||
|  |           this.totalResults = results.total_results; | ||||||
|  |         }) | ||||||
|  |         .then(this.updateQueryParams) | ||||||
|  |         .finally(() => (this.loading = false)); | ||||||
|  |     }, | ||||||
|  |     setupAutoloadObserver() { | ||||||
|  |       this.observer = new IntersectionObserver(this.handleButtonIntersection, { | ||||||
|  |         root: this.$refs.resultSection.$el, | ||||||
|  |         rootMargin: "0px", | ||||||
|  |         threshold: 0 | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       this.observer.observe(this.$refs.loadMoreButton); | ||||||
|  |     }, | ||||||
|  |     handleButtonIntersection(entries) { | ||||||
|  |       entries.map(entry => | ||||||
|  |         entry.isIntersecting && this.autoLoad ? this.loadMore() : null | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.page = this.getPageFromUrl() || this.page; | ||||||
|  |     if (this.results.length === 0) this.getListResults(); | ||||||
|  |  | ||||||
|  |     if (!this.shortList) { | ||||||
|  |       store.dispatch( | ||||||
|  |         "documentTitle/updateTitle", | ||||||
|  |         `${this.$router.history.current.name} ${this.title}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     if (!this.shortList) { | ||||||
|  |       this.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,122 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <list-header :title="title" :info="resultCount" :sticky="true" /> |  | ||||||
|  |  | ||||||
|     <results-list :results="results" /> |  | ||||||
|      |  | ||||||
|     <div v-if="page < totalPages" class="fullwidth-button"> |  | ||||||
|       <seasoned-button @click="loadMore">load more</seasoned-button> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div class="notFound" v-if="results.length == 0 && loading == false"> |  | ||||||
|       <h1 class="notFound-title">No results for search: <b>{{ query }}</b></h1> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <loader v-if="loading" /> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .notFound { |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   align-items: center; |  | ||||||
|  |  | ||||||
|   &-title { |  | ||||||
|     font-weight: 400; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import { searchTmdb } from '@/api' |  | ||||||
| import ListHeader from '@/components/ListHeader' |  | ||||||
| import ResultsList from '@/components/ResultsList' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' |  | ||||||
| import Loader from '@/components/ui/Loader' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { ListHeader, ResultsList, SeasonedButton, Loader }, |  | ||||||
|   props: { |  | ||||||
|     propQuery: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     propPage: { |  | ||||||
|       type: Number, |  | ||||||
|       required: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       loading: true, |  | ||||||
|       query: String, |  | ||||||
|       title: String, |  | ||||||
|       page: Number, |  | ||||||
|       adult: undefined, |  | ||||||
|       mediaType: null, |  | ||||||
|       totalPages: 0, |  | ||||||
|       results: [], |  | ||||||
|       totalResults: [] |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     resultCount() { |  | ||||||
|       const loadedResults = this.results.length |  | ||||||
|       const totalResults = this.totalResults < 10000 ? this.totalResults : '∞' |  | ||||||
|       return `${loadedResults} of ${totalResults} results` |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     search(query=this.query, page=this.page, adult=this.adult, mediaType=this.mediaType) { |  | ||||||
|       searchTmdb(query, page, adult, mediaType) |  | ||||||
|         .then(this.parseResponse) |  | ||||||
|     }, |  | ||||||
|     parseResponse(data) { |  | ||||||
|       if (this.results.length > 0) { |  | ||||||
|         this.results.push(...data.results) |  | ||||||
|       } else { |  | ||||||
|         this.results = data.results |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.totalPages = data.total_pages |  | ||||||
|       this.totalResults = data.total_results || data.results.length |  | ||||||
|  |  | ||||||
|       this.loading = false |  | ||||||
|     }, |  | ||||||
|     loadMore() { |  | ||||||
|       this.page++ |  | ||||||
|  |  | ||||||
|       window.history.replaceState({}, 'search', `/#/search?query=${this.query}&page=${this.page}`) |  | ||||||
|       this.search() |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     const { query, page, adult, media_type } = this.$route.query |  | ||||||
|  |  | ||||||
|     if (!query) { |  | ||||||
|       // abort |  | ||||||
|       console.error('abort, no query') |  | ||||||
|     } |  | ||||||
|     this.query = decodeURIComponent(query) |  | ||||||
|     this.page = page || 1 |  | ||||||
|     this.adult = adult || this.adult |  | ||||||
|     this.mediaType = media_type || this.mediaType |  | ||||||
|     this.title = `Search results: ${this.query}` |  | ||||||
|  |  | ||||||
|     this.search() |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .fullwidth-button { |  | ||||||
|   width: 100%; |  | ||||||
|   margin: 1rem 0; |  | ||||||
|   padding-bottom: 2rem; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,376 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <div class="search"> |  | ||||||
|       <input |  | ||||||
|         ref="input" |  | ||||||
|         type="text" |  | ||||||
|         placeholder="Search for movie or show" |  | ||||||
|         aria-label="Search input for finding a movie or show" |  | ||||||
|         autocorrect="off" |  | ||||||
|         autocapitalize="off" |  | ||||||
|         tabindex="1" |  | ||||||
|         v-model="query"  |  | ||||||
|         @input="handleInput"  |  | ||||||
|         @click="focus = true" |  | ||||||
|         @keydown.escape="handleEscape" |  | ||||||
|         @keyup.enter="handleSubmit" |  | ||||||
|         @keydown.up="navigateUp" |  | ||||||
|         @keydown.down="navigateDown" /> |  | ||||||
|  |  | ||||||
|       <svg class="search-icon" fill="currentColor" @click="handleSubmit"><use xlink:href="#iconSearch"></use></svg> |  | ||||||
|     </div>  |  | ||||||
|  |  | ||||||
|     <transition name="fade"> |  | ||||||
|       <div class="dropdown" v-if="!disabled && focus && query.length > 0"> |  | ||||||
|         <div class="filter"> |  | ||||||
|           <h2>Filter your search:</h2> |  | ||||||
|  |  | ||||||
|           <div class="filter-items"> |  | ||||||
|             <toggle-button :options="searchTypes" :selected.sync="selectedSearchType" /> |  | ||||||
|  |  | ||||||
|             <label>Adult |  | ||||||
|               <input type="checkbox" value="adult" v-model="adult"> |  | ||||||
|             </label> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <hr /> |  | ||||||
|  |  | ||||||
|         <div class="dropdown-results" v-if="elasticSearchResults.length"> |  | ||||||
|           <ul v-for="(item, index) in elasticSearchResults" |  | ||||||
|               @click="openResult(item, index + 1)" |  | ||||||
|               :class="{ active: index + 1 === selectedResult}"> |  | ||||||
|                  |  | ||||||
|               {{ item.name }} |  | ||||||
|           </ul> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <div v-else class="dropdown"> |  | ||||||
|           <div class="dropdown-results"> |  | ||||||
|             <h2 class="not-found">No results for query: <b>{{ query }}</b></h2> |  | ||||||
|           </div> |  | ||||||
|         </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' |  | ||||||
| import ToggleButton from '@/components/ui/ToggleButton'; |  | ||||||
|  |  | ||||||
| import { elasticSearchMoviesAndShows } from '@/api' |  | ||||||
| import config from '@/config.json' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   name: 'SearchInput', |  | ||||||
|   components: { |  | ||||||
|     SeasonedButton, |  | ||||||
|     ToggleButton |  | ||||||
|   }, |  | ||||||
|   props: ['value'], |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       adult: true, |  | ||||||
|       searchTypes: ['all', 'movie', 'show', 'person'], |  | ||||||
|       selectedSearchType: 'all', |  | ||||||
|  |  | ||||||
|       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 |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     adult: function(value) { |  | ||||||
|       this.handleInput() |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   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-- |  | ||||||
|       const input = this.$refs.input; |  | ||||||
|       const textLength = input.value.length |  | ||||||
|  |  | ||||||
|       setTimeout(() => { |  | ||||||
|         input.focus() |  | ||||||
|         input.setSelectionRange(textLength, textLength + 1) |  | ||||||
|       }, 1) |  | ||||||
|     }, |  | ||||||
|     openResult(item, index) { |  | ||||||
|       this.selectedResult = index; |  | ||||||
|       this.$popup.open(item.id, item.type) |  | ||||||
|     }, |  | ||||||
|     handleInput(e){ |  | ||||||
|       this.selectedResult = 0 |  | ||||||
|       this.$emit('input', this.query); |  | ||||||
|  |  | ||||||
|       if (! this.focus) { |  | ||||||
|         this.focus = true; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       elasticSearchMoviesAndShows(this.query) |  | ||||||
|       .then(resp => { |  | ||||||
|         const data = resp.hits.hits |  | ||||||
|  |  | ||||||
|         let results = data.map(item => { |  | ||||||
|           const index = item._index.slice(0, -1) |  | ||||||
|           if (index === 'movie' || item._source.original_title) { |  | ||||||
|             return { |  | ||||||
|               name: item._source.original_title, |  | ||||||
|               id: item._source.id, |  | ||||||
|               adult: item._source.adult, |  | ||||||
|               type: 'movie' |  | ||||||
|             } |  | ||||||
|           } else if (index === 'show' || item._source.original_name) { |  | ||||||
|             return { |  | ||||||
|               name: item._source.original_name, |  | ||||||
|               id: item._source.id, |  | ||||||
|               adult: item._source.adult, |  | ||||||
|               type: 'show' |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         results = this.removeDuplicates(results) |  | ||||||
|         this.elasticSearchResults = results |  | ||||||
|       }) |  | ||||||
|     }, |  | ||||||
|     removeDuplicates(searchResults) { |  | ||||||
|       let filteredResults = [] |  | ||||||
|       searchResults.map(result => { |  | ||||||
|         const numberOfDuplicates = filteredResults.filter(filterItem => filterItem.id == result.id) |  | ||||||
|         if (numberOfDuplicates.length >= 1) { |  | ||||||
|           return null |  | ||||||
|         } |  | ||||||
|         filteredResults.push(result) |  | ||||||
|       }) |  | ||||||
|  |  | ||||||
|       if (this.adult == false) { |  | ||||||
|         filteredResults = filteredResults.filter(result => result.adult == false) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return filteredResults |  | ||||||
|     }, |  | ||||||
|     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, "+"')) |  | ||||||
|         const media_type = this.selectedSearchType !== 'all' ? this.selectedSearchType : null |  | ||||||
|         this.$router.push({ name: 'search', query: { query: encodedQuery, adult: this.adult, media_type }}); |  | ||||||
|         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; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .filter { |  | ||||||
|   // background-color: rgba(004, 122, 125, 0.2); |  | ||||||
|   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%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dropdown { |  | ||||||
|   width: 100%; |  | ||||||
|   position: relative; |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   z-index: 5; |  | ||||||
|   min-height: $header-size; |  | ||||||
|   right: 0px; |  | ||||||
|   background-color: $background-color-secondary; |  | ||||||
|  |  | ||||||
|   @include mobile-only { |  | ||||||
|     position: fixed; |  | ||||||
|     top: 50px; |  | ||||||
|     padding-top: 20px; |  | ||||||
|     width: calc(100%); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .not-found { |  | ||||||
|     font-weight: 400; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &-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; |  | ||||||
|       color: $text-color-50; |  | ||||||
|  |  | ||||||
|       &.active, &:hover, &:active { |  | ||||||
|         color: $text-color; |  | ||||||
|         border-bottom: 2px solid $text-color; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .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 0 13px 45px; |  | ||||||
|     outline: none; |  | ||||||
|     margin: 0; |  | ||||||
|     border: 0; |  | ||||||
|     background-color: $background-color-secondary; |  | ||||||
|     font-weight: 300; |  | ||||||
|     font-size: 19px; |  | ||||||
|     color: $text-color; |  | ||||||
|     transition: background-color .5s ease, color .5s ease; |  | ||||||
|  |  | ||||||
|     @include tablet-min { |  | ||||||
|       padding: 13px 30px 13px 60px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &-icon{ |  | ||||||
|     width: 20px; |  | ||||||
|     height: 20px; |  | ||||||
|     fill: $text-color-50; |  | ||||||
|     transition: fill 0.5s ease; |  | ||||||
|     pointer-events: none; |  | ||||||
|     position: absolute; |  | ||||||
|     left: 15px; |  | ||||||
|     top: 15px; |  | ||||||
|  |  | ||||||
|     @include tablet-min{ |  | ||||||
|       top: 27px; |  | ||||||
|       left: 25px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,198 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section class="profile"> |  | ||||||
|     <div class="profile__content" v-if="userLoggedIn"> |  | ||||||
|       <section class='settings'> |  | ||||||
|         <h3 class='settings__header'>Plex account</h3> |  | ||||||
|  |  | ||||||
|         <div v-if="!hasPlexUser"> |  | ||||||
|           <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 placeholder="plex username" icon="Email" :value.sync="plexUsername"/> |  | ||||||
|             <seasoned-input placeholder="plex password" icon="Keyhole" type="password" |  | ||||||
|               :value.sync="plexPassword" @submit="authenticatePlex" /> |  | ||||||
|  |  | ||||||
|             <seasoned-button @click="authenticatePlex">link plex account</seasoned-button> |  | ||||||
|           </form> |  | ||||||
|         </div> |  | ||||||
|         <div v-else> |  | ||||||
|           <span class="settings__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 :messages.sync="messages" /> |  | ||||||
|  |  | ||||||
|         <hr class='setting__divider'> |  | ||||||
|  |  | ||||||
|         <h3 class='settings__header'>Change password</h3> |  | ||||||
|         <form class="form"> |  | ||||||
|           <seasoned-input placeholder="new password" icon="Keyhole" type="password" |  | ||||||
|             :value.sync="newPassword" /> |  | ||||||
|  |  | ||||||
|           <seasoned-input placeholder="repeat new password" icon="Keyhole" type="password" |  | ||||||
|             :value.sync="newPasswordRepeat" /> |  | ||||||
|  |  | ||||||
|           <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 store from '@/store' |  | ||||||
| import storage from '@/storage' |  | ||||||
| import SeasonedInput from '@/components/ui/SeasonedInput' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' |  | ||||||
| import SeasonedMessages from '@/components/ui/SeasonedMessages' |  | ||||||
|  |  | ||||||
| import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { SeasonedInput, SeasonedButton, SeasonedMessages }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       userLoggedIn: '', |  | ||||||
|       messages: [], |  | ||||||
|       plexUsername: null, |  | ||||||
|       plexPassword: null, |  | ||||||
|       newPassword: null, |  | ||||||
|       newPasswordRepeat: null, |  | ||||||
|       emoji: null |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     hasPlexUser: function() { |  | ||||||
|       return this.settings && this.settings['plex_userid'] |  | ||||||
|     }, |  | ||||||
|     settings: { |  | ||||||
|       get: () => { |  | ||||||
|         return store.getters['userModule/settings'] |  | ||||||
|       }, |  | ||||||
|       set: function(newSettings) { |  | ||||||
|         store.dispatch('userModule/setSettings', newSettings) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setValue(l, t) { |  | ||||||
|       this[l] = t |  | ||||||
|     }, |  | ||||||
|     changePassword() { |  | ||||||
|       return |  | ||||||
|     }, |  | ||||||
|     async authenticatePlex() { |  | ||||||
|       let username = this.plexUsername |  | ||||||
|       let password = this.plexPassword |  | ||||||
|  |  | ||||||
|       const response = await linkPlexAccount(username, password) |  | ||||||
|  |  | ||||||
|       this.messages.push({ |  | ||||||
|         type: response.success ? 'success' : 'error', |  | ||||||
|         title: response.success ? 'Authenticated with plex' : 'Something went wrong', |  | ||||||
|         message: response.message |  | ||||||
|       }) |  | ||||||
|  |  | ||||||
|       if (response.success) |  | ||||||
|         getSettings().then(settings => this.settings = settings) |  | ||||||
|     }, |  | ||||||
|     async unauthenticatePlex() { |  | ||||||
|       const response = await unlinkPlexAccount() |  | ||||||
|  |  | ||||||
|       this.messages.push({ |  | ||||||
|         type: response.success ? 'success' : 'error', |  | ||||||
|         title: response.success ? 'Unlinked plex account ' : 'Something went wrong', |  | ||||||
|         message: response.message |  | ||||||
|       }) |  | ||||||
|  |  | ||||||
|       if (response.success) |  | ||||||
|         getSettings().then(settings => this.settings = settings) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     const token = localStorage.getItem('token') || false; |  | ||||||
|     if (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: 3rem; |  | ||||||
|  |  | ||||||
|   @include mobile-only { |  | ||||||
|     padding: 1rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|    &__header { |  | ||||||
|       margin: 0; |  | ||||||
|       line-height: 16px; |  | ||||||
|       color: $text-color; |  | ||||||
|       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 $text-color-50; |  | ||||||
|       margin-top: 30px; |  | ||||||
|       margin-bottom: 70px; |  | ||||||
|       margin-left: 20px; |  | ||||||
|       width: 96%; |  | ||||||
|       text-align: left; |  | ||||||
|    } |  | ||||||
|    span { |  | ||||||
|       font-weight: 200; |  | ||||||
|       size: 16px; |  | ||||||
|    } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,114 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <section> |  | ||||||
|     <h1>Sign in</h1> |  | ||||||
|  |  | ||||||
|     <seasoned-input placeholder="username" |  | ||||||
|                     icon="Email" |  | ||||||
|                     type="email" |  | ||||||
|                     @enter="submit" |  | ||||||
|                     :value.sync="username" /> |  | ||||||
|     <seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/> |  | ||||||
|  |  | ||||||
|     <seasoned-button @click="submit">sign in</seasoned-button> |  | ||||||
|     <router-link class="link" to="/register">Don't have a user? Register here</router-link> |  | ||||||
|  |  | ||||||
|     <seasoned-messages :messages.sync="messages"></seasoned-messages> |  | ||||||
|   </section> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import { login } from '@/api' |  | ||||||
| import storage from '../storage' |  | ||||||
| import SeasonedInput from '@/components/ui/SeasonedInput' |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' |  | ||||||
| import SeasonedMessages from '@/components/ui/SeasonedMessages' |  | ||||||
| import { parseJwt } from '@/utils' |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { SeasonedInput, SeasonedButton, SeasonedMessages }, |  | ||||||
|   data(){ |  | ||||||
|     return{ |  | ||||||
|       messages: [], |  | ||||||
|       username: null, |  | ||||||
|       password: null |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setValue(l, t) { |  | ||||||
|       this[l] = t |  | ||||||
|     }, |  | ||||||
|     submit() { |  | ||||||
|       this.messages = []; |  | ||||||
|       let username = this.username; |  | ||||||
|       let password = this.password; |  | ||||||
|  |  | ||||||
|       if (username == null || username.length == 0) { |  | ||||||
|         this.messages.push({ type: 'error', title: 'Missing username' }) |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (password == null || password.length == 0) { |  | ||||||
|         this.messages.push({ type: 'error', title: 'Missing password' }) |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.signin(username, password) |  | ||||||
|     }, |  | ||||||
|     signin(username, password) { |  | ||||||
|       login(username, password, true) |  | ||||||
|         .then(data => { |  | ||||||
|           if (data.success){ |  | ||||||
|             const jwtData = parseJwt(data.token) |  | ||||||
|             localStorage.setItem('token', data.token); |  | ||||||
|             localStorage.setItem('username', jwtData['username']); |  | ||||||
|             localStorage.setItem('admin', jwtData['admin'] || false); |  | ||||||
|  |  | ||||||
|             eventHub.$emit('setUserStatus'); |  | ||||||
|             this.$router.push({ name: 'profile' }) |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         .catch(error => { |  | ||||||
|           if (error.status === 401) { |  | ||||||
|             this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' }) |  | ||||||
|           } |  | ||||||
|           else { |  | ||||||
|             this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message }) |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created(){ |  | ||||||
|     document.title = 'Sign in' + storage.pageTitlePostfix; |  | ||||||
|     storage.backTitle = document.title; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
|  |  | ||||||
| section { |  | ||||||
|   padding: 1.3rem; |  | ||||||
|  |  | ||||||
|   @include tablet-min { |  | ||||||
|     padding: 4rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   h1 { |  | ||||||
|     margin: 0; |  | ||||||
|     line-height: 16px; |  | ||||||
|     color: $text-color; |  | ||||||
|     font-weight: 300; |  | ||||||
|     margin-bottom: 20px; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .link { |  | ||||||
|     display: block; |  | ||||||
|     width: max-content; |  | ||||||
|     margin-top: 1rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,108 +1,151 @@ | |||||||
| <template> | <template> | ||||||
|   <div v-if="show" class="container"> |   <div v-if="show" class="container"> | ||||||
|     <h2 class="torrentHeader-text">Searching for: {{ editedSearchQuery || query }}</h2> |     <h2 class="torrentHeader-text editable"> | ||||||
| <!--     <div class="torrentHeader"> |       Searching for: | ||||||
|       <span class="torrentHeader-text">Searching for: </span> |       <span :contenteditable="!edit" @input="this.handleInput">{{ | ||||||
|  |         query | ||||||
|  |       }}</span> | ||||||
|  |  | ||||||
|  |       <IconSearch | ||||||
|  |         class="icon" | ||||||
|  |         v-if="editedSearchQuery && editedSearchQuery.length" | ||||||
|  |       /> | ||||||
|  |       <IconEdit v-else class="icon" @click="() => (this.edit = !this.edit)" /> | ||||||
|  |     </h2> | ||||||
|  |  | ||||||
|       <span id="search" :contenteditable="editSearchQuery ? true : false" class="torrentHeader-text editable">{{ editedSearchQuery || query }}</span> |     <div v-if="!loading"> | ||||||
|  |  | ||||||
|  |  | ||||||
|       <svg v-if="!editSearchQuery" class="torrentHeader-editIcon" @click="toggleEditSearchQuery"> |  | ||||||
|         <use xlink:href="#icon_radar"></use> |  | ||||||
|       </svg> |  | ||||||
|  |  | ||||||
|       <svg v-else class="torrentHeader-editIcon" @click="toggleEditSearchQuery"> |  | ||||||
|         <use xlink:href="#icon_check"></use> |  | ||||||
|       </svg> |  | ||||||
|  |  | ||||||
|     </div> --> |  | ||||||
|  |  | ||||||
|     <div v-if="listLoaded"> |  | ||||||
|       <div v-if="torrents.length > 0"> |       <div v-if="torrents.length > 0"> | ||||||
|         <!-- <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> --> |  | ||||||
|  |  | ||||||
|         <toggle-button :options="release_types" :selected.sync="selectedRelaseType" class="toggle"></toggle-button> |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         <table> |         <table> | ||||||
|           <tr class="table__header noselect"> |           <thead class="table__header noselect"> | ||||||
|             <th @click="sortTable('name')" :class="selectedSortableClass('name')"> |             <tr> | ||||||
|  |               <th | ||||||
|  |                 v-for="column in columns" | ||||||
|  |                 :key="column" | ||||||
|  |                 @click="sortTable(column)" | ||||||
|  |                 :class="column === selectedColumn ? 'active' : null" | ||||||
|  |               > | ||||||
|  |                 {{ column }} | ||||||
|  |                 <span v-if="prevCol === column && direction">↑</span> | ||||||
|  |                 <span v-if="prevCol === column && !direction">↓</span> | ||||||
|  |               </th> | ||||||
|  |             </tr> | ||||||
|  |             <!--             <th | ||||||
|  |               @click="sortTable('name')" | ||||||
|  |               :class="selectedSortableClass('name')" | ||||||
|  |             > | ||||||
|               <span>Name</span> |               <span>Name</span> | ||||||
|               <span v-if="prevCol === 'name' && direction">↑</span> |               <span v-if="prevCol === 'name' && direction">↑</span> | ||||||
|               <span v-if="prevCol === 'name' && !direction">↓</span> |               <span v-if="prevCol === 'name' && !direction">↓</span> | ||||||
|             </th> |             </th> | ||||||
|             <th @click="sortTable('seed')" :class="selectedSortableClass('seed')"> |             <th | ||||||
|  |               @click="sortTable('seed')" | ||||||
|  |               :class="selectedSortableClass('seed')" | ||||||
|  |             > | ||||||
|               <span>Seed</span> |               <span>Seed</span> | ||||||
|               <span v-if="prevCol === 'seed' && direction">↑</span> |               <span v-if="prevCol === 'seed' && direction">↑</span> | ||||||
|               <span v-if="prevCol === 'seed' && !direction">↓</span> |               <span v-if="prevCol === 'seed' && !direction">↓</span> | ||||||
|             </th> |             </th> | ||||||
|             <th @click="sortTable('size')" :class="selectedSortableClass('size')"> |             <th | ||||||
|  |               @click="sortTable('size')" | ||||||
|  |               :class="selectedSortableClass('size')" | ||||||
|  |             > | ||||||
|               <span>Size</span> |               <span>Size</span> | ||||||
|               <span v-if="prevCol === 'size' && direction">↑</span> |               <span v-if="prevCol === 'size' && direction">↑</span> | ||||||
|               <span v-if="prevCol === 'size' && !direction">↓</span> |               <span v-if="prevCol === 'size' && !direction">↓</span> | ||||||
|  |             </th> | ||||||
|  |  | ||||||
|             <th> |             <th> | ||||||
|               <span>Magnet</span> |               <span>Magnet</span> | ||||||
|             </th> |             </th> --> | ||||||
|           </tr> |           </thead> | ||||||
|           <tr v-for="torrent in torrents" class="table__content"> |  | ||||||
|             <td @click="expand($event, torrent.name)">{{ torrent.name }}</td> |           <tbody> | ||||||
|             <td @click="expand($event, torrent.name)">{{ torrent.seed }}</td> |             <tr | ||||||
|             <td @click="expand($event, torrent.name)">{{ torrent.size }}</td> |               v-for="torrent in torrents" | ||||||
|             <td @click="sendTorrent(torrent.magnet, torrent.name, $event)" class="download"> |               class="table__content" | ||||||
|               <svg class="download__icon"><use xlink:href="#iconUnmatched"></use></svg> |               :key="torrent.magnet" | ||||||
|             </td> |             > | ||||||
|           </tr> |               <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" | ||||||
|  |               > | ||||||
|  |                 <IconMagnet /> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           </tbody> | ||||||
|         </table> |         </table> | ||||||
|  |  | ||||||
|         <div style=" |         <div style="display: flex; justify-content: center; padding: 1rem"> | ||||||
|           display: flex; |           <seasonedButton @click="resetTorrentsAndToggleEditSearchQuery" | ||||||
|           justify-content: center; |             >Edit search query</seasonedButton | ||||||
|           padding: 1rem; |           > | ||||||
|         "> |  | ||||||
|           <seasonedButton @click="resetTorrentsAndToggleEditSearchQuery">Edit search query</seasonedButton> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|     <div v-else style="display: flex; |       <div | ||||||
|                        padding-bottom: 2rem; |         v-else | ||||||
|                        justify-content: center; |         style=" | ||||||
|                        flex-direction: column; |           display: flex; | ||||||
|                        width: 100%; |           padding-bottom: 2rem; | ||||||
|                        align-items: center;"> |           justify-content: center; | ||||||
|       <h2>No results found</h2> |           flex-direction: column; | ||||||
|       <br /> |           width: 100%; | ||||||
|  |           align-items: center; | ||||||
|  |         " | ||||||
|  |       > | ||||||
|  |         <h2>No results found</h2> | ||||||
|  |         <br /> | ||||||
|  |  | ||||||
|       <div class="editQuery" v-if="editSearchQuery"> |         <div class="editQuery" v-if="editSearchQuery"> | ||||||
|  |           <seasonedInput | ||||||
|  |             placeholder="Torrent query" | ||||||
|  |             :value.sync="editedSearchQuery" | ||||||
|  |             @enter="fetchTorrents(editedSearchQuery)" | ||||||
|  |           /> | ||||||
|  |  | ||||||
|         <seasonedInput placeholder="Torrent query" icon="_torrents" :value.sync="editedSearchQuery" @enter="fetchTorrents(editedSearchQuery)" /> |           <div style="height: 45px; width: 5px"></div> | ||||||
|  |  | ||||||
|         <div style="height: 45px; width: 5px;"></div> |           <seasonedButton @click="fetchTorrents(editedSearchQuery)" | ||||||
|  |             >Search</seasonedButton | ||||||
|  |           > | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <seasonedButton @click="fetchTorrents(editedSearchQuery)">Search</seasonedButton> |         <seasonedButton | ||||||
|  |           @click="toggleEditSearchQuery" | ||||||
|  |           :active="editSearchQuery ? true : false" | ||||||
|  |           >Edit search query</seasonedButton | ||||||
|  |         > | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <seasonedButton @click="toggleEditSearchQuery" :active="editSearchQuery ? true : false">Edit search query</seasonedButton> |  | ||||||
|     </div> |  | ||||||
|     </div> |     </div> | ||||||
|     <div v-else class="torrentloader"><i></i></div> |     <div v-else class="torrentloader"><i></i></div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import storage from '@/storage' | import store from "@/store"; | ||||||
| import store from '@/store' | import { sortableSize } from "@/utils"; | ||||||
| import { sortableSize } from '@/utils' | import { searchTorrents, addMagnet } from "@/api"; | ||||||
| import { searchTorrents, addMagnet } from '@/api' |  | ||||||
|  |  | ||||||
| import SeasonedButton from '@/components/ui/SeasonedButton' | import IconMagnet from "../icons/IconMagnet"; | ||||||
| import SeasonedInput from '@/components/ui/SeasonedInput' | import IconEdit from "../icons/IconEdit"; | ||||||
| import ToggleButton from '@/components/ui/ToggleButton' | import IconSearch from "../icons/IconSearch"; | ||||||
|  |  | ||||||
|  | import SeasonedButton from "@/components/ui/SeasonedButton"; | ||||||
|  | import SeasonedInput from "@/components/ui/SeasonedInput"; | ||||||
|  | import ToggleButton from "@/components/ui/ToggleButton"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { SeasonedButton, SeasonedInput, ToggleButton }, |   components: { | ||||||
|  |     IconMagnet, | ||||||
|  |     IconEdit, | ||||||
|  |     IconSearch, | ||||||
|  |     SeasonedButton, | ||||||
|  |     SeasonedInput, | ||||||
|  |     ToggleButton | ||||||
|  |   }, | ||||||
|   props: { |   props: { | ||||||
|     query: { |     query: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -118,175 +161,208 @@ export default { | |||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       listLoaded: false, |       edit: true, | ||||||
|  |       loading: false, | ||||||
|       torrents: [], |       torrents: [], | ||||||
|       torrentResponse: undefined, |       torrentResponse: undefined, | ||||||
|       currentPage: 0, |       currentPage: 0, | ||||||
|       prevCol: '', |       prevCol: "", | ||||||
|       direction: false, |       direction: false, | ||||||
|       release_types: ['all'], |       release_types: ["all"], | ||||||
|       selectedRelaseType: 'all', |       selectedRelaseType: "all", | ||||||
|       editSearchQuery: false, |       editSearchQuery: false, | ||||||
|       editedSearchQuery: '' |       editedSearchQuery: "", | ||||||
|     } |  | ||||||
|  |       columns: ["name", "seed", "size", "magnet"], | ||||||
|  |       selectedColumn: null | ||||||
|  |     }; | ||||||
|   }, |   }, | ||||||
|   beforeMount() { |   created() { | ||||||
|     if (localStorage.getItem('admin')) { |     this.fetchTorrents().then(_ => this.sortTable("size")); | ||||||
|       this.fetchTorrents() |  | ||||||
|     } |  | ||||||
|     store.dispatch('torrentModule/reset') |  | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     selectedRelaseType: function(newValue) { |     selectedRelaseType: function (newValue) { | ||||||
|       this.applyFilter(newValue) |       this.applyFilter(newValue); | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     selectedSortableClass(headerName) { |     selectedSortableClass(headerName) { | ||||||
|       return headerName === this.prevCol ? 'active' : '' |       return headerName === this.prevCol ? "active" : ""; | ||||||
|     }, |     }, | ||||||
|     resetTorrentsAndToggleEditSearchQuery() { |     resetTorrentsAndToggleEditSearchQuery() { | ||||||
|       this.torrents = [] |       this.torrents = []; | ||||||
|       this.toggleEditSearchQuery() |       this.toggleEditSearchQuery(); | ||||||
|     }, |     }, | ||||||
|     toggleEditSearchQuery() { |     toggleEditSearchQuery() { | ||||||
|       this.editSearchQuery = !this.editSearchQuery; |       this.editSearchQuery = !this.editSearchQuery; | ||||||
|     }, |     }, | ||||||
|     expand(event, name) { |     expand(event, name) { | ||||||
|       const existingExpandedElement = document.getElementsByClassName('expanded')[0] |       const existingExpandedElement = | ||||||
|  |         document.getElementsByClassName("expanded")[0]; | ||||||
|  |  | ||||||
|       const clickedElement = event.target.parentNode; |       const clickedElement = event.target.parentNode; | ||||||
|       const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0] |       const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0]; | ||||||
|  |  | ||||||
|       if (existingExpandedElement) { |       if (existingExpandedElement) { | ||||||
|         const expandedSibling = event.target.parentNode.nextSibling.className === 'expanded' |         const expandedSibling = | ||||||
|  |           event.target.parentNode.nextSibling.className === "expanded"; | ||||||
|  |  | ||||||
|         existingExpandedElement.remove() |         existingExpandedElement.remove(); | ||||||
|         const table = document.getElementsByTagName('table')[0] |         const table = document.getElementsByTagName("table")[0]; | ||||||
|         table.style.display = 'block' |         table.style.display = "block"; | ||||||
|  |  | ||||||
|         if (expandedSibling) { |         if (expandedSibling) { | ||||||
|           return |           return; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const nameRow = document.createElement('tr') |       const nameRow = document.createElement("tr"); | ||||||
|       const nameCol = document.createElement('td') |       const nameCol = document.createElement("td"); | ||||||
|       nameRow.className = 'expanded' |       nameRow.className = "expanded"; | ||||||
|       nameRow.dataset[scopedStyleDataVariable] = ""; |       nameRow.dataset[scopedStyleDataVariable] = ""; | ||||||
|       nameCol.innerText = name |       nameCol.innerText = name; | ||||||
|       nameCol.dataset[scopedStyleDataVariable] = ""; |       nameCol.dataset[scopedStyleDataVariable] = ""; | ||||||
|  |  | ||||||
|       nameRow.appendChild(nameCol) |       nameRow.appendChild(nameCol); | ||||||
|  |  | ||||||
|       clickedElement.insertAdjacentElement('afterend', nameRow) |       clickedElement.insertAdjacentElement("afterend", nameRow); | ||||||
|     }, |     }, | ||||||
|     sendTorrent(magnet, name, event){ |     sendTorrent(magnet, name, event) { | ||||||
|       this.$notifications.info({ |       this.$notifications.info({ | ||||||
|         title: 'Adding torrent 🦜', |         title: "Adding torrent 🦜", | ||||||
|         description: this.query, |         description: this.query, | ||||||
|         timeout: 3000 |         timeout: 3000 | ||||||
|       }) |       }); | ||||||
|  |  | ||||||
|       event.target.parentNode.classList.add('active') |       event.target.parentNode.classList.add("active"); | ||||||
|       addMagnet(magnet, name, this.tmdb_id) |       addMagnet(magnet, name, this.tmdb_id) | ||||||
|       .catch((resp) => { console.log('error:', resp.data) }) |         .catch(resp => { | ||||||
|       .then((resp) => { |           console.log("error:", resp.data); | ||||||
|         console.log('addTorrent resp: ', resp) |  | ||||||
|         this.$notifications.success({ |  | ||||||
|           title: 'Torrent added 🎉', |  | ||||||
|           description: this.query, |  | ||||||
|           timeout: 3000 |  | ||||||
|         }) |         }) | ||||||
|       }) |         .then(resp => { | ||||||
|  |           console.log("addTorrent resp: ", resp); | ||||||
|  |           this.$notifications.success({ | ||||||
|  |             title: "Torrent added 🎉", | ||||||
|  |             description: this.query, | ||||||
|  |             timeout: 3000 | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|     }, |     }, | ||||||
|     sortTable(col, sameDirection=false) { |     sortTable(col, sameDirection = false) { | ||||||
|       if (this.prevCol === col && sameDirection === false) { |       if (this.prevCol === col && sameDirection === false) { | ||||||
|         this.direction = !this.direction |         this.direction = !this.direction; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       switch (col) { |       if (col === "name") this.sortName(); | ||||||
|         case 'name': |       else if (col === "seed") this.sortSeed(); | ||||||
|           this.sortName() |       else if (col === "size") this.sortSize(); | ||||||
|           break |  | ||||||
|         case 'seed': |       this.prevCol = col; | ||||||
|           this.sortSeed() |  | ||||||
|           break |  | ||||||
|         case 'size': |  | ||||||
|           this.sortSize() |  | ||||||
|           break |  | ||||||
|       } |  | ||||||
|       this.prevCol = col |  | ||||||
|     }, |     }, | ||||||
|     sortName() { |     sortName() { | ||||||
|       const torrentsCopy = [...this.torrents] |       const torrentsCopy = [...this.torrents]; | ||||||
|       if (this.direction) { |       if (this.direction) { | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => (a.name < b.name) ? 1 : -1) |         this.torrents = torrentsCopy.sort((a, b) => (a.name < b.name ? 1 : -1)); | ||||||
|       } else { |       } else { | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => (a.name > b.name) ? 1 : -1) |         this.torrents = torrentsCopy.sort((a, b) => (a.name > b.name ? 1 : -1)); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     sortSeed() { |     sortSeed() { | ||||||
|       const torrentsCopy = [...this.torrents] |       const torrentsCopy = [...this.torrents]; | ||||||
|       if (this.direction) { |       if (this.direction) { | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(a.seed) - parseInt(b.seed)); |         this.torrents = torrentsCopy.sort( | ||||||
|  |           (a, b) => parseInt(a.seed) - parseInt(b.seed) | ||||||
|  |         ); | ||||||
|       } else { |       } else { | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(b.seed) - parseInt(a.seed)); |         this.torrents = torrentsCopy.sort( | ||||||
|  |           (a, b) => parseInt(b.seed) - parseInt(a.seed) | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     sortSize() { |     sortSize() { | ||||||
|       const torrentsCopy = [...this.torrents] |       const torrentsCopy = [...this.torrents]; | ||||||
|       if (this.direction) { |       if (this.direction) { | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(sortableSize(a.size)) - parseInt(sortableSize(b.size))); |         this.torrents = torrentsCopy.sort( | ||||||
|  |           (a, b) => | ||||||
|  |             parseInt(sortableSize(a.size)) - parseInt(sortableSize(b.size)) | ||||||
|  |         ); | ||||||
|       } else { |       } else { | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => parseInt(sortableSize(b.size)) - parseInt(sortableSize(a.size))); |         this.torrents = torrentsCopy.sort( | ||||||
|  |           (a, b) => | ||||||
|  |             parseInt(sortableSize(b.size)) - parseInt(sortableSize(a.size)) | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     findRelaseTypes() { |     findRelaseTypes() { | ||||||
|       this.torrents.forEach(item => this.release_types.push(...item.release_type)) |       this.torrents.forEach(item => | ||||||
|       this.release_types = [...new Set(this.release_types)] |         this.release_types.push(...item.release_type) | ||||||
|  |       ); | ||||||
|  |       this.release_types = [...new Set(this.release_types)]; | ||||||
|     }, |     }, | ||||||
|     applyFilter(item, index) { |     applyFilter(item, index) { | ||||||
|       this.selectedRelaseType = item; |       this.selectedRelaseType = item; | ||||||
|       const torrents = [...this.torrentResponse] |       const torrents = [...this.torrentResponse]; | ||||||
|  |  | ||||||
|       if (item === 'all') { |       if (item === "all") { | ||||||
|         this.torrents = torrents |         this.torrents = torrents; | ||||||
|         this.sortTable(this.prevCol, true) |         this.sortTable(this.prevCol, true); | ||||||
|         return |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.torrents = torrents.filter(torrent => torrent.release_type.includes(item)) |       this.torrents = torrents.filter(torrent => | ||||||
|       this.sortTable(this.prevCol, true) |         torrent.release_type.includes(item) | ||||||
|  |       ); | ||||||
|  |       this.sortTable(this.prevCol, true); | ||||||
|     }, |     }, | ||||||
|     updateResultCountInStore() { |     updateResultCountInStore() { | ||||||
|       store.dispatch('torrentModule/setResults', this.torrents) |       store.dispatch("torrentModule/setResults", this.torrents); | ||||||
|       store.dispatch('torrentModule/setResultCount', this.torrentResponse.length) |       store.dispatch( | ||||||
|  |         "torrentModule/setResultCount", | ||||||
|  |         this.torrentResponse.length | ||||||
|  |       ); | ||||||
|     }, |     }, | ||||||
|     fetchTorrents(query=undefined){ |     filterDeadTorrents(torrents) { | ||||||
|       this.listLoaded = false; |       return torrents.filter(torrent => { | ||||||
|  |         if (isNaN(torrent.seed)) return false; | ||||||
|  |         return parseInt(torrent.seed) > 0; | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     fetchTorrents(query = undefined) { | ||||||
|  |       this.loading = true; | ||||||
|       this.editSearchQuery = false; |       this.editSearchQuery = false; | ||||||
|  |  | ||||||
|       searchTorrents(query || this.query, 'all', this.currentPage, storage.token) |       return searchTorrents(query || this.query) | ||||||
|         .then(data => { |         .then(data => { | ||||||
|             this.torrentResponse = [...data.results]; |           const { results } = data; | ||||||
|             this.torrents = data.results; |           if (results) { | ||||||
|             this.listLoaded = true; |             this.torrentResponse = results; | ||||||
|  |             this.torrents = this.filterDeadTorrents(results); | ||||||
|  |           } else { | ||||||
|  |             this.torrents = []; | ||||||
|  |           } | ||||||
|         }) |         }) | ||||||
|         .then(this.updateResultCountInStore) |         .then(this.updateResultCountInStore) | ||||||
|         .then(this.findRelaseTypes) |         .then(this.findRelaseTypes) | ||||||
|         .catch(e => { |         .catch(e => { | ||||||
|           const error = e.toString() |           console.log("e:", e); | ||||||
|           this.errorMessage = error.indexOf('401') != -1 ? 'Permission denied' : 'Nothing found'; |           const error = e.toString(); | ||||||
|           this.listLoaded = true; |           this.errorMessage = | ||||||
|  |             error.indexOf("401") != -1 ? "Permission denied" : "Nothing found"; | ||||||
|  |         }) | ||||||
|  |         .finally(() => { | ||||||
|  |           this.loading = false; | ||||||
|         }); |         }); | ||||||
|     }, |     }, | ||||||
|  |     handleInput(event) { | ||||||
|  |       this.editedSearchQuery = event.target.innerText; | ||||||
|  |       console.log("edit text:", this.editedSearchQuery); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "./src/scss/variables"; | @import "src/scss/variables"; | ||||||
| .expanded { | .expanded { | ||||||
|   display: flex; |   display: flex; | ||||||
|   padding: 0.25rem 1rem; |   padding: 0.25rem 1rem; | ||||||
| @@ -301,11 +377,153 @@ export default { | |||||||
|     width: 100%; |     width: 100%; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | $checkboxSize: 20px; | ||||||
|  | $ui-border-width: 2px; | ||||||
|  |  | ||||||
|  | .checkbox { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   margin-bottom: $checkboxSize * 0.5; | ||||||
|  |  | ||||||
|  |   input[type="checkbox"] { | ||||||
|  |     display: block; | ||||||
|  |     opacity: 0; | ||||||
|  |     position: absolute; | ||||||
|  |  | ||||||
|  |     + div { | ||||||
|  |       position: relative; | ||||||
|  |       display: inline-block; | ||||||
|  |       padding-left: 1.25rem; | ||||||
|  |       font-size: 20px; | ||||||
|  |       line-height: $checkboxSize + $ui-border-width * 2; | ||||||
|  |       left: $checkboxSize; | ||||||
|  |       cursor: pointer; | ||||||
|  |  | ||||||
|  |       &::before { | ||||||
|  |         content: ""; | ||||||
|  |         display: inline-block; | ||||||
|  |         position: absolute; | ||||||
|  |         left: -$checkboxSize; | ||||||
|  |         border: $ui-border-width solid var(--color-green); | ||||||
|  |         width: $checkboxSize; | ||||||
|  |         height: $checkboxSize; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &::after { | ||||||
|  |         transition: all 0.3s ease; | ||||||
|  |         content: ""; | ||||||
|  |         position: absolute; | ||||||
|  |         display: inline-block; | ||||||
|  |         left: -$checkboxSize + $ui-border-width; | ||||||
|  |         top: $ui-border-width; | ||||||
|  |         width: $checkboxSize + $ui-border-width; | ||||||
|  |         height: $checkboxSize + $ui-border-width; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:checked { | ||||||
|  |       + div::after { | ||||||
|  |         background-color: var(--color-green); | ||||||
|  |         opacity: 1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover:not(checked) { | ||||||
|  |       + div::after { | ||||||
|  |         background-color: var(--color-green); | ||||||
|  |         opacity: 0.4; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:focus { | ||||||
|  |       + div::before { | ||||||
|  |         outline: 2px solid Highlight; | ||||||
|  |         outline-style: auto; | ||||||
|  |         outline-color: -webkit-focus-ring-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
| <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"; | ||||||
| @import "./src/scss/elements"; | @import "src/scss/elements"; | ||||||
|  |  | ||||||
|  | h2 { | ||||||
|  |   font-size: 20px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | thead { | ||||||
|  |   user-select: none; | ||||||
|  |   -webkit-user-select: none; | ||||||
|  |   color: var(--background-color); | ||||||
|  |   text-transform: uppercase; | ||||||
|  |   cursor: pointer; | ||||||
|  |   background-color: var(--text-color); | ||||||
|  |   letter-spacing: 0.8px; | ||||||
|  |   font-size: 1rem; | ||||||
|  |   border: 1px solid var(--text-color-90); | ||||||
|  |  | ||||||
|  |   th:first-of-type { | ||||||
|  |     border-top-left-radius: 8px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   th:last-of-type { | ||||||
|  |     border-top-right-radius: 8px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tbody { | ||||||
|  |   tr > td:first-of-type { | ||||||
|  |     white-space: unset; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tr > td:not(td:first-of-type) { | ||||||
|  |     text-align: center; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tr > td:last-of-type { | ||||||
|  |     cursor: pointer; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tr td:first-of-type { | ||||||
|  |     border-left: 1px solid var(--text-color-90); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tr td:last-of-type { | ||||||
|  |     border-right: 1px solid var(--text-color-90); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tr:last-of-type { | ||||||
|  |     td { | ||||||
|  |       border-bottom: 1px solid var(--text-color-90); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     td:first-of-type { | ||||||
|  |       border-bottom-left-radius: 8px; | ||||||
|  |     } | ||||||
|  |     td:last-of-type { | ||||||
|  |       border-bottom-right-radius: 8px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tr:nth-child(even) { | ||||||
|  |     background-color: var(--background-70); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | th, | ||||||
|  | td { | ||||||
|  |   padding: 0.35rem 0.25rem; | ||||||
|  |   white-space: nowrap; | ||||||
|  |  | ||||||
|  |   svg { | ||||||
|  |     width: 24px; | ||||||
|  |     fill: var(--text-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .toggle { | .toggle { | ||||||
|   max-width: unset !important; |   max-width: unset !important; | ||||||
| @@ -323,17 +541,21 @@ export default { | |||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   padding-bottom: 20px; |   padding-bottom: 20px; | ||||||
|  |  | ||||||
|  |  | ||||||
|   &-text { |   &-text { | ||||||
|     font-weight: 400; |     font-weight: 400; | ||||||
|     text-transform: uppercase; |     text-transform: uppercase; | ||||||
|     font-size: 14px; |     font-size: 20px; | ||||||
|     color: $green; |     // color: $green; | ||||||
|     text-align: center; |     text-align: center; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|  |  | ||||||
|     @include tablet-min { |     .icon { | ||||||
|       font-size: 16px |       vertical-align: text-top; | ||||||
|  |       margin-left: 1rem; | ||||||
|  |       fill: var(--text-color); | ||||||
|  |       width: 22px; | ||||||
|  |       height: 22px; | ||||||
|  |       // stroke: white !important; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     &.editable { |     &.editable { | ||||||
| @@ -355,59 +577,67 @@ export default { | |||||||
| } | } | ||||||
|  |  | ||||||
| table { | table { | ||||||
|   border-collapse: collapse; |   // border-collapse: collapse; | ||||||
|  |   border-spacing: 0; | ||||||
|  |   margin-top: 1rem; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   table-layout: fixed; |   // table-layout: fixed; | ||||||
| } | } | ||||||
|  |  | ||||||
| .table__content, .table__header { | // .table__content, | ||||||
|   display: flex; | // .table__header { | ||||||
|   padding: 0; | //   display: flex; | ||||||
|   border-left: 1px solid $text-color; | //   padding: 0; | ||||||
|   border-right: 1px solid $text-color; | //   border-left: 1px solid $text-color; | ||||||
|   border-bottom: 1px solid $text-color; | //   border-right: 1px solid $text-color; | ||||||
|  | //   border-bottom: 1px solid $text-color; | ||||||
|  |  | ||||||
|   th, td { | //   th, | ||||||
|     display: flex; | //   td { | ||||||
|     flex-direction: column; | //     display: flex; | ||||||
|     flex-basis: 100%; | //     flex-direction: column; | ||||||
|  | //     flex-basis: 100%; | ||||||
|  |  | ||||||
|     padding: 0.4rem; | //     padding: 0.4rem; | ||||||
|  |  | ||||||
|     white-space: nowrap; | //     white-space: nowrap; | ||||||
|     text-overflow: ellipsis; | //     text-overflow: ellipsis; | ||||||
|     overflow: hidden; | //     overflow: hidden; | ||||||
|     min-width: 75px; | //     min-width: 75px; | ||||||
|   } | //   } | ||||||
|  |  | ||||||
|   th:first-child, td:first-child { | //   th:first-child, | ||||||
|     flex: 1; | //   td:first-child { | ||||||
|   } | //     flex: 1; | ||||||
|  | //   } | ||||||
|  |  | ||||||
|   th:not(:first-child), td:not(:first-child) { | //   th:not(:first-child), | ||||||
|     flex: 0.2; | //   td:not(:first-child) { | ||||||
|   } | //     flex: 0.2; | ||||||
|  | //   } | ||||||
|  |  | ||||||
|   th:nth-child(2), td:nth-child(2) { | //   th:nth-child(2), | ||||||
|     flex: 0.1; | //   td:nth-child(2) { | ||||||
|   } | //     flex: 0.1; | ||||||
|  | //   } | ||||||
|  |  | ||||||
|   @include mobile-only { | //   @include mobile-only { | ||||||
|     th:first-child, td:first-child { | //     th:first-child, | ||||||
|       display: none; | //     td:first-child { | ||||||
|  | //       display: none; | ||||||
|  |  | ||||||
|       &.show { | //       &.show { | ||||||
|         display: block; | //         display: block; | ||||||
|         align: flex-end; | //         align: flex-end; | ||||||
|       } | //       } | ||||||
|     } | //     } | ||||||
|  |  | ||||||
|     th:not(:first-child), td:not(:first-child) { | //     th:not(:first-child), | ||||||
|       flex: 1; | //     td:not(:first-child) { | ||||||
|     } | //       flex: 1; | ||||||
|   } | //     } | ||||||
|  | //   } | ||||||
| } | // } | ||||||
|  |  | ||||||
| .table__content { | .table__content { | ||||||
|   td:not(:last-child) { |   td:not(:last-child) { | ||||||
| @@ -422,58 +652,54 @@ table { | |||||||
|   border-bottom-right-radius: 3px; |   border-bottom-right-radius: 3px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .table__header { | // .table__header { | ||||||
|   color: $text-color; | //   color: $text-color; | ||||||
|   text-transform: uppercase; | //   text-transform: uppercase; | ||||||
|   cursor: pointer; | //   cursor: pointer; | ||||||
|   background-color: $background-color-secondary; | //   background-color: $background-color-secondary; | ||||||
|  |  | ||||||
|   border-top: 1px solid $text-color; | //   border-top: 1px solid $text-color; | ||||||
|   border-top-left-radius: 3px; | //   border-top-left-radius: 3px; | ||||||
|   border-top-right-radius: 3px; | //   border-top-right-radius: 3px; | ||||||
|  |  | ||||||
|   th { | //   th { | ||||||
|     display: flex; | //     display: flex; | ||||||
|     flex-direction: row; | //     flex-direction: row; | ||||||
|     font-weight: 400; | //     font-weight: 400; | ||||||
|     letter-spacing: 0.7px; | //     letter-spacing: 0.7px; | ||||||
|     // font-size: 1.08rem; | //     // font-size: 1.08rem; | ||||||
|     font-size: 15px; | //     font-size: 15px; | ||||||
|  |  | ||||||
|     &::before { | //     &::before { | ||||||
|       content: ''; | //       content: ""; | ||||||
|       min-width: 0.2rem; | //       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 $text-color; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | //     span:first-child { | ||||||
|  | //       margin-right: 0.6rem; | ||||||
|  | //     } | ||||||
|  | //     span:nth-child(2) { | ||||||
|  | //       margin-right: 0.1rem; | ||||||
|  | //     } | ||||||
|  | //   } | ||||||
|  |  | ||||||
|  | //   th:not(:last-child) { | ||||||
|  | //     border-right: 1px solid $text-color; | ||||||
|  | //   } | ||||||
|  | // } | ||||||
|  |  | ||||||
| .editQuery { | .editQuery { | ||||||
|   display: flex; |   display: flex; | ||||||
|   width: 70%; |   width: 70%; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|  |   margin-bottom: 1rem; | ||||||
|  |  | ||||||
|   @include mobile-only { |   @include mobile-only { | ||||||
|     width: 90%; |     width: 90%; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| .download { | .download { | ||||||
|  |  | ||||||
|   &__icon { |   &__icon { | ||||||
|     fill: $text-color-70; |     fill: $text-color-70; | ||||||
|     height: 1.2rem; |     height: 1.2rem; | ||||||
| @@ -506,7 +732,7 @@ table { | |||||||
|     &:after { |     &:after { | ||||||
|       border: 5px solid $green; |       border: 5px solid $green; | ||||||
|       border-radius: 50%; |       border-radius: 50%; | ||||||
|       content: ''; |       content: ""; | ||||||
|       left: 10px; |       left: 10px; | ||||||
|       position: absolute; |       position: absolute; | ||||||
|       top: 16px; |       top: 16px; | ||||||
| @@ -514,6 +740,8 @@ table { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| @keyframes load { | @keyframes load { | ||||||
|   100% { transform: rotate(360deg); } |   100% { | ||||||
|  |     transform: rotate(360deg); | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										262
									
								
								src/components/header/AutocompleteDropdown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,262 @@ | |||||||
|  | <template> | ||||||
|  |   <transition name="shut"> | ||||||
|  |     <ul class="dropdown"> | ||||||
|  |       <li | ||||||
|  |         v-for="result in searchResults" | ||||||
|  |         :key="`${result.index}-${result.title}-${result.type}`" | ||||||
|  |         @click="openPopup(result)" | ||||||
|  |         :class=" | ||||||
|  |           `result di-${result.index} ${result.index === index ? 'active' : ''}` | ||||||
|  |         " | ||||||
|  |       > | ||||||
|  |         <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> | ||||||
|  | import { mapActions } from "vuex"; | ||||||
|  | import IconMovie from "src/icons/IconMovie"; | ||||||
|  | import IconShow from "src/icons/IconShow"; | ||||||
|  | import IconPerson from "src/icons/IconPerson"; | ||||||
|  | import { elasticSearchMoviesAndShows } from "@/api"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { IconMovie, IconShow, IconPerson }, | ||||||
|  |   props: { | ||||||
|  |     query: { | ||||||
|  |       type: String, | ||||||
|  |       default: null, | ||||||
|  |       required: false | ||||||
|  |     }, | ||||||
|  |     index: { | ||||||
|  |       type: Number, | ||||||
|  |       default: -1, | ||||||
|  |       required: false | ||||||
|  |     }, | ||||||
|  |     results: { | ||||||
|  |       type: Array, | ||||||
|  |       default: [], | ||||||
|  |       required: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     query(newQuery) { | ||||||
|  |       if (newQuery && newQuery.length > 1) this.fetchAutocompleteResults(); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       searchResults: [], | ||||||
|  |       keyboardNavigationIndex: 0, | ||||||
|  |       numberOfResults: 10 | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapActions("popup", ["open"]), | ||||||
|  |     openPopup(result) { | ||||||
|  |       const { id, type } = result; | ||||||
|  |       this.open({ id, type }); | ||||||
|  |     }, | ||||||
|  |     fetchAutocompleteResults() { | ||||||
|  |       this.keyboardNavigationIndex = 0; | ||||||
|  |       this.searchResults = []; | ||||||
|  |  | ||||||
|  |       elasticSearchMoviesAndShows(this.query, this.numberOfResults).then( | ||||||
|  |         resp => { | ||||||
|  |           const data = resp.hits.hits; | ||||||
|  |  | ||||||
|  |           let results = data.map(item => { | ||||||
|  |             let index = null; | ||||||
|  |             if (item._source.log.file.path.includes("movie")) index = "movie"; | ||||||
|  |             if (item._source.log.file.path.includes("series")) index = "show"; | ||||||
|  |  | ||||||
|  |             if (index === "movie" || index === "show") { | ||||||
|  |               return { | ||||||
|  |                 title: | ||||||
|  |                   item._source.original_name || item._source.original_title, | ||||||
|  |                 id: item._source.id, | ||||||
|  |                 adult: item._source.adult, | ||||||
|  |                 type: index | ||||||
|  |               }; | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           results = this.removeDuplicates(results); | ||||||
|  |           results = results.map((el, index) => { | ||||||
|  |             return { ...el, index }; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           this.$emit("update:results", results); | ||||||
|  |           this.searchResults = results; | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |     removeDuplicates(searchResults) { | ||||||
|  |       let filteredResults = []; | ||||||
|  |       searchResults.map(result => { | ||||||
|  |         if (result === undefined) return; | ||||||
|  |         const numberOfDuplicates = filteredResults.filter( | ||||||
|  |           filterItem => filterItem.id == result.id | ||||||
|  |         ); | ||||||
|  |         if (numberOfDuplicates.length >= 1) { | ||||||
|  |           return null; | ||||||
|  |         } | ||||||
|  |         filteredResults.push(result); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (this.adult == false) { | ||||||
|  |         filteredResults = filteredResults.filter( | ||||||
|  |           result => result.adult == false | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return filteredResults; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     if (this.query) this.fetchAutocompleteResults(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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> | ||||||
							
								
								
									
										126
									
								
								src/components/header/NavigationHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,126 @@ | |||||||
|  | <template> | ||||||
|  |   <nav> | ||||||
|  |     <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 }"> | ||||||
|  |       <NavigationIcons> | ||||||
|  |         <NavigationIcon :route="profileRoute" /> | ||||||
|  |       </NavigationIcons> | ||||||
|  |     </div> | ||||||
|  |   </nav> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { mapGetters, mapActions } from "vuex"; | ||||||
|  | import TmdbLogo from "@/icons/tmdb-logo"; | ||||||
|  | import IconProfile from "@/icons/IconProfile"; | ||||||
|  | import IconProfileLock from "@/icons/IconProfileLock"; | ||||||
|  | import IconSettings from "@/icons/IconSettings"; | ||||||
|  | import IconActivity from "@/icons/IconActivity"; | ||||||
|  | import SearchInput from "@/components/header/SearchInput"; | ||||||
|  | import NavigationIcons from "src/components/header/NavigationIcons"; | ||||||
|  | import NavigationIcon from "src/components/header/NavigationIcon"; | ||||||
|  | import Hamburger from "@/components/ui/Hamburger"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     NavigationIcons, | ||||||
|  |     NavigationIcon, | ||||||
|  |     SearchInput, | ||||||
|  |     TmdbLogo, | ||||||
|  |     IconProfile, | ||||||
|  |     IconProfileLock, | ||||||
|  |     IconSettings, | ||||||
|  |     IconActivity, | ||||||
|  |     Hamburger | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters("user", ["loggedIn"]), | ||||||
|  |     ...mapGetters("hamburger", ["isOpen"]), | ||||||
|  |     isHome() { | ||||||
|  |       return this.$route.path === "/"; | ||||||
|  |     }, | ||||||
|  |     profileRoute() { | ||||||
|  |       return { | ||||||
|  |         title: !this.loggedIn ? "Signin" : "Profile", | ||||||
|  |         route: !this.loggedIn ? "/signin" : "/profile", | ||||||
|  |         icon: !this.loggedIn ? IconProfileLock : IconProfile | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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; | ||||||
|  |  | ||||||
|  |   &.open { | ||||||
|  |     opacity: 1; | ||||||
|  |     visibility: visible; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										81
									
								
								src/components/header/NavigationIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,81 @@ | |||||||
|  | <template> | ||||||
|  |   <router-link | ||||||
|  |     :to="{ path: route.route }" | ||||||
|  |     :key="route.title" | ||||||
|  |     v-if="route.requiresAuth == undefined || (route.requiresAuth && loggedIn)" | ||||||
|  |   > | ||||||
|  |     <li class="navigation-link" :class="{ active: route.route == active }"> | ||||||
|  |       <component class="navigation-icon" :is="route.icon"></component> | ||||||
|  |       <span>{{ route.title }}</span> | ||||||
|  |     </li> | ||||||
|  |   </router-link> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { mapGetters, mapActions } from "vuex"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "NavigationIcon", | ||||||
|  |   props: { | ||||||
|  |     active: { | ||||||
|  |       type: String, | ||||||
|  |       required: false | ||||||
|  |     }, | ||||||
|  |     route: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters("user", ["loggedIn"]) | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | a { | ||||||
|  |   text-decoration: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .navigation-icon { | ||||||
|  |   width: 28px; | ||||||
|  |   fill: var(--text-color-70); | ||||||
|  |   transition: inherit; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										103
									
								
								src/components/header/NavigationIcons.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,103 @@ | |||||||
|  | <template> | ||||||
|  |   <ul class="navigation-icons"> | ||||||
|  |     <NavigationIcon | ||||||
|  |       v-for="route in routes" | ||||||
|  |       :key="route.route" | ||||||
|  |       :route="route" | ||||||
|  |       :active="activeRoute" | ||||||
|  |     /> | ||||||
|  |     <slot></slot> | ||||||
|  |   </ul> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import NavigationIcon from "@/components/header/NavigationIcon"; | ||||||
|  | import IconInbox from "@/icons/IconInbox"; | ||||||
|  | import IconNowPlaying from "@/icons/IconNowPlaying"; | ||||||
|  | import IconPopular from "@/icons/IconPopular"; | ||||||
|  | import IconUpcoming from "@/icons/IconUpcoming"; | ||||||
|  | import IconSettings from "@/icons/IconSettings"; | ||||||
|  | import IconActivity from "@/icons/IconActivity"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "NavigationIcons", | ||||||
|  |   components: { | ||||||
|  |     NavigationIcon, | ||||||
|  |     IconInbox, | ||||||
|  |     IconPopular, | ||||||
|  |     IconNowPlaying, | ||||||
|  |     IconUpcoming, | ||||||
|  |     IconSettings, | ||||||
|  |     IconActivity | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       routes: [ | ||||||
|  |         { | ||||||
|  |           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, | ||||||
|  |           icon: IconActivity | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           title: "Settings", | ||||||
|  |           route: "/profile?settings=true", | ||||||
|  |           requiresAuth: true, | ||||||
|  |           icon: IconSettings | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       activeRoute: null | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     $route() { | ||||||
|  |       this.activeRoute = window.location.pathname; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.activeRoute = 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> | ||||||
							
								
								
									
										281
									
								
								src/components/header/SearchInput.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,281 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div class="search" :class="{ active: focusingInput }"> | ||||||
|  |       <IconSearch class="search-icon" tabindex="-1" /> | ||||||
|  |  | ||||||
|  |       <input | ||||||
|  |         ref="input" | ||||||
|  |         type="text" | ||||||
|  |         placeholder="Search for movie or show" | ||||||
|  |         aria-label="Search input for finding a movie or show" | ||||||
|  |         autocorrect="off" | ||||||
|  |         autocapitalize="off" | ||||||
|  |         tabindex="0" | ||||||
|  |         v-model="query" | ||||||
|  |         @input="handleInput" | ||||||
|  |         @click="focusingInput = true" | ||||||
|  |         @keydown.escape="handleEscape" | ||||||
|  |         @keyup.enter="handleSubmit" | ||||||
|  |         @keydown.up="navigateUp" | ||||||
|  |         @keydown.down="navigateDown" | ||||||
|  |         @focus="focusingInput = true" | ||||||
|  |         @blur="focusingInput = false" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <IconClose | ||||||
|  |         tabindex="0" | ||||||
|  |         aria-label="button" | ||||||
|  |         v-if="query && query.length" | ||||||
|  |         @click="resetQuery" | ||||||
|  |         @keydown.enter.stop="resetQuery" | ||||||
|  |         class="close-icon" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <AutocompleteDropdown | ||||||
|  |       v-if="showAutocompleteResults" | ||||||
|  |       :query="query" | ||||||
|  |       :index="dropdownIndex" | ||||||
|  |       :results.sync="dropdownResults" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { mapActions, mapGetters } from "vuex"; | ||||||
|  | import SeasonedButton from "@/components/ui/SeasonedButton"; | ||||||
|  | import AutocompleteDropdown from "@/components/header/AutocompleteDropdown"; | ||||||
|  |  | ||||||
|  | import IconSearch from "src/icons/IconSearch"; | ||||||
|  | import IconClose from "src/icons/IconClose"; | ||||||
|  | import config from "@/config"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "SearchInput", | ||||||
|  |   components: { | ||||||
|  |     SeasonedButton, | ||||||
|  |     AutocompleteDropdown, | ||||||
|  |     IconClose, | ||||||
|  |     IconSearch | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       query: null, | ||||||
|  |       disabled: false, | ||||||
|  |       dropdownIndex: -1, | ||||||
|  |       dropdownResults: [], | ||||||
|  |       focusingInput: false, | ||||||
|  |       showAutocomplete: false | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters("popup", ["isOpen"]), | ||||||
|  |     showAutocompleteResults() { | ||||||
|  |       return ( | ||||||
|  |         !this.disabled && | ||||||
|  |         this.focusingInput && | ||||||
|  |         this.query && | ||||||
|  |         this.query.length > 0 | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     const params = new URLSearchParams(window.location.search); | ||||||
|  |     if (params && params.has("query")) { | ||||||
|  |       this.query = decodeURIComponent(params.get("query")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const elasticUrl = config.ELASTIC_URL; | ||||||
|  |     if (elasticUrl === undefined || elasticUrl === false || elasticUrl === "") { | ||||||
|  |       this.disabled = true; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapActions("popup", ["open"]), | ||||||
|  |     navigateDown() { | ||||||
|  |       if (this.dropdownIndex < this.dropdownResults.length - 1) { | ||||||
|  |         this.dropdownIndex++; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     navigateUp() { | ||||||
|  |       if (this.dropdownIndex > -1) this.dropdownIndex--; | ||||||
|  |  | ||||||
|  |       const input = this.$refs.input; | ||||||
|  |       const textLength = input.value.length; | ||||||
|  |  | ||||||
|  |       setTimeout(() => { | ||||||
|  |         input.focus(); | ||||||
|  |         input.setSelectionRange(textLength, textLength + 1); | ||||||
|  |       }, 1); | ||||||
|  |     }, | ||||||
|  |     search() { | ||||||
|  |       const encodedQuery = encodeURI(this.query.replace('/ /g, "+"')); | ||||||
|  |  | ||||||
|  |       this.$router.push({ | ||||||
|  |         name: "search", | ||||||
|  |         query: { | ||||||
|  |           ...this.$route.query, | ||||||
|  |           query: encodedQuery | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     resetQuery(event) { | ||||||
|  |       this.query = ""; | ||||||
|  |       this.$refs.input.focus(); | ||||||
|  |     }, | ||||||
|  |     handleInput(e) { | ||||||
|  |       this.$emit("input", this.query); | ||||||
|  |       this.dropdownIndex = -1; | ||||||
|  |     }, | ||||||
|  |     handleSubmit() { | ||||||
|  |       if (!this.query || this.query.length == 0) return; | ||||||
|  |  | ||||||
|  |       if (this.dropdownIndex >= 0) { | ||||||
|  |         const resultItem = this.dropdownResults[this.dropdownIndex]; | ||||||
|  |  | ||||||
|  |         console.log("resultItem:", resultItem); | ||||||
|  |         this.open({ | ||||||
|  |           id: resultItem.id, | ||||||
|  |           type: resultItem.type | ||||||
|  |         }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.search(); | ||||||
|  |       this.$refs.input.blur(); | ||||||
|  |       this.dropdownIndex = -1; | ||||||
|  |     }, | ||||||
|  |     handleEscape() { | ||||||
|  |       if (!this.isOpen) { | ||||||
|  |         this.$refs.input.blur(); | ||||||
|  |         this.dropdownIndex = -1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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> | ||||||
							
								
								
									
										82
									
								
								src/components/popup/ActionButton.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,82 @@ | |||||||
|  | <template> | ||||||
|  |   <li | ||||||
|  |     class="sidebar-list-element" | ||||||
|  |     @click="event => $emit('click', event)" | ||||||
|  |     :class="{ active, disabled }" | ||||||
|  |   > | ||||||
|  |     <slot></slot> | ||||||
|  |   </li> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     active: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     }, | ||||||
|  |     disabled: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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; | ||||||
|  |  | ||||||
|  |   &: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> | ||||||
							
								
								
									
										124
									
								
								src/components/popup/Description.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,124 @@ | |||||||
|  | <template> | ||||||
|  |   <div | ||||||
|  |     id="description" | ||||||
|  |     class="movie-description noselect" | ||||||
|  |     @click="overflow ? (truncated = !truncated) : null" | ||||||
|  |   > | ||||||
|  |     <span ref="description" :class="{ truncated }">{{ description }}</span> | ||||||
|  |  | ||||||
|  |     <button v-if="description && overflow" class="truncate-toggle"> | ||||||
|  |       <IconArrowDown :class="{ rotate: !truncated }" /> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import IconArrowDown from "../../icons/IconArrowDown"; | ||||||
|  | export default { | ||||||
|  |   components: { IconArrowDown }, | ||||||
|  |   props: { | ||||||
|  |     description: { | ||||||
|  |       type: String, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       truncated: true, | ||||||
|  |       overflow: false | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.checkDescriptionOverflowing(); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     checkDescriptionOverflowing() { | ||||||
|  |       const descriptionEl = document.getElementById("description"); | ||||||
|  |       if (!descriptionEl) return; | ||||||
|  |  | ||||||
|  |       const { height, width } = descriptionEl.getBoundingClientRect(); | ||||||
|  |       const { fontSize, lineHeight } = getComputedStyle(descriptionEl); | ||||||
|  |  | ||||||
|  |       const elementWithoutOverflow = document.createElement("div"); | ||||||
|  |       elementWithoutOverflow.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 | ||||||
|  |  | ||||||
|  |       elementWithoutOverflow.classList.add("dummy-non-overflow"); | ||||||
|  |       elementWithoutOverflow.innerText = this.description; | ||||||
|  |  | ||||||
|  |       document.body.appendChild(elementWithoutOverflow); | ||||||
|  |       const elemWithoutOverflowHeight = | ||||||
|  |         elementWithoutOverflow.getBoundingClientRect()["height"]; | ||||||
|  |  | ||||||
|  |       this.overflow = elemWithoutOverflowHeight > height; | ||||||
|  |       this.removeElements(document.querySelectorAll(".dummy-non-overflow")); | ||||||
|  |     }, | ||||||
|  |     removeElements: elems => elems.forEach(el => el.remove()) | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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> | ||||||
							
								
								
									
										58
									
								
								src/components/popup/Detail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="movie-detail"> | ||||||
|  |     <h2 class="title">{{ title }}</h2> | ||||||
|  |     <span v-if="detail" class="info">{{ detail }}</span> | ||||||
|  |  | ||||||
|  |     <slot></slot> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     title: { | ||||||
|  |       type: String, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     detail: { | ||||||
|  |       required: false, | ||||||
|  |       default: null | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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> | ||||||
| @@ -10,12 +10,15 @@ | |||||||
|         <img |         <img | ||||||
|           class="movie-item__img is-loaded" |           class="movie-item__img is-loaded" | ||||||
|           ref="poster-image" |           ref="poster-image" | ||||||
|           src="~assets/placeholder.png" |           src="/assets/placeholder.png" | ||||||
|         /> |         /> | ||||||
|       </figure> |       </figure> | ||||||
| 
 | 
 | ||||||
|       <h1 class="movie__title" v-if="movie">{{ movie.title }}</h1> |       <div v-if="movie" class="movie__title"> | ||||||
|       <loading-placeholder v-else :count="1" /> |         <h1>{{ movie.title || movie.name }}</h1> | ||||||
|  |         <i>{{ movie.tagline }}</i> | ||||||
|  |       </div> | ||||||
|  |       <loading-placeholder v-else :count="2" /> | ||||||
|     </header> |     </header> | ||||||
| 
 | 
 | ||||||
|     <!-- Siderbar and movie info --> |     <!-- Siderbar and movie info --> | ||||||
| @@ -23,50 +26,58 @@ | |||||||
|       <div class="movie__wrap movie__wrap--main"> |       <div class="movie__wrap movie__wrap--main"> | ||||||
|         <!-- SIDEBAR ACTIONS --> |         <!-- SIDEBAR ACTIONS --> | ||||||
|         <div class="movie__actions" v-if="movie"> |         <div class="movie__actions" v-if="movie"> | ||||||
|           <sidebar-list-element |           <action-button :active="matched" :disabled="true"> | ||||||
|             :iconRef="'#iconNot_exsits'" |             <IconThumbsUp v-if="matched" /> | ||||||
|             :active="matched" |             <IconThumbsDown v-else /> | ||||||
|             :iconRefActive="'#iconExists'" |             {{ !matched ? "Not yet available" : "Already available 🎉" }} | ||||||
|             :textActive="'Already in plex 🎉'" |           </action-button> | ||||||
|           > |  | ||||||
|             Not yet in plex |  | ||||||
|           </sidebar-list-element> |  | ||||||
|           <sidebar-list-element |  | ||||||
|             @click="sendRequest" |  | ||||||
|             :iconRef="'#iconSent'" |  | ||||||
|             :active="requested" |  | ||||||
|             :textActive="'Requested to be downloaded'" |  | ||||||
|           > |  | ||||||
|             Request to be downloaded? |  | ||||||
|           </sidebar-list-element> |  | ||||||
| 
 | 
 | ||||||
|           <sidebar-list-element |           <action-button @click="sendRequest" :active="requested"> | ||||||
|             v-if="isPlexAuthenticated && matched" |             <transition name="fade" mode="out-in"> | ||||||
|             @click="openInPlex" |               <div v-if="!requested" key="request"><IconRequest /></div> | ||||||
|             :iconString="'⏯ '" |               <div v-else key="requested"><IconRequested /></div> | ||||||
|           > |             </transition> | ||||||
|             Watch in plex now! |             {{ !requested ? `Request ${this.type}?` : "Already requested" }} | ||||||
|           </sidebar-list-element> |           </action-button> | ||||||
| 
 | 
 | ||||||
|           <sidebar-list-element |           <action-button v-if="plexId && matched" @click="openInPlex"> | ||||||
|             v-if="admin" |             <IconPlay /> | ||||||
|  |             Open and watch in plex now! | ||||||
|  |           </action-button> | ||||||
|  | 
 | ||||||
|  |           <action-button | ||||||
|  |             v-if="credits && credits.cast && credits.cast.length" | ||||||
|  |             :active="showCast" | ||||||
|  |             @click="() => (showCast = !showCast)" | ||||||
|  |           > | ||||||
|  |             <IconProfile class="icon" /> | ||||||
|  |             {{ showCast ? "Hide cast" : "Show cast" }} | ||||||
|  |           </action-button> | ||||||
|  | 
 | ||||||
|  |           <action-button | ||||||
|  |             v-if="admin === true" | ||||||
|             @click="showTorrents = !showTorrents" |             @click="showTorrents = !showTorrents" | ||||||
|             :iconRef="'#icon_torrents'" |  | ||||||
|             :active="showTorrents" |             :active="showTorrents" | ||||||
|             :supplementaryText="numberOfTorrentResults" |  | ||||||
|           > |           > | ||||||
|  |             <IconBinoculars /> | ||||||
|             Search for torrents |             Search for torrents | ||||||
|           </sidebar-list-element> |             <span v-if="numberOfTorrentResults" class="meta">{{ | ||||||
|           <sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'"> |               numberOfTorrentResults | ||||||
|  |             }}</span> | ||||||
|  |           </action-button> | ||||||
|  | 
 | ||||||
|  |           <action-button @click="openTmdb"> | ||||||
|  |             <IconInfo /> | ||||||
|             See more info |             See more info | ||||||
|           </sidebar-list-element> |           </action-button> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <!-- Loading placeholder --> |         <!-- Loading placeholder --> | ||||||
|         <div class="movie__actions text-input__loading" v-else> |         <div class="movie__actions text-input__loading" v-else> | ||||||
|           <div |           <div | ||||||
|  |             v-for="index in admin ? Array(4) : Array(3)" | ||||||
|             class="movie__actions-link" |             class="movie__actions-link" | ||||||
|             v-for="_ in admin ? Array(4) : Array(3)" |             :key="index" | ||||||
|           > |           > | ||||||
|             <div |             <div | ||||||
|               class="movie__actions-text text-input__loading--line" |               class="movie__actions-text text-input__loading--line" | ||||||
| @@ -78,70 +89,63 @@ | |||||||
|         <!-- MOVIE INFO --> |         <!-- MOVIE INFO --> | ||||||
|         <div class="movie__info"> |         <div class="movie__info"> | ||||||
|           <!-- Loading placeholder --> |           <!-- Loading placeholder --> | ||||||
|           <div |           <div v-if="loading"> | ||||||
|             class="movie__description noselect" |  | ||||||
|             @click="truncatedDescription = !truncatedDescription" |  | ||||||
|             v-if="!loading" |  | ||||||
|           > |  | ||||||
|             <span :class="truncatedDescription ? 'truncated' : null">{{ |  | ||||||
|               movie.overview |  | ||||||
|             }}</span> |  | ||||||
|             <button class="truncate-toggle"><i>⬆</i></button> |  | ||||||
|           </div> |  | ||||||
|           <div v-else class="movie__description"> |  | ||||||
|             <loading-placeholder :count="5" /> |             <loading-placeholder :count="5" /> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|  |           <Description | ||||||
|  |             v-if="!loading && movie && movie.overview" | ||||||
|  |             :description="movie.overview" | ||||||
|  |           /> | ||||||
|  | 
 | ||||||
|           <div class="movie__details" v-if="movie"> |           <div class="movie__details" v-if="movie"> | ||||||
|             <div v-if="movie.year"> |             <Detail | ||||||
|               <h2 class="title">Release Date</h2> |               v-if="movie.year" | ||||||
|               <div class="text">{{ movie.year }}</div> |               title="Release date" | ||||||
|             </div> |               :detail="movie.year" | ||||||
| 
 |             /> | ||||||
|             <div v-if="movie.rating"> |             <Detail v-if="movie.rating" title="Rating" :detail="movie.rating" /> | ||||||
|               <h2 class="title">Rating</h2> |             <Detail | ||||||
|               <div class="text">{{ movie.rating }}</div> |               v-if="movie.type == 'show'" | ||||||
|             </div> |               title="Seasons" | ||||||
| 
 |               :detail="movie.seasons" | ||||||
|             <div v-if="movie.type == 'show'"> |             /> | ||||||
|               <h2 class="title">Seasons</h2> |             <Detail | ||||||
|               <div class="text">{{ movie.seasons }}</div> |               v-if="movie.genres && movie.genres.length" | ||||||
|             </div> |               title="Genres" | ||||||
| 
 |               :detail="movie.genres.join(', ')" | ||||||
|             <div v-if="movie.genres"> |             /> | ||||||
|               <h2 class="title">Genres</h2> |             <Detail | ||||||
|               <div class="text">{{ movie.genres.join(", ") }}</div> |               v-if=" | ||||||
|             </div> |                 movie.production_status && | ||||||
| 
 |                 movie.production_status !== 'Released' | ||||||
|             <div v-if="movie.type == 'show'"> |               " | ||||||
|               <h2 class="title">Production status</h2> |               title="Production status" | ||||||
|               <div class="text">{{ movie.production_status }}</div> |               :detail="movie.production_status" | ||||||
|             </div> |             /> | ||||||
| 
 |             <Detail | ||||||
|             <div v-if="movie.type == 'show'"> |               v-if="movie.runtime" | ||||||
|               <h2 class="title">Runtime</h2> |               title="Runtime" | ||||||
|               <div class="text">{{ movie.runtime[0] }} minutes</div> |               :detail="humanMinutes(movie.runtime)" | ||||||
|             </div> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <!-- TODO: change this classname, this is general  --> |         <!-- TODO: change this classname, this is general  --> | ||||||
| 
 | 
 | ||||||
|         <div class="movie__admin" v-if="movie && movie.credits"> |         <div | ||||||
|           <h2 class="movie__details-title">Cast</h2> |           class="movie__admin" | ||||||
|           <div style="display: flex; flex-wrap: wrap"> |           v-if="showCast && credits && credits.cast && credits.cast.length" | ||||||
|             <person |         > | ||||||
|               v-for="cast in movie.credits.cast" |           <Detail title="cast"> | ||||||
|               :info="cast" |             <CastList :cast="credits.cast" /> | ||||||
|               style="flex-basis: 0" |           </Detail> | ||||||
|             ></person> |  | ||||||
|           </div> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <!-- TORRENT LIST --> |       <!-- TORRENT LIST --> | ||||||
|       <TorrentList |       <TorrentList | ||||||
|         v-if="movie" |         v-if="movie && admin" | ||||||
|         :show="showTorrents" |         :show="showTorrents" | ||||||
|         :query="title" |         :query="title" | ||||||
|         :tmdb_id="id" |         :tmdb_id="id" | ||||||
| @@ -152,18 +156,29 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import storage from "@/storage"; | import { mapGetters } from "vuex"; | ||||||
| import img from "@/directives/v-image"; | import img from "@/directives/v-image"; | ||||||
| import TorrentList from "./TorrentList"; | import IconProfile from "@/icons/IconProfile"; | ||||||
| import Person from "./Person"; | import IconThumbsUp from "@/icons/IconThumbsUp"; | ||||||
| import SidebarListElement from "./ui/sidebarListElem"; | import IconThumbsDown from "@/icons/IconThumbsDown"; | ||||||
|  | import IconInfo from "@/icons/IconInfo"; | ||||||
|  | import IconRequest from "@/icons/IconRequest"; | ||||||
|  | import IconRequested from "@/icons/IconRequested"; | ||||||
|  | import IconBinoculars from "@/icons/IconBinoculars"; | ||||||
|  | import IconPlay from "@/icons/IconPlay"; | ||||||
|  | import TorrentList from "@/components/TorrentList"; | ||||||
|  | import CastList from "@/components/CastList"; | ||||||
|  | import Detail from "@/components/popup/Detail"; | ||||||
|  | import ActionButton from "@/components/popup/ActionButton"; | ||||||
|  | import Description from "@/components/popup/Description"; | ||||||
| import store from "@/store"; | import store from "@/store"; | ||||||
| import LoadingPlaceholder from "./ui/LoadingPlaceholder"; | import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder"; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   getMovie, |   getMovie, | ||||||
|   getPerson, |  | ||||||
|   getShow, |   getShow, | ||||||
|  |   getPerson, | ||||||
|  |   getCredits, | ||||||
|   request, |   request, | ||||||
|   getRequestStatus, |   getRequestStatus, | ||||||
|   watchLink |   watchLink | ||||||
| @@ -181,7 +196,22 @@ export default { | |||||||
|       type: String |       type: String | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement }, |   components: { | ||||||
|  |     Description, | ||||||
|  |     Detail, | ||||||
|  |     ActionButton, | ||||||
|  |     IconProfile, | ||||||
|  |     IconThumbsUp, | ||||||
|  |     IconThumbsDown, | ||||||
|  |     IconRequest, | ||||||
|  |     IconRequested, | ||||||
|  |     IconInfo, | ||||||
|  |     IconBinoculars, | ||||||
|  |     IconPlay, | ||||||
|  |     TorrentList, | ||||||
|  |     CastList, | ||||||
|  |     LoadingPlaceholder | ||||||
|  |   }, | ||||||
|   directives: { img: img }, // TODO decide to remove or use |   directives: { img: img }, // TODO decide to remove or use | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
| @@ -192,22 +222,17 @@ export default { | |||||||
|       poster: undefined, |       poster: undefined, | ||||||
|       backdrop: undefined, |       backdrop: undefined, | ||||||
|       matched: false, |       matched: false, | ||||||
|       userLoggedIn: storage.sessionId ? true : false, |  | ||||||
|       requested: false, |       requested: false, | ||||||
|       admin: localStorage.getItem("admin") == "true" ? true : false, |  | ||||||
|       showTorrents: false, |       showTorrents: false, | ||||||
|  |       showCast: false, | ||||||
|  |       credits: [], | ||||||
|       compact: false, |       compact: false, | ||||||
|       loading: true, |       loading: true | ||||||
|       truncatedDescription: true |  | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     id: function (val) { |     id: function (val) { | ||||||
|       if (this.type === "movie") { |       this.fetchByType(); | ||||||
|         this.fetchMovie(val); |  | ||||||
|       } else { |  | ||||||
|         this.fetchShow(val); |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     backdrop: function (backdrop) { |     backdrop: function (backdrop) { | ||||||
|       if (backdrop != null) { |       if (backdrop != null) { | ||||||
| @@ -221,15 +246,32 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  |     ...mapGetters("user", ["loggedIn", "admin", "plexId"]), | ||||||
|     numberOfTorrentResults: () => { |     numberOfTorrentResults: () => { | ||||||
|       let numTorrents = store.getters["torrentModule/resultCount"]; |       let numTorrents = store.getters["torrentModule/resultCount"]; | ||||||
|       return numTorrents !== null ? numTorrents + " results" : null; |       return numTorrents !== null ? numTorrents + " results" : null; | ||||||
|     }, |  | ||||||
|     isPlexAuthenticated: () => { |  | ||||||
|       return store.getters["userModule/isPlexAuthenticated"]; |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     async fetchByType() { | ||||||
|  |       try { | ||||||
|  |         let response; | ||||||
|  |         if (this.type === "movie") { | ||||||
|  |           response = await getMovie(this.id, true, false); | ||||||
|  |         } else if (this.type === "show") { | ||||||
|  |           response = await getShow(this.id, false, false); | ||||||
|  |         } else { | ||||||
|  |           this.$router.push({ name: "404" }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.parseResponse(response); | ||||||
|  |       } catch (error) { | ||||||
|  |         this.$router.push({ name: "404" }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // async get credits | ||||||
|  |       getCredits(this.type, this.id).then(credits => (this.credits = credits)); | ||||||
|  |     }, | ||||||
|     parseResponse(movie) { |     parseResponse(movie) { | ||||||
|       this.loading = false; |       this.loading = false; | ||||||
|       this.movie = { ...movie }; |       this.movie = { ...movie }; | ||||||
| @@ -248,21 +290,37 @@ export default { | |||||||
|     setPosterSrc() { |     setPosterSrc() { | ||||||
|       const poster = this.$refs["poster-image"]; |       const poster = this.$refs["poster-image"]; | ||||||
|       if (this.poster == null) { |       if (this.poster == null) { | ||||||
|         poster.src = "/no-image.png"; |         poster.src = "/assets/no-image.svg"; | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`; |       poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`; | ||||||
|     }, |     }, | ||||||
|  |     humanMinutes(minutes) { | ||||||
|  |       if (minutes instanceof Array) { | ||||||
|  |         minutes = minutes[0]; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const hours = Math.floor(minutes / 60); | ||||||
|  |       const minutesLeft = minutes - hours * 60; | ||||||
|  | 
 | ||||||
|  |       if (minutesLeft == 0) { | ||||||
|  |         return hours > 1 ? `${hours} hours` : `${hours} hour`; | ||||||
|  |       } else if (hours == 0) { | ||||||
|  |         return `${minutesLeft} min`; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return `${hours}h ${minutesLeft}m`; | ||||||
|  |     }, | ||||||
|     sendRequest() { |     sendRequest() { | ||||||
|       request(this.id, this.type, storage.token).then(resp => { |       request(this.id, this.type).then(resp => { | ||||||
|         if (resp.success) { |         if (resp.success) { | ||||||
|           this.requested = true; |           this.requested = true; | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     openInPlex() { |     openInPlex() { | ||||||
|       watchLink(this.title, this.movie.year, storage.token).then( |       watchLink(this.title, this.movie.year).then( | ||||||
|         watchLink => (window.location = watchLink) |         watchLink => (window.location = watchLink) | ||||||
|       ); |       ); | ||||||
|     }, |     }, | ||||||
| @@ -273,27 +331,9 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   created() { |   created() { | ||||||
|  |     store.dispatch("torrentModule/setResultCount", null); | ||||||
|     this.prevDocumentTitle = store.getters["documentTitle/title"]; |     this.prevDocumentTitle = store.getters["documentTitle/title"]; | ||||||
| 
 |     this.fetchByType(); | ||||||
|     if (this.type === "movie") { |  | ||||||
|       getMovie(this.id, true) |  | ||||||
|         .then(this.parseResponse) |  | ||||||
|         .catch(error => { |  | ||||||
|           this.$router.push({ name: "404" }); |  | ||||||
|         }); |  | ||||||
|     } else if (this.type == "person") { |  | ||||||
|       getPerson(this.id, true) |  | ||||||
|         .then(this.parseResponse) |  | ||||||
|         .catch(error => { |  | ||||||
|           this.$router.push({ name: "404" }); |  | ||||||
|         }); |  | ||||||
|     } else { |  | ||||||
|       getShow(this.id, true) |  | ||||||
|         .then(this.parseResponse) |  | ||||||
|         .catch(error => { |  | ||||||
|           this.$router.push({ name: "404" }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|   }, |   }, | ||||||
|   beforeDestroy() { |   beforeDestroy() { | ||||||
|     store.dispatch("documentTitle/updateTitle", this.prevDocumentTitle); |     store.dispatch("documentTitle/updateTitle", this.prevDocumentTitle); | ||||||
| @@ -302,14 +342,13 @@ export default { | |||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "./src/scss/loading-placeholder"; | @import "src/scss/loading-placeholder"; | ||||||
| @import "./src/scss/variables"; | @import "src/scss/variables"; | ||||||
| @import "./src/scss/media-queries"; | @import "src/scss/media-queries"; | ||||||
| @import "./src/scss/main"; | @import "src/scss/main"; | ||||||
| 
 | 
 | ||||||
| header { | header { | ||||||
|   $duration: 0.2s; |   $duration: 0.2s; | ||||||
|   height: 250px; |  | ||||||
|   transform: scaleY(1); |   transform: scaleY(1); | ||||||
|   transition: height $duration ease; |   transition: height $duration ease; | ||||||
|   transform-origin: top; |   transform-origin: top; | ||||||
| @@ -318,23 +357,32 @@ header { | |||||||
|   background-repeat: no-repeat; |   background-repeat: no-repeat; | ||||||
|   background-position: 50% 50%; |   background-position: 50% 50%; | ||||||
|   background-color: $background-color; |   background-color: $background-color; | ||||||
|   display: flex; |   display: grid; | ||||||
|   align-items: center; |   grid-template-columns: 1fr 1fr; | ||||||
|  |   height: 350px; | ||||||
| 
 | 
 | ||||||
|   @include tablet-min { |   @include mobile { | ||||||
|     height: 350px; |     grid-template-columns: 1fr; | ||||||
|  |     height: 250px; | ||||||
|  |     place-items: center; | ||||||
|   } |   } | ||||||
|   &:before { | 
 | ||||||
|  |   * { | ||||||
|  |     z-index: 2; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &::before { | ||||||
|     content: ""; |     content: ""; | ||||||
|     display: block; |     display: block; | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     top: 0; |     top: 0; | ||||||
|     left: 0; |     left: 0; | ||||||
|     z-index: 0; |     z-index: 1; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     background: $background-dark-85; |     background: $background-dark-85; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   @include mobile { |   @include mobile { | ||||||
|     &.compact { |     &.compact { | ||||||
|       height: 100px; |       height: 100px; | ||||||
| @@ -346,53 +394,21 @@ header { | |||||||
|   display: none; |   display: none; | ||||||
| 
 | 
 | ||||||
|   @include desktop { |   @include desktop { | ||||||
|     background: $background-color; |     background: var(--background-color); | ||||||
|     height: 0; |     height: auto; | ||||||
|     display: block; |     display: block; | ||||||
|     position: absolute; |     width: calc(100% - 80px); | ||||||
|     width: calc(45% - 40px); |     margin: 40px; | ||||||
|     top: 40px; |  | ||||||
|     left: 40px; |  | ||||||
| 
 | 
 | ||||||
|     > img { |     > img { | ||||||
|       width: 100%; |       width: 100%; | ||||||
|  |       border-radius: 10px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .truncate-toggle { |  | ||||||
|   border: none; |  | ||||||
|   background: none; |  | ||||||
|   width: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   text-align: center; |  | ||||||
|   color: $text-color; |  | ||||||
| 
 |  | ||||||
|   > i { |  | ||||||
|     font-style: unset; |  | ||||||
|     font-size: 0.7rem; |  | ||||||
|     transition: 0.3s ease all; |  | ||||||
|     transform: rotateY(180deg); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   &::before, |  | ||||||
|   &::after { |  | ||||||
|     content: ""; |  | ||||||
|     flex: 1; |  | ||||||
|     border-bottom: 1px solid $text-color-50; |  | ||||||
|   } |  | ||||||
|   &::before { |  | ||||||
|     margin-right: 1rem; |  | ||||||
|   } |  | ||||||
|   &::after { |  | ||||||
|     margin-left: 1rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .movie { | .movie { | ||||||
|   &__wrap { |   &__wrap { | ||||||
|     display: flex; |  | ||||||
|     &--header { |     &--header { | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       height: 100%; |       height: 100%; | ||||||
| @@ -426,32 +442,33 @@ header { | |||||||
|   &__title { |   &__title { | ||||||
|     position: relative; |     position: relative; | ||||||
|     padding: 20px; |     padding: 20px; | ||||||
|     color: $green; |  | ||||||
|     text-align: center; |     text-align: center; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|  |     height: fit-content; | ||||||
|  | 
 | ||||||
|     @include tablet-min { |     @include tablet-min { | ||||||
|       width: 55%; |  | ||||||
|       text-align: left; |       text-align: left; | ||||||
|       margin-left: 45%; |       padding: 140px 30px 0 40px; | ||||||
|       padding: 30px 30px 30px 40px; |  | ||||||
|     } |     } | ||||||
|     h1 { |     h1 { | ||||||
|  |       color: var(--color-green); | ||||||
|       font-weight: 500; |       font-weight: 500; | ||||||
|       line-height: 1.4; |       line-height: 1.4; | ||||||
|       font-size: 24px; |       font-size: 24px; | ||||||
|  |       margin-bottom: 0; | ||||||
|  | 
 | ||||||
|       @include tablet-min { |       @include tablet-min { | ||||||
|         font-size: 30px; |         font-size: 30px; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|   &__main { |  | ||||||
|     min-height: calc(100vh - 250px); |  | ||||||
|     @include tablet-min { |  | ||||||
|       min-height: 0; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     height: 100%; |     i { | ||||||
|  |       display: block; | ||||||
|  |       color: rgba(255, 255, 255, 0.8); | ||||||
|  |       margin-top: 1rem; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   &__actions { |   &__actions { | ||||||
|     text-align: center; |     text-align: center; | ||||||
|     width: 100%; |     width: 100%; | ||||||
| @@ -479,53 +496,15 @@ header { | |||||||
|   &__info { |   &__info { | ||||||
|     margin-left: 0; |     margin-left: 0; | ||||||
|   } |   } | ||||||
|   &__description { |  | ||||||
|     font-weight: 300; |  | ||||||
|     font-size: 13px; |  | ||||||
|     line-height: 1.8; |  | ||||||
|     margin-bottom: 20px; |  | ||||||
| 
 |  | ||||||
|     & .truncated { |  | ||||||
|       display: -webkit-box; |  | ||||||
|       overflow: hidden; |  | ||||||
|       -webkit-line-clamp: 4; |  | ||||||
|       -webkit-box-orient: vertical; |  | ||||||
| 
 |  | ||||||
|       & + .truncate-toggle > i { |  | ||||||
|         transform: rotateY(0deg) rotateZ(180deg); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @include tablet-min { |  | ||||||
|       margin-bottom: 30px; |  | ||||||
|       font-size: 14px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__details { |   &__details { | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-wrap: wrap; |     flex-wrap: wrap; | ||||||
| 
 | 
 | ||||||
|     > div { |     > * { | ||||||
|       margin-bottom: 20px; |       margin-right: 30px; | ||||||
|       margin-right: 20px; | 
 | ||||||
|       @include tablet-min { |       @include mobile { | ||||||
|         margin-bottom: 30px; |         margin-right: 20px; | ||||||
|         margin-right: 30px; |  | ||||||
|       } |  | ||||||
|       & .title { |  | ||||||
|         margin: 0; |  | ||||||
|         font-weight: 400; |  | ||||||
|         text-transform: uppercase; |  | ||||||
|         font-size: 14px; |  | ||||||
|         color: $green; |  | ||||||
|         @include tablet-min { |  | ||||||
|           font-size: 16px; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       & .text { |  | ||||||
|         font-weight: 300; |  | ||||||
|         font-size: 14px; |  | ||||||
|         margin-top: 5px; |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -553,4 +532,13 @@ header { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .fade-enter-active, | ||||||
|  | .fade-leave-active { | ||||||
|  |   transition: opacity 0.4s; | ||||||
|  | } | ||||||
|  | .fade-enter, | ||||||
|  | .fade-leave-to { | ||||||
|  |   opacity: 0; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
							
								
								
									
										300
									
								
								src/components/popup/Person.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,300 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="person"> | ||||||
|  |     <header ref="header"> | ||||||
|  |       <div class="info"> | ||||||
|  |         <h1 v-if="person"> | ||||||
|  |           {{ person.title || person.name }} | ||||||
|  |         </h1> | ||||||
|  |         <div v-else> | ||||||
|  |           <loading-placeholder :count="1" /> | ||||||
|  |           <loading-placeholder :count="1" lineClass="short" :top="3.5" /> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <span class="known-for" v-if="person && person['known_for_department']"> | ||||||
|  |           {{ | ||||||
|  |             person.known_for_department === "Acting" | ||||||
|  |               ? "Actor" | ||||||
|  |               : person.known_for_department | ||||||
|  |           }} | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <figure class="person__poster"> | ||||||
|  |         <img | ||||||
|  |           class="person-item__img is-loaded" | ||||||
|  |           ref="poster-image" | ||||||
|  |           src="/assets/placeholder.png" | ||||||
|  |         /> | ||||||
|  |       </figure> | ||||||
|  |     </header> | ||||||
|  |  | ||||||
|  |     <div v-if="loading"> | ||||||
|  |       <loading-placeholder :count="6" /> | ||||||
|  |       <loading-placeholder lineClass="short" :top="3" /> | ||||||
|  |       <loading-placeholder :count="6" lineClass="fullwidth" /> | ||||||
|  |       <loading-placeholder lineClass="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 | ||||||
|  |         title="movies" | ||||||
|  |         :detail="`Credited in ${movieCredits.length} movies`" | ||||||
|  |         v-if="credits" | ||||||
|  |       > | ||||||
|  |         <CastList :cast="movieCredits" /> | ||||||
|  |       </Detail> | ||||||
|  |  | ||||||
|  |       <Detail | ||||||
|  |         title="shows" | ||||||
|  |         :detail="`Credited in ${showCredits.length} shows`" | ||||||
|  |         v-if="credits" | ||||||
|  |       > | ||||||
|  |         <CastList :cast="showCredits" /> | ||||||
|  |       </Detail> | ||||||
|  |     </div> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import img from "@/directives/v-image"; | ||||||
|  | import CastList from "@/components/CastList"; | ||||||
|  | import Detail from "@/components/popup/Detail"; | ||||||
|  | import Description from "@/components/popup/Description"; | ||||||
|  | import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder"; | ||||||
|  |  | ||||||
|  | import { getPerson, getPersonCredits } from "@/api"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     id: { | ||||||
|  |       required: true, | ||||||
|  |       type: Number | ||||||
|  |     }, | ||||||
|  |     type: { | ||||||
|  |       required: false, | ||||||
|  |       type: String, | ||||||
|  |       default: "person" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   components: { | ||||||
|  |     Detail, | ||||||
|  |     Description, | ||||||
|  |     CastList, | ||||||
|  |     LoadingPlaceholder | ||||||
|  |   }, | ||||||
|  |   directives: { img: img }, // TODO decide to remove or use | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       ASSET_URL: "https://image.tmdb.org/t/p/", | ||||||
|  |       ASSET_SIZES: ["w500", "w780", "original"], | ||||||
|  |       person: undefined, | ||||||
|  |       loading: true, | ||||||
|  |       credits: undefined | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     backdrop: function (backdrop) { | ||||||
|  |       if (backdrop != null) { | ||||||
|  |         const style = { | ||||||
|  |           backgroundImage: | ||||||
|  |             "url(" + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ")" | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         Object.assign(this.$refs.header.style, style); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     age: function () { | ||||||
|  |       if (!this.person || !this.person.birthday) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const today = new Date().getFullYear(); | ||||||
|  |       const birthYear = new Date(this.person.birthday).getFullYear(); | ||||||
|  |       return `${today - birthYear} years old`; | ||||||
|  |     }, | ||||||
|  |     movieCredits: function () { | ||||||
|  |       const { cast } = this.credits; | ||||||
|  |       if (!cast) return; | ||||||
|  |  | ||||||
|  |       return cast | ||||||
|  |         .filter(l => l.type === "movie") | ||||||
|  |         .filter((item, pos, self) => self.indexOf(item) == pos) | ||||||
|  |         .sort((a, b) => a.popularity < b.popularity); | ||||||
|  |     }, | ||||||
|  |     showCredits: function () { | ||||||
|  |       const { cast } = this.credits; | ||||||
|  |       if (!cast) return; | ||||||
|  |  | ||||||
|  |       const alreadyExists = (item, pos, self) => { | ||||||
|  |         const names = self.map(item => item.title); | ||||||
|  |         return names.indexOf(item.title) == pos; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return cast | ||||||
|  |         .filter(item => item.type === "show") | ||||||
|  |         .filter(alreadyExists) | ||||||
|  |         .sort((a, b) => a.popularity < b.popularity); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     parseResponse(person) { | ||||||
|  |       this.loading = false; | ||||||
|  |       this.person = { ...person }; | ||||||
|  |       this.title = person.title; | ||||||
|  |       this.poster = person.poster; | ||||||
|  |       if (person.credits) this.credits = person.credits; | ||||||
|  |  | ||||||
|  |       this.setPosterSrc(); | ||||||
|  |     }, | ||||||
|  |     setPosterSrc() { | ||||||
|  |       const poster = this.$refs["poster-image"]; | ||||||
|  |       if (this.poster == null) { | ||||||
|  |         poster.src = "/assets/no-image.svg"; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     getPerson(this.id, false) | ||||||
|  |       .then(this.parseResponse) | ||||||
|  |       .catch(error => { | ||||||
|  |         console.error(error); | ||||||
|  |         this.$router.push({ name: "404" }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |     getPersonCredits(this.id) | ||||||
|  |       .then(credits => (this.credits = credits)) | ||||||
|  |       .catch(error => { | ||||||
|  |         console.error(error); | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </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; | ||||||
|  |   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> | ||||||
| @@ -1,49 +1,44 @@ | |||||||
| <template> | <template> | ||||||
| 
 |  | ||||||
|   <div class="darkToggle"> |   <div class="darkToggle"> | ||||||
|     <span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span> |     <span @click="toggleDarkmode">{{ darkmodeToggleIcon }}</span> | ||||||
|   </div> |   </div> | ||||||
| 
 |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| export default { | export default { | ||||||
| 
 |  | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       darkmode: this.supported |       darkmode: this.systemDarkModeEnabled() | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     toggleDarkmode() { |     toggleDarkmode() { | ||||||
|       this.darkmode = !this.darkmode; |       this.darkmode = !this.darkmode; | ||||||
|       document.body.className = this.darkmode ? 'dark' : 'light' |       document.body.className = this.darkmode ? "dark" : "light"; | ||||||
|     }, |     }, | ||||||
|     supported() { |     systemDarkModeEnabled() { | ||||||
|       const computedStyle = window.getComputedStyle(document.body) |       const computedStyle = window.getComputedStyle(document.body); | ||||||
|       if (computedStyle['colorScheme'] != null) |       if (computedStyle["colorScheme"] != null) { | ||||||
|         return computedStyle.colorScheme.includes('dark') |         return computedStyle.colorScheme.includes("dark"); | ||||||
|       return false |       } | ||||||
|  |       return false; | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     darkmodeToggleIcon() { |     darkmodeToggleIcon() { | ||||||
|       return this.darkmode ? '🌝' : '🌚' |       return this.darkmode ? "🌝" : "🌚"; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| 
 |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .darkToggle { | .darkToggle { | ||||||
|   height: 25px; |   height: 25px; | ||||||
|   width: 25px; |   width: 25px; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   // background-color: red; |  | ||||||
|   position: fixed; |   position: fixed; | ||||||
|   margin-bottom: 10px; |   margin-bottom: 1.5rem; | ||||||
|   margin-right: 2px; |   margin-right: 2px; | ||||||
|   bottom: 0; |   bottom: 0; | ||||||
|   right: 0; |   right: 0; | ||||||
| @@ -54,4 +49,4 @@ export default { | |||||||
|   -ms-user-select: none; |   -ms-user-select: none; | ||||||
|   user-select: none; |   user-select: none; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
							
								
								
									
										82
									
								
								src/components/ui/Hamburger.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,82 @@ | |||||||
|  | <template> | ||||||
|  |   <div | ||||||
|  |     class="nav__hamburger" | ||||||
|  |     :class="{ open: isOpen }" | ||||||
|  |     @click="toggle" | ||||||
|  |     @keydown.enter="toggle" | ||||||
|  |     tabindex="0" | ||||||
|  |   > | ||||||
|  |     <div v-for="(_, index) in 3" :key="index" class="bar"></div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { mapGetters, mapActions } from "vuex"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   computed: { ...mapGetters("hamburger", ["isOpen"]) }, | ||||||
|  |   methods: { ...mapActions("hamburger", ["toggle"]) } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
|  | .nav__hamburger { | ||||||
|  |   display: block; | ||||||
|  |   position: relative; | ||||||
|  |   width: var(--header-size); | ||||||
|  |   height: var(--header-size); | ||||||
|  |   cursor: pointer; | ||||||
|  |   border-left: 1px solid var(--background-color); | ||||||
|  |   background-color: var(--background-color-secondary); | ||||||
|  |  | ||||||
|  |   @include tablet-min { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .bar { | ||||||
|  |     position: absolute; | ||||||
|  |     width: 23px; | ||||||
|  |     height: 1px; | ||||||
|  |     background-color: var(--text-color-70); | ||||||
|  |     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; | ||||||
|  |         transition: all 300ms ease; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &:nth-child(3) { | ||||||
|  |       right: 15px; | ||||||
|  |       top: 33px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   &.open { | ||||||
|  |     .bar { | ||||||
|  |       &:nth-child(1), | ||||||
|  |       &:nth-child(3) { | ||||||
|  |         width: 0; | ||||||
|  |       } | ||||||
|  |       &:nth-child(2) { | ||||||
|  |         transform: rotate(-45deg); | ||||||
|  |       } | ||||||
|  |       &:nth-child(2):after { | ||||||
|  |         transform: rotate(-90deg); | ||||||
|  |         background-color: var(--text-color-70); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -7,7 +7,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "./src/scss/variables"; | @import "src/scss/variables"; | ||||||
|  |  | ||||||
| .loader { | .loader { | ||||||
|   display: flex; |   display: flex; | ||||||
| @@ -16,7 +16,7 @@ | |||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|  |  | ||||||
|   &--icon{ |   &--icon { | ||||||
|     border: 2px solid $text-color-70; |     border: 2px solid $text-color-70; | ||||||
|     border-radius: 50%; |     border-radius: 50%; | ||||||
|     display: block; |     display: block; | ||||||
| @@ -32,7 +32,7 @@ | |||||||
|       &:after { |       &:after { | ||||||
|         border: 7px solid $green-90; |         border: 7px solid $green-90; | ||||||
|         border-radius: 50%; |         border-radius: 50%; | ||||||
|         content: ''; |         content: ""; | ||||||
|         left: 8px; |         left: 8px; | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: 22px; |         top: 22px; | ||||||
| @@ -40,7 +40,9 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   @keyframes load { |   @keyframes load { | ||||||
|     100% { transform: rotate(360deg); } |     100% { | ||||||
|  |       transform: rotate(360deg); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,8 +1,11 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div class="text-input__loading" :style="`margin-top: ${top}rem`"> | ||||||
|     <div class="text-input__loading"> |     <div | ||||||
|       <div class="text-input__loading--line" :class="lineClass" v-for="_ in Array(count)"></div> |       class="text-input__loading--line" | ||||||
|     </div> |       :class="lineClass" | ||||||
|  |       v-for="l in Array(count)" | ||||||
|  |       :key="l" | ||||||
|  |     ></div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -11,17 +14,21 @@ export default { | |||||||
|   props: { |   props: { | ||||||
|     count: { |     count: { | ||||||
|       type: Number, |       type: Number, | ||||||
|       require: true |       default: 1, | ||||||
|  |       require: false | ||||||
|     }, |     }, | ||||||
|     lineClass: { |     lineClass: { | ||||||
|       type: String, |       type: String, | ||||||
|       default: '' |       default: "" | ||||||
|  |     }, | ||||||
|  |     top: { | ||||||
|  |       type: Number, | ||||||
|  |       default: 0 | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "./src/scss/loading-placeholder"; | @import "src/scss/loading-placeholder"; | ||||||
|  | </style> | ||||||
| </style> |  | ||||||
|   | |||||||
| @@ -1,31 +1,39 @@ | |||||||
| <template> | <template> | ||||||
|   <button type="button" @click="emit('click')" :class="{ active: active }"> |   <button | ||||||
|  |     type="button" | ||||||
|  |     @click="emit('click')" | ||||||
|  |     :class="{ active: active, fullwidth: fullWidth }" | ||||||
|  |   > | ||||||
|     <slot></slot> |     <slot></slot> | ||||||
|   </button> |   </button> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'seasonedButton', |   name: "seasonedButton", | ||||||
|   props: { |   props: { | ||||||
|     active: { |     active: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: false, |       default: false, | ||||||
|       required: false |       required: false | ||||||
|  |     }, | ||||||
|  |     fullWidth: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |       required: false | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     emit() { |     emit() { | ||||||
|       this.$emit('click') |       this.$emit("click"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </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"; | ||||||
|  |  | ||||||
| button { | button { | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
| @@ -43,14 +51,25 @@ button { | |||||||
|   background: $background-color-secondary; |   background: $background-color-secondary; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   outline: none; |   outline: none; | ||||||
|   transition: background 0.5s ease, color 0.5s ease, border-color .5s ease; |   transition: background 0.5s ease, color 0.5s ease, border-color 0.5s ease; | ||||||
|  |  | ||||||
|   @include desktop { |   @include desktop { | ||||||
|     font-size: 0.8rem; |     font-size: 0.8rem; | ||||||
|     padding: 6px 20px 5px 20px; |     padding: 6px 20px 5px 20px; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:focus, &:active, &.active { |   &.fullwidth { | ||||||
|  |     font-size: 14px; | ||||||
|  |     width: 40%; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       width: 60%; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &:focus, | ||||||
|  |   &:active, | ||||||
|  |   &.active { | ||||||
|     background: $text-color; |     background: $text-color; | ||||||
|     color: $background-color; |     color: $background-color; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,116 +1,134 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="group" :class="{ completed: value }"> |   <div class="group" :class="{ completed: value, focus }"> | ||||||
|     <svg class="group__input-icon"><use v-bind="{'xlink:href':'#icon' + icon}"></use></svg> |     <component :is="inputIcon" v-if="inputIcon" /> | ||||||
|     <input class="group__input" :type="tempType || type" @input="handleInput" v-model="inputValue" |  | ||||||
|           :placeholder="placeholder" @keyup.enter="submit" /> |     <input | ||||||
|      |       class="input" | ||||||
|     <i v-if="value && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i> |       :type="tempType || type" | ||||||
|  |       @input="handleInput" | ||||||
|  |       v-model="inputValue" | ||||||
|  |       :placeholder="placeholder" | ||||||
|  |       @keyup.enter="event => $emit('enter', event)" | ||||||
|  |       @focus="focus = true" | ||||||
|  |       @blur="focus = false" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <i | ||||||
|  |       v-if="value && type === 'password'" | ||||||
|  |       @click="toggleShowPassword" | ||||||
|  |       @keydown.enter="toggleShowPassword" | ||||||
|  |       class="show noselect" | ||||||
|  |       tabindex="0" | ||||||
|  |       >{{ tempType == "password" ? "show" : "hide" }}</i | ||||||
|  |     > | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import IconKey from "../../icons/IconKey"; | ||||||
|  | import IconEmail from "../../icons/IconEmail"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|  |   components: { IconKey, IconEmail }, | ||||||
|   props: { |   props: { | ||||||
|     placeholder: { type: String }, |     placeholder: { type: String }, | ||||||
|     icon: { type: String }, |     type: { type: String, default: "text" }, | ||||||
|     type: { type: String, default: 'text' }, |  | ||||||
|     value: { type: String, default: undefined } |     value: { type: String, default: undefined } | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       inputValue: this.value || undefined, |       inputValue: this.value || undefined, | ||||||
|       tempType: undefined |       tempType: this.type, | ||||||
|  |       focus: false | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     inputIcon() { | ||||||
|  |       if (this.type === "password") return IconKey; | ||||||
|  |       if (this.type === "email") return IconEmail; | ||||||
|  |       return false; | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     submit(event) { |  | ||||||
|       this.$emit('enter') |  | ||||||
|     }, |  | ||||||
|     handleInput(event) { |     handleInput(event) { | ||||||
|       if (this.value !== undefined) { |       if (this.value !== undefined) { | ||||||
|         this.$emit('update:value', this.inputValue) |         this.$emit("update:value", this.inputValue); | ||||||
|       } else { |       } else { | ||||||
|         this.$emit('change', this.inputValue, event) |         this.$emit("change", this.inputValue, event); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     toggleShowPassword() { |     toggleShowPassword() { | ||||||
|       if (this.tempType === 'text') { |       if (this.tempType === "text") { | ||||||
|         this.tempType = 'password' |         this.tempType = "password"; | ||||||
|       } else { |       } else { | ||||||
|         this.tempType = 'text' |         this.tempType = "text"; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </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"; | ||||||
|  |  | ||||||
| .group{ | .group { | ||||||
|   display: flex; |   display: flex; | ||||||
|   margin-bottom: 1rem; |  | ||||||
|   width: 100%; |   width: 100%; | ||||||
|  |   position: relative; | ||||||
|  |   max-width: 35rem; | ||||||
|  |   border: 1px solid var(--text-color-50); | ||||||
|  |   background-color: var(--background-color-secondary); | ||||||
|  |  | ||||||
|   &:hover, &:focus { |   &.completed, | ||||||
|     .group__input { |   &.focus, | ||||||
|       border-color: $text-color; |   &:hover, | ||||||
|  |   &:focus { | ||||||
|  |     border-color: var(--text-color); | ||||||
|  |  | ||||||
|       &-icon { |     svg { | ||||||
|         fill: $text-color; |       fill: var(--text-color); | ||||||
|       }       |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.completed { |   svg { | ||||||
|     .group__input { |  | ||||||
|       border-color: $text-color; |  | ||||||
|  |  | ||||||
|       &-icon { |  | ||||||
|         fill: $text-color; |  | ||||||
|       }       |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__input { |  | ||||||
|     width: 100%; |  | ||||||
|     max-width: 35rem; |  | ||||||
|     padding: 10px 10px 10px 45px; |  | ||||||
|     outline: none; |  | ||||||
|     background-color: $background-color-secondary; |  | ||||||
|     color: $text-color; |  | ||||||
|     font-weight: 100; |  | ||||||
|     font-size: 1.2rem; |  | ||||||
|     border: 1px solid $text-color-50; |  | ||||||
|     margin: 0; |  | ||||||
|     margin-left: -2.2rem !important; |  | ||||||
|     z-index: 3; |  | ||||||
|     transition: color .5s ease, background-color .5s ease, border .5s ease; |  | ||||||
|  |  | ||||||
|     border-radius: 0; |  | ||||||
|     -webkit-appearance: none; |  | ||||||
|  |  | ||||||
|     &-show { |  | ||||||
|       position: relative; |  | ||||||
|       left: -50px; |  | ||||||
|       z-index: 11; |  | ||||||
|       margin: auto 0; |  | ||||||
|       height: 100%; |  | ||||||
|       font-size: 0.9rem; |  | ||||||
|       cursor: pointer; |  | ||||||
|       color: $text-color-50; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__input-icon { |  | ||||||
|     width: 24px; |     width: 24px; | ||||||
|     height: 24px; |     height: 24px; | ||||||
|     fill: $text-color-50; |     fill: var(--text-color-50); | ||||||
|     transition: fill 0.5s ease; |  | ||||||
|     pointer-events: none; |     pointer-events: none; | ||||||
|     margin-top: 10px; |     margin-top: 10px; | ||||||
|     margin-left: 10px; |     margin-left: 10px; | ||||||
|     z-index: 8; |     z-index: 8; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   input { | ||||||
|  |     width: 100%; | ||||||
|  |     padding: 10px; | ||||||
|  |     outline: none; | ||||||
|  |     background-color: var(--background-color-secondary); | ||||||
|  |     color: var(--text-color); | ||||||
|  |     font-weight: 100; | ||||||
|  |     font-size: 1.2rem; | ||||||
|  |     margin: 0; | ||||||
|  |     z-index: 3; | ||||||
|  |     border: none; | ||||||
|  |  | ||||||
|  |     border-radius: 0; | ||||||
|  |     -webkit-appearance: none; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .show { | ||||||
|  |     position: absolute; | ||||||
|  |     display: grid; | ||||||
|  |     place-items: center; | ||||||
|  |     right: 20px; | ||||||
|  |     z-index: 11; | ||||||
|  |     margin: auto 0; | ||||||
|  |     height: 100%; | ||||||
|  |     font-size: 0.9rem; | ||||||
|  |     cursor: pointer; | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     -webkit-user-select: none; | ||||||
|  |     user-select: none; | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,10 +1,19 @@ | |||||||
| <template> | <template> | ||||||
|   <transition-group name="fade"> |   <transition-group name="fade"> | ||||||
|     <div class="message" v-for="(message, index) in reversedMessages" :class="message.type || 'warning'" :key="index"> |     <div | ||||||
|  |       class="message" | ||||||
|  |       v-for="(message, index) in reversedMessages" | ||||||
|  |       :key="`${index}-${message.title}-${message.type}}`" | ||||||
|  |       :class="message.type || 'warning'" | ||||||
|  |     > | ||||||
|       <span class="pinstripe"></span> |       <span class="pinstripe"></span> | ||||||
|       <div> |       <div> | ||||||
|         <h2 class="title">{{ message.title || defaultTitles[message.type] }}</h2> |         <h2 class="title"> | ||||||
|         <span v-if="message.message" class="message">{{ message.message }}</span> |           {{ message.title || defaultTitles[message.type] }} | ||||||
|  |         </h2> | ||||||
|  |         <span v-if="message.message" class="message">{{ | ||||||
|  |           message.message | ||||||
|  |         }}</span> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <button class="dismiss" @click="clicked(message)">X</button> |       <button class="dismiss" @click="clicked(message)">X</button> | ||||||
| @@ -13,7 +22,6 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|     messages: { |     messages: { | ||||||
| @@ -24,37 +32,36 @@ export default { | |||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       defaultTitles: { |       defaultTitles: { | ||||||
|         error: 'Unexpected error', |         error: "Unexpected error", | ||||||
|         warning: 'Something went wrong', |         warning: "Something went wrong", | ||||||
|         undefined: 'Something went wrong' |         undefined: "Something went wrong" | ||||||
|       }, |       }, | ||||||
|       localMessages: [...this.messages] |       localMessages: [...this.messages] | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     reversedMessages() { |     reversedMessages() { | ||||||
|       return [...this.messages].reverse() |       return [...this.messages].reverse(); | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     clicked(e) { |     clicked(e) { | ||||||
|       const removedMessage = [...this.messages].filter(mes => mes !== e) |       const removedMessage = [...this.messages].filter(mes => mes !== e); | ||||||
|       this.$emit('update:messages', removedMessage) |       this.$emit("update:messages", removedMessage); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
|  |  | ||||||
| </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"; | ||||||
|  |  | ||||||
| .fade-enter-active { | .fade-enter-active { | ||||||
|   transition: opacity .4s; |   transition: opacity 0.4s; | ||||||
| } | } | ||||||
| .fade-leave-active { | .fade-leave-active { | ||||||
|   transition: opacity .1s; |   transition: opacity 0.1s; | ||||||
| } | } | ||||||
| .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { | .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { | ||||||
|   opacity: 0; |   opacity: 0; | ||||||
| @@ -72,7 +79,6 @@ export default { | |||||||
|   > div { |   > div { | ||||||
|     margin: 10px 24px; |     margin: 10px 24px; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .title { |   .title { | ||||||
| @@ -81,12 +87,12 @@ export default { | |||||||
|     margin: 0; |     margin: 0; | ||||||
|     font-size: 1.3rem; |     font-size: 1.3rem; | ||||||
|     color: $text-color; |     color: $text-color; | ||||||
|     transition: color .5s ease; |     transition: color 0.5s ease; | ||||||
|   } |   } | ||||||
|   .message { |   .message { | ||||||
|     font-weight: 300; |     font-weight: 300; | ||||||
|     color: $text-color-70; |     color: $text-color-70; | ||||||
|     transition: color .5s ease; |     transition: color 0.5s ease; | ||||||
|     margin: 0.2rem 0 0.5rem; |     margin: 0.2rem 0 0.5rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -101,7 +107,6 @@ export default { | |||||||
|     span { |     span { | ||||||
|       font-size: 0.9rem; |       font-size: 0.9rem; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .pinstripe { |   .pinstripe { | ||||||
| @@ -126,7 +131,7 @@ export default { | |||||||
|     margin-top: 0.5rem; |     margin-top: 0.5rem; | ||||||
|     margin-right: 0.5rem; |     margin-right: 0.5rem; | ||||||
|     color: $text-color-70; |     color: $text-color-70; | ||||||
|     transition: color .5s ease; |     transition: color 0.5s ease; | ||||||
|  |  | ||||||
|     &:hover { |     &:hover { | ||||||
|       color: $text-color; |       color: $text-color; | ||||||
| @@ -140,7 +145,7 @@ export default { | |||||||
|       background-color: $color-success-highlight; |       background-color: $color-success-highlight; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |  | ||||||
|   &.error { |   &.error { | ||||||
|     background-color: $color-error; |     background-color: $color-error; | ||||||
|  |  | ||||||
| @@ -151,11 +156,10 @@ export default { | |||||||
|  |  | ||||||
|   &.warning { |   &.warning { | ||||||
|     background-color: $color-warning; |     background-color: $color-warning; | ||||||
|      |  | ||||||
|     .pinstripe { |     .pinstripe { | ||||||
|       background-color: $color-warning-highlight; |       background-color: $color-warning-highlight; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | </style> | ||||||
| </style> |  | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div v-html="require(`@/assets/icons/${ icon }.svg`)"></div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
|   export default { |  | ||||||
|     props: ['icon'] |  | ||||||
|   } |  | ||||||
| </script> |  | ||||||
| @@ -1,14 +1,19 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="toggle-container"> |   <div class="toggle-container"> | ||||||
|     <button v-for="option in options" class="toggle-button" @click="toggle(option)" |     <button | ||||||
|  |       v-for="option in options" | ||||||
|  |       :key="option" | ||||||
|  |       class="toggle-button" | ||||||
|  |       @click="toggle(option)" | ||||||
|       :class="toggleValue === option ? 'selected' : null" |       :class="toggleValue === option ? 'selected' : null" | ||||||
|     >{{ option }}</button> |     > | ||||||
|  |       {{ option }} | ||||||
|  |     </button> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|     options: { |     options: { | ||||||
|       Array, |       Array, | ||||||
| @@ -23,38 +28,35 @@ export default { | |||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       toggleValue: this.selected || this.options[0] |       toggleValue: this.selected || this.options[0] | ||||||
|     } |     }; | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     this.toggle(this.toggleValue) |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     toggle(toggleValue) { |     toggle(toggleValue) { | ||||||
|       this.toggleValue = toggleValue; |       this.toggleValue = toggleValue; | ||||||
|       if (this.selected !== undefined) { |       if (this.selected !== undefined) { | ||||||
|         this.$emit('update:selected', toggleValue) |         this.$emit("update:selected", toggleValue); | ||||||
|  |         this.$emit("change", toggleValue); | ||||||
|       } else { |       } else { | ||||||
|         this.$emit('change', toggleValue) |         this.$emit("change", toggleValue); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "./src/scss/variables"; | @import "src/scss/variables"; | ||||||
|  |  | ||||||
| $background: $background-ui; | $background: $background-ui; | ||||||
| $background-selected: $background-color-secondary; | $background-selected: $background-color-secondary; | ||||||
|  |  | ||||||
| .toggle-container { | .toggle-container { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   max-width: 15rem; |  | ||||||
|   display: flex; |   display: flex; | ||||||
|  |   overflow-x: scroll; | ||||||
|   flex-direction: row; |   flex-direction: row; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   // padding: 0.2rem; |  | ||||||
|   background-color: $background; |   background-color: $background; | ||||||
|   border: 2px solid $background; |   border: 2px solid $background; | ||||||
|   border-radius: 8px; |   border-radius: 8px; | ||||||
| @@ -65,36 +67,20 @@ $background-selected: $background-color-secondary; | |||||||
|     font-size: 1rem; |     font-size: 1rem; | ||||||
|     line-height: 1rem; |     line-height: 1rem; | ||||||
|     font-weight: normal; |     font-weight: normal; | ||||||
|     width: 100%; |     padding: 0.5rem; | ||||||
|     padding: 0.5rem 0; |  | ||||||
|     border: 0; |     border: 0; | ||||||
|     color: $text-color; |     color: $text-color; | ||||||
|     // background-color: $text-color-5; |  | ||||||
|     background-color: $background; |     background-color: $background; | ||||||
|     text-transform: capitalize; |     text-transform: capitalize; | ||||||
|  |     cursor: pointer; | ||||||
|  |     display: block; | ||||||
|  |     flex: 1 0 auto; | ||||||
|  |  | ||||||
|     &.selected { |     &.selected { | ||||||
|       color: $text-color; |       color: $text-color; | ||||||
|       // background-color: $background-color-secondary; |  | ||||||
|       background-color: $background-selected; |       background-color: $background-selected; | ||||||
|       border-radius: 8px; |       border-radius: 8px; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // &:first-of-type, &:last-of-type { |  | ||||||
|     //   border-left: 4px solid $background; |  | ||||||
|     //   border-right: 4px solid $background; |  | ||||||
|     // } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     // &:first-of-type { |  | ||||||
|     //   border-top-left-radius: 4px; |  | ||||||
|     //   border-bottom-left-radius: 4px; |  | ||||||
|     // } |  | ||||||
|  |  | ||||||
|     // &:last-of-type { |  | ||||||
|     //   border-top-right-radius: 4px; |  | ||||||
|     //   border-bottom-right-radius: 4px; |  | ||||||
|     // } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,138 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <a @click="$emit('click')"> |  | ||||||
|       <li> |  | ||||||
|         <figure v-if="iconRef" :class="activeClassIfActive"> |  | ||||||
|           <svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg> |  | ||||||
|         </figure> |  | ||||||
|  |  | ||||||
|         <span class="text" :class="activeClassIfActive">{{ contentTextToDisplay }}</span> |  | ||||||
|  |  | ||||||
|         <span v-if="supplementaryText" class="supplementary-text"> |  | ||||||
|           {{ supplementaryText }} |  | ||||||
|         </span> |  | ||||||
|       </li> |  | ||||||
|     </a> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
|   // TODO if a image is hovered and we can't set the hover color we want to |  | ||||||
|   // go into it and change the fill |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     iconRef: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     iconRefActive: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     active: { |  | ||||||
|       type: Boolean, |  | ||||||
|       default: false, |  | ||||||
|     }, |  | ||||||
|     textActive: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     supplementaryText: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     iconRefNameIfActive() { |  | ||||||
|       const { iconRefActive, iconRef, active } = this |  | ||||||
|  |  | ||||||
|       if ((iconRefActive && iconRef) && active) { |  | ||||||
|         return iconRefActive |  | ||||||
|       } |  | ||||||
|       return iconRef |  | ||||||
|     }, |  | ||||||
|     contentTextToDisplay() { |  | ||||||
|       const { textActive, active, $slots } = this |  | ||||||
|  |  | ||||||
|       if (textActive && active) |  | ||||||
|         return textActive |  | ||||||
|  |  | ||||||
|       if ($slots.default && $slots.default.length > 0) |  | ||||||
|         return $slots.default[0].text |  | ||||||
|  |  | ||||||
|       return '' |  | ||||||
|     }, |  | ||||||
|     activeClassIfActive() { |  | ||||||
|       return this.active ? 'active' : '' |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "./src/scss/variables"; |  | ||||||
| @import "./src/scss/media-queries"; |  | ||||||
|  |  | ||||||
| li { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   text-decoration: none; |  | ||||||
|   text-transform: uppercase; |  | ||||||
|   color: $text-color-50; |  | ||||||
|   transition: color 0.5s ease; |  | ||||||
|   font-size: 11px; |  | ||||||
|   padding: 10px 0; |  | ||||||
|   border-bottom: 1px solid $text-color-5; |  | ||||||
|  |  | ||||||
|   &:hover { |  | ||||||
|     color: $text-color; |  | ||||||
|     cursor: pointer; |  | ||||||
|  |  | ||||||
|     .icon { |  | ||||||
|       fill: $text-color; |  | ||||||
|       cursor: pointer; |  | ||||||
|         transform: scale(1.1, 1.1); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   .active { |  | ||||||
|     color: $text-color; |  | ||||||
|  |  | ||||||
|     .icon { |  | ||||||
|       fill: $green; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   .pending { |  | ||||||
|     color: #f8bd2d; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .text { |  | ||||||
|     margin-left: 26px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .supplementary-text { |  | ||||||
|     flex-grow: 1; |  | ||||||
|     text-align: right; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   figure { |  | ||||||
|     position: absolute; |  | ||||||
|  |  | ||||||
|     > svg { |  | ||||||
|       position: relative; |  | ||||||
|       top: 50%; |  | ||||||
|       width: 16px; |  | ||||||
|       height: 16px; |  | ||||||
|       margin: 0 7px 0 0; |  | ||||||
|       fill: $text-color-50; |  | ||||||
|       transition: fill 0.5s ease, transform 0.5s ease; |  | ||||||
|  |  | ||||||
|       & .waiting { |  | ||||||
|         transform: scale(0.8, 0.8); |  | ||||||
|       } |  | ||||||
|       & .pending { |  | ||||||
|         fill: #f8bd2d; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										9
									
								
								src/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | import type IConfig from "./interfaces/IConfig"; | ||||||
|  |  | ||||||
|  | const config: IConfig = { | ||||||
|  |   SEASONED_URL: "", | ||||||
|  |   ELASTIC_URL: "https://elastic.kevinmidboe.com/", | ||||||
|  |   ELASTIC_INDEX: "shows,movies" | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default config; | ||||||
							
								
								
									
										16
									
								
								src/icons/IconActivity.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="24" | ||||||
|  |     height="24" | ||||||
|  |     viewBox="0 0 24 24" | ||||||
|  |     fill="none" | ||||||
|  |     stroke="currentColor" | ||||||
|  |     stroke-width="2" | ||||||
|  |     stroke-linecap="round" | ||||||
|  |     stroke-linejoin="round" | ||||||
|  |     style="transition: stroke-width 0.5s ease" | ||||||
|  |   > | ||||||
|  |     <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										8
									
								
								src/icons/IconArrowDown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M28.725 8.058l-12.725 12.721-12.725-12.721-1.887 1.887 13.667 13.667c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l13.667-13.667-1.879-1.887z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										16
									
								
								src/icons/IconBinoculars.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M31.313 18.896v0l-5.071-10.846c-0.004-0.008-0.008-0.017-0.012-0.029-0.542-1.154-1.529-2.021-2.696-2.429l-1.129-1.921c-0.004-0.004-0.008-0.013-0.012-0.017-0.358-0.608-1.021-0.987-1.725-0.987-1.104 0-2 0.896-2 2v2.071c-0.825 0.842-1.333 1.996-1.333 3.263v1.025c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-1.025c0-1.267-0.508-2.421-1.333-3.263v-2.071c0-1.104-0.896-2-2-2-0.704 0-1.367 0.379-1.725 0.987-0.004 0.004-0.008 0.013-0.012 0.017l-1.129 1.921c-1.171 0.408-2.158 1.275-2.696 2.429-0.004 0.008-0.008 0.017-0.013 0.025l-5.071 10.85c-0.442 0.946-0.688 1.996-0.688 3.104 0 4.042 3.292 7.333 7.333 7.333 3.942 0 7.167-3.125 7.325-7.029 0.396 0.229 0.85 0.363 1.342 0.363 0.488 0 0.946-0.133 1.342-0.363 0.158 3.904 3.383 7.029 7.325 7.029 4.042 0 7.333-3.292 7.333-7.333 0-1.108-0.246-2.158-0.688-3.104zM26.5 14.9c-0.587-0.15-1.2-0.233-1.833-0.233-1.771 0-3.396 0.629-4.667 1.679v-6.346c0-1.104 0.896-2 2-2 0.767 0 1.471 0.446 1.804 1.133 0.004 0.008 0.008 0.012 0.008 0.021l2.688 5.746zM20.667 4c0.233 0 0.446 0.117 0.567 0.317 0.004 0.004 0.004 0.008 0.008 0.013l0.592 1.008c-0.654 0.025-1.275 0.183-1.833 0.446v-1.117c0-0.367 0.3-0.667 0.667-0.667zM16 12c0.733 0 1.333 0.6 1.333 1.333v4.358c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-4.358c0-0.733 0.6-1.333 1.333-1.333zM10.767 4.317c0.121-0.2 0.333-0.317 0.567-0.317 0.367 0 0.667 0.3 0.667 0.667v1.117c-0.558-0.267-1.179-0.425-1.833-0.446l0.592-1.008c0.004-0.004 0.004-0.008 0.008-0.013zM8.188 9.154c0.004-0.008 0.004-0.012 0.008-0.021 0.333-0.688 1.037-1.133 1.804-1.133 1.104 0 2 0.896 2 2v6.346c-1.271-1.050-2.896-1.679-4.667-1.679-0.633 0-1.246 0.079-1.833 0.233l2.688-5.746zM7.333 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667zM16 21.333c-0.733 0-1.333-0.6-1.333-1.333s0.6-1.333 1.333-1.333c0.733 0 1.333 0.6 1.333 1.333s-0.6 1.333-1.333 1.333zM24.667 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M5.333 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M22.667 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										15
									
								
								src/icons/IconClose.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     id="icon-cross" | ||||||
|  |     viewBox="0 0 32 32" | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     @click="$emit('click')" | ||||||
|  |     @keydown="event => $emit('keydown', event)" | ||||||
|  |     style="transition-duration: 0s;" | ||||||
|  |   > | ||||||
|  |     <path | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M27.942 5.942l-1.883-1.883-10.058 10.054-10.058-10.054-1.883 1.883 10.054 10.058-10.054 10.058 1.883 1.883 10.058-10.054 10.058 10.054 1.883-1.883-10.054-10.058z" | ||||||
|  |     ></path> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										12
									
								
								src/icons/IconEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     @click="$emit('click')" | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     viewBox="0 0 32 32" | ||||||
|  |   > | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M30.229 1.771c-1.142-1.142-2.658-1.771-4.275-1.771s-3.133 0.629-4.275 1.771l-18.621 18.621c-0.158 0.158-0.275 0.358-0.337 0.575l-2.667 9.333c-0.133 0.467-0.004 0.967 0.338 1.308 0.254 0.254 0.596 0.392 0.942 0.392 0.121 0 0.246-0.017 0.367-0.050l9.333-2.667c0.217-0.063 0.417-0.179 0.575-0.337l18.621-18.621c2.358-2.362 2.358-6.196 0-8.554zM6.079 21.137l14.392-14.392 4.779 4.779-14.387 14.396-4.783-4.783zM21.413 5.804l1.058-1.058 4.779 4.779-1.058 1.058-4.779-4.779zM5.167 22.108l4.725 4.725-6.617 1.892 1.892-6.617zM28.346 8.438l-0.15 0.15-4.783-4.783 0.15-0.15c0.642-0.637 1.488-0.988 2.392-0.988s1.75 0.35 2.392 0.992c1.317 1.317 1.317 3.458 0 4.779z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										8
									
								
								src/icons/IconEmail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258h6.667v-2.667h-6.667c-7.35 0-13.333-5.983-13.333-13.333s5.983-13.333 13.333-13.333c7.35 0 13.333 5.983 13.333 13.333v0.667c0 1.837-1.496 3.333-3.333 3.333s-3.333-1.496-3.333-3.333v-7.333h-2.667v1.338c-1.117-0.838-2.5-1.338-4-1.338-3.675 0-6.667 2.992-6.667 6.667s2.992 6.667 6.667 6.667c2.079 0 3.938-0.958 5.162-2.454 1.092 1.488 2.854 2.454 4.837 2.454 3.308 0 6-2.692 6-6v-0.667c0-2.158-0.425-4.254-1.258-6.229zM16 20c-2.204 0-4-1.796-4-4s1.796-4 4-4 4 1.796 4 4-1.796 4-4 4z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										22
									
								
								src/icons/IconExpand.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     id="icon-full-screen-enter" | ||||||
|  |     viewBox="0 0 32 32" | ||||||
|  |     width="100%" | ||||||
|  |     height="100%" | ||||||
|  |     style="transition-duration: 0s;" | ||||||
|  |   > | ||||||
|  |     <path | ||||||
|  |       style="transition-duration: 0s;" | ||||||
|  |       d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       style="transition-duration: 0s;" | ||||||
|  |       d="M11.333 17.058l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       style="transition-duration: 0s;" | ||||||
|  |       d="M26 8h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z" | ||||||
|  |     ></path> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										35
									
								
								src/icons/IconInbox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | |||||||
|  | <template> | ||||||
|  |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       fill="inherit" | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M31.671 16.054v0l-6.421-11.708c-0.117-0.213-0.342-0.346-0.583-0.346h-17.333c-0.242 0-0.467 0.133-0.583 0.346l-6.421 11.708c-0.208 0.379-0.329 0.817-0.329 1.279v8c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-8c0-0.462-0.121-0.9-0.329-1.279zM29.333 25.333h-26.667v-8h8.167c0.592 2.296 2.683 4 5.167 4 2.479 0 4.571-1.704 5.167-4h8.167v8zM20.667 14.667c-0.367 0-0.667 0.3-0.667 0.667v0.667c0 2.204-1.796 4-4 4s-4-1.796-4-4v-0.667c0-0.367-0.3-0.667-0.667-0.667h-8.667c-0.021 0-0.038 0-0.058 0l5.121-9.333h16.546l5.121 9.333c-0.021 0-0.038 0-0.058 0h-8.671z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  |  | ||||||
|  |   <!--   <svg | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="24" | ||||||
|  |     height="24" | ||||||
|  |     viewBox="0 0 24 24" | ||||||
|  |     fill="none" | ||||||
|  |     stroke="currentColor" | ||||||
|  |     stroke-width="2" | ||||||
|  |     stroke-linecap="round" | ||||||
|  |     stroke-linejoin="round" | ||||||
|  |     style="transition: stroke-width 0.5s ease" | ||||||
|  |   > | ||||||
|  |     <polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline> | ||||||
|  |     <path | ||||||
|  |       d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" | ||||||
|  |     ></path> | ||||||
|  |   </svg> --> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <!-- <style lang="scss"> | ||||||
|  | svg { | ||||||
|  |   fill: var(--text-color); | ||||||
|  |   stroke: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  --> | ||||||
							
								
								
									
										8
									
								
								src/icons/IconInfo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258s4.254-0.425 6.229-1.258c1.904-0.804 3.617-1.958 5.083-3.429 1.471-1.471 2.621-3.179 3.429-5.083 0.833-1.975 1.258-4.071 1.258-6.229s-0.425-4.254-1.258-6.229zM27.438 9.15c-0.171 0.083-0.342 0.192-0.508 0.321l-0.087 0.067-0.063 0.092c-0.625 0.925-1.604 1.208-2.254 1.008-0.35-0.108-0.529-0.321-0.529-0.637 0-0.9-0.479-1.6-0.863-2.167-0.333-0.492-0.533-0.804-0.467-1.033 0.058-0.2 0.358-0.612 1.629-1.233 1.25 0.996 2.317 2.213 3.142 3.583zM16 2.667c0.271 0 0.538 0.008 0.8 0.025-0.133 0.087-0.292 0.158-0.471 0.242-0.521 0.237-1.238 0.567-1.613 1.483l-0.012 0.025-0.008 0.025c-0.858 2.717-2.008 3.783-2.771 4.487-0.546 0.504-1.017 0.942-1.025 1.667-0.008 0.629 0.329 1.296 1.242 2.458 0.729 0.929 1.429 1.4 2.142 1.45 0.9 0.058 1.533-0.558 2.046-1.054 0.258-0.25 0.525-0.512 0.725-0.579 0.054-0.017 0.175-0.058 0.479 0.242 1.108 1.108 2.012 1.4 2.675 1.617 0.613 0.2 0.892 0.292 1.204 0.887 0.521 0.987 1.308 1.371 1.883 1.65 0.629 0.304 0.708 0.383 0.708 0.708 0 0.212 0.008 0.442 0.012 0.679 0.025 0.85 0.058 2.137-0.325 2.533-0.054 0.058-0.146 0.121-0.354 0.121-1.329 0-1.863 1.183-2.217 1.967-0.1 0.225-0.267 0.587-0.375 0.704-0.871-0.121-1.938 0.875-3.592 2.492-0.367 0.358-0.85 0.833-1.233 1.167 0.042-0.442 0.142-1.104 0.358-2.046 0.292-1.279 0.708-2.658 1.008-3.354 0.196-0.454 0.25-1.163-0.608-1.942-0.462-0.417-1.1-0.792-1.721-1.154-0.446-0.258-0.863-0.504-1.183-0.746-0.358-0.271-0.425-0.408-0.433-0.433 0-0.246 0.046-0.525 0.088-0.821 0.171-1.113 0.425-2.796-1.821-3.779-0.233-0.104-0.479-0.2-0.713-0.292-1.879-0.742-3.817-1.508-4.162-6.667 2.392-2.325 5.662-3.763 9.267-3.763zM3.817 21.413c0.796 0.137 1.375-0.446 1.804-0.875 0.142-0.142 0.438-0.438 0.558-0.467 0.058 0.025 0.479 0.25 1.204 2.167 0.425 1.125 0.446 2.283 0.467 3.621 0.004 0.225 0.008 0.454 0.013 0.696-1.742-1.346-3.142-3.113-4.046-5.142zM9.229 27.483c-0.033-0.571-0.042-1.117-0.054-1.65-0.025-1.404-0.046-2.725-0.554-4.071-0.742-1.958-1.371-2.825-2.175-3-0.779-0.167-1.354 0.413-1.775 0.833-0.171 0.171-0.521 0.521-0.633 0.5-0.046-0.008-0.446-0.137-1.163-1.742-0.138-0.767-0.208-1.55-0.208-2.354 0-3.104 1.067-5.967 2.854-8.233 0.242 1.792 0.721 3.175 1.446 4.196 1.004 1.417 2.296 1.925 3.433 2.375 0.233 0.092 0.454 0.179 0.671 0.275 1.308 0.571 1.208 1.242 1.037 2.354-0.050 0.337-0.104 0.688-0.104 1.033 0 0.988 1.108 1.633 2.279 2.321 0.558 0.329 1.137 0.667 1.496 0.992 0.308 0.279 0.292 0.396 0.279 0.425-0.35 0.813-0.817 2.367-1.129 3.783-0.171 0.767-0.283 1.45-0.338 1.979-0.075 0.779-0.017 1.242 0.192 1.546 0.075 0.108 0.175 0.196 0.287 0.258-2.121-0.15-4.108-0.796-5.842-1.821zM16 29.333c-0.067 0-0.129 0-0.196 0 0.525-0.188 1.167-0.8 2.275-1.883 0.533-0.521 1.083-1.058 1.571-1.475 0.613-0.521 0.871-0.625 0.942-0.642 0.288 0.042 0.788 0.012 1.212-0.525 0.217-0.271 0.367-0.604 0.525-0.958 0.375-0.833 0.6-1.179 1.004-1.179 0.521 0 0.975-0.183 1.308-0.525 0.779-0.8 0.738-2.233 0.704-3.5-0.008-0.229-0.012-0.446-0.012-0.642 0-1.204-0.846-1.613-1.462-1.908-0.496-0.242-0.967-0.467-1.283-1.067-0.567-1.079-1.283-1.313-1.975-1.537-0.592-0.192-1.262-0.412-2.146-1.292-0.587-0.588-1.208-0.779-1.846-0.563-0.488 0.162-0.863 0.529-1.229 0.887-0.371 0.358-0.717 0.7-1.025 0.679-0.175-0.012-0.558-0.15-1.183-0.942-0.637-0.813-0.963-1.354-0.958-1.613 0.004-0.15 0.229-0.367 0.6-0.708 0.808-0.75 2.162-2.004 3.129-5.033 0.167-0.392 0.438-0.529 0.925-0.754 0.5-0.229 1.125-0.517 1.483-1.267 1.704 0.308 3.296 0.938 4.708 1.825-0.988 0.563-1.508 1.108-1.688 1.729-0.246 0.846 0.229 1.542 0.646 2.154 0.325 0.475 0.633 0.925 0.633 1.412 0 0.9 0.563 1.633 1.471 1.912 0.246 0.075 0.521 0.117 0.808 0.117 0.958 0 2.079-0.446 2.875-1.554 0.087-0.063 0.171-0.108 0.246-0.146 0.817 1.713 1.271 3.633 1.271 5.662 0 7.35-5.983 13.333-13.333 13.333z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										20
									
								
								src/icons/IconKey.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | |||||||
|  | <template> | ||||||
|  |   <svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M24 13.333v-2.667c0-2.137-0.833-4.146-2.342-5.658s-3.521-2.342-5.658-2.342-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658v2.667c-1.471 0-2.667 1.196-2.667 2.667v10.667c0 1.471 1.196 2.667 2.667 2.667h16c1.471 0 2.667-1.196 2.667-2.667v-10.667c0-1.471-1.196-2.667-2.667-2.667zM10.667 10.667c0-2.942 2.392-5.333 5.333-5.333s5.333 2.392 5.333 5.333v2.667h-10.667v-2.667zM24 26.667h-16v-10.667h16v10.667c0.004 0 0 0 0 0z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M12 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM12 21.333c0 0 0 0 0 0v0z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M16 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333c0.733 0 1.333-0.6 1.333-1.333s-0.6-1.333-1.333-1.333zM16 21.333c0 0 0 0 0 0v0z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M20 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM20 21.333c0 0 0 0 0 0v0z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										8
									
								
								src/icons/IconMagnet.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <template> | ||||||
|  |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M31.608 13.725l-4-4c-0.304-0.304-0.729-0.442-1.154-0.375-0.421 0.067-0.788 0.333-0.979 0.713-2.938 5.804-5.517 9.617-8.354 12.358-0.004 0.004-0.012 0.012-0.017 0.017-1.008 1.008-2.346 1.563-3.771 1.563s-2.762-0.554-3.771-1.563c-1.008-1.008-1.563-2.346-1.563-3.771s0.554-2.762 1.563-3.771c0.004-0.004 0.012-0.012 0.017-0.017 2.742-2.842 6.55-5.417 12.358-8.354 0.383-0.192 0.646-0.558 0.712-0.979s-0.071-0.85-0.375-1.154l-4-4c-0.404-0.404-1.021-0.504-1.538-0.25-2.271 1.125-4.475 2.438-6.554 3.9-2.175 1.529-4.279 3.271-6.258 5.179-0.004 0.004-0.013 0.012-0.017 0.017-2.521 2.521-3.908 5.867-3.908 9.429s1.387 6.908 3.904 9.429c2.521 2.517 5.867 3.904 9.429 3.904s6.908-1.387 9.429-3.904c0.004-0.004 0.012-0.012 0.017-0.017 1.908-1.979 3.65-4.088 5.179-6.258 1.462-2.079 2.775-4.283 3.904-6.558 0.254-0.517 0.15-1.133-0.254-1.537zM17.075 2.962l2.025 2.025c-1.188 0.629-2.292 1.246-3.317 1.854l-2-2c1.071-0.667 2.167-1.296 3.292-1.879zM20.867 26.217c-2.012 2.008-4.688 3.117-7.533 3.117-2.85 0-5.529-1.108-7.542-3.125-4.154-4.154-4.158-10.917-0.008-15.075 2.183-2.104 4.454-3.942 6.858-5.55l1.971 1.971c-2.879 1.804-5.117 3.571-6.946 5.463-1.504 1.508-2.333 3.517-2.333 5.65 0 2.137 0.833 4.146 2.342 5.658 1.512 1.508 3.521 2.342 5.658 2.342 2.133 0 4.138-0.829 5.65-2.333 1.892-1.829 3.663-4.067 5.463-6.946l1.971 1.971c-1.608 2.404-3.446 4.675-5.55 6.858zM27.158 18.212l-1.996-1.996c0.608-1.025 1.225-2.129 1.854-3.317l2.025 2.025c-0.587 1.125-1.217 2.221-1.883 3.288z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										8
									
								
								src/icons/IconMovie.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M32 10.667c0-3.675-2.992-6.667-6.667-6.667-1.050 0-2.1 0.25-3.029 0.725-0.271 0.138-0.533 0.296-0.783 0.471-0.333-0.546-0.729-1.058-1.196-1.521-1.512-1.508-3.521-2.342-5.658-2.342s-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658c0 1.262 0.3 2.517 0.871 3.633 0.446 0.875 1.062 1.671 1.796 2.329v1.35l-7.475-3.204c-0.413-0.175-0.883-0.133-1.258 0.113s-0.6 0.662-0.6 1.113v14.667c0 0.475 0.254 0.917 0.662 1.154 0.208 0.121 0.438 0.179 0.671 0.179 0.229 0 0.458-0.058 0.662-0.175l7.338-4.196v1.704c0 1.471 1.196 2.667 2.667 2.667h17.333c1.471 0 2.667-1.196 2.667-2.667v-13.333c0-0.567-0.175-1.088-0.479-1.521 0.317-0.783 0.479-1.625 0.479-2.479zM29.333 25.333h-17.333v-10.667h17.333v10.667zM25.333 6.667c2.204 0 4 1.796 4 4 0 0.458-0.079 0.908-0.229 1.333h-6.892c0.3-0.846 0.454-1.746 0.454-2.667 0-0.517-0.050-1.021-0.142-1.517 0.746-0.737 1.742-1.15 2.808-1.15zM14.667 4c2.942 0 5.333 2.392 5.333 5.333 0 0.95-0.246 1.863-0.712 2.667h-7.287c-0.6 0-1.154 0.2-1.6 0.537-0.688-0.912-1.067-2.025-1.067-3.204 0-2.942 2.392-5.333 5.333-5.333zM2.667 27.038v-10.35l6.667 2.858v3.679l-6.667 3.813zM12 28.004c0 0 0-0.004 0 0v-1.337h17.333v1.333l-17.333 0.004z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										38
									
								
								src/icons/IconNowPlaying.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,38 @@ | |||||||
|  | <template> | ||||||
|  |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M31.608 12.392l-3.642-3.642c-0.521-0.521-1.367-0.521-1.887 0-0.375 0.375-0.879 0.583-1.413 0.583s-1.038-0.208-1.413-0.588c-0.779-0.779-0.779-2.050 0-2.829 0.521-0.521 0.521-1.367 0-1.888l-3.646-3.638c-0.25-0.25-0.587-0.392-0.942-0.392s-0.692 0.142-0.942 0.392l-17.333 17.333c-0.521 0.521-0.521 1.367 0 1.887l3.642 3.642c0.25 0.25 0.588 0.392 0.942 0.392s0.692-0.142 0.942-0.392c0.379-0.379 0.879-0.587 1.412-0.587s1.037 0.208 1.412 0.587 0.592 0.879 0.592 1.413c0 0.533-0.208 1.038-0.588 1.413-0.25 0.25-0.392 0.587-0.392 0.942s0.142 0.692 0.392 0.942l3.642 3.642c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l17.333-17.333c0.525-0.517 0.525-1.358 0.004-1.879zM13.333 28.779l-1.896-1.896c0.954-1.767 0.688-4.029-0.804-5.521-0.883-0.875-2.054-1.363-3.3-1.363 0 0 0 0 0 0-0.787 0-1.546 0.196-2.221 0.558l-1.892-1.892 15.446-15.446 1.896 1.896c-0.954 1.767-0.688 4.029 0.804 5.521 0.908 0.908 2.104 1.367 3.3 1.367 0.767 0 1.529-0.188 2.221-0.558l1.896 1.896-15.45 15.438z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M17.137 8.196c-0.258-0.258-0.683-0.258-0.942 0l-8 8c-0.258 0.258-0.258 0.683 0 0.942l6.667 6.667c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l8-8c0.258-0.258 0.258-0.683 0-0.942l-6.667-6.667zM15.333 22.392l-5.725-5.725 7.058-7.058 5.725 5.725-7.058 7.058z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  |  | ||||||
|  |   <!--   <svg | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="24" | ||||||
|  |     height="24" | ||||||
|  |     viewBox="0 0 24 24" | ||||||
|  |     fill="none" | ||||||
|  |     stroke="currentColor" | ||||||
|  |     stroke-width="2" | ||||||
|  |     stroke-linecap="round" | ||||||
|  |     stroke-linejoin="round" | ||||||
|  |     style="transition: stroke-width 0.5s ease" | ||||||
|  |   > | ||||||
|  |     <circle cx="12" cy="12" r="10"></circle> | ||||||
|  |     <polygon points="10 8 16 12 10 16 10 8"></polygon> | ||||||
|  |   </svg> --> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <!-- <style lang="scss" scoped> | ||||||
|  | svg { | ||||||
|  |   fill: var(--text-color); | ||||||
|  |   stroke: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  --> | ||||||
							
								
								
									
										12
									
								
								src/icons/IconPerson.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										12
									
								
								src/icons/IconPlay.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | |||||||
|  | <template> | ||||||
|  |   <svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M31.333 4h-30.667c-0.367 0-0.667 0.3-0.667 0.667v20c0 0.367 0.3 0.667 0.667 0.667h16.417c1.579 1.642 3.796 2.667 6.25 2.667 4.779 0 8.667-3.887 8.667-8.667v-14.667c0-0.367-0.3-0.667-0.667-0.667zM5.333 5.333v2.667h-2.667v-2.667h2.667zM2.667 17.333h2.667v2.667h-2.667v-2.667zM2.667 16v-2.667h2.667v2.667h-2.667zM2.667 12v-2.667h2.667v2.667h-2.667zM2.667 24v-2.667h2.667v2.667h-2.667zM29.333 12h-1.387c-0.404-0.254-0.833-0.479-1.279-0.667v-2h2.667v2.667zM8 6.667h16v4.025c-0.221-0.017-0.442-0.025-0.667-0.025-4.779 0-8.667 3.888-8.667 8.667 0 1.179 0.238 2.308 0.667 3.333h-7.333v-16zM23.333 26.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333zM29.333 8h-2.667v-2.667h2.667v2.667z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M27.675 18.762l-6.667-4c-0.204-0.125-0.462-0.125-0.671-0.008s-0.337 0.342-0.337 0.579v8c0 0.242 0.129 0.462 0.337 0.579 0.1 0.058 0.217 0.087 0.329 0.087 0.121 0 0.238-0.033 0.342-0.096l6.667-4c0.2-0.121 0.325-0.337 0.325-0.571s-0.121-0.45-0.325-0.571zM21.333 22.154v-5.642l4.704 2.821-4.704 2.821z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										29
									
								
								src/icons/IconPopular.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M21.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM18.667 30.667v-8h1.333v8h-1.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M13.333 14.667h-2.667c-0.738 0-1.333 0.596-1.333 1.333v15.333c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-15.333c0-0.738-0.596-1.333-1.333-1.333zM10.667 30.667v-13.333h1.333v13.333h-1.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M5.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM2.667 30.667v-8h1.333v8h-1.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M29.333 9.333h-2.667c-0.738 0-1.333 0.596-1.333 1.333v20.667c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-20.667c0-0.738-0.596-1.333-1.333-1.333zM26.667 30.667v-18.667h1.333v18.667h-1.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M31.333 0h-3.333v1.333h1.592l-11.738 9.267-9.004-2.571c-0.208-0.058-0.429-0.012-0.6 0.121l-7.188 5.75 0.833 1.042 6.917-5.533 9.004 2.571c0.204 0.058 0.429 0.017 0.596-0.117l12.254-9.679v1.817h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										14
									
								
								src/icons/IconProfile.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										16
									
								
								src/icons/IconProfileLock.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M12 16c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.587-8 8 3.588 8 8 8zM12 2.667c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333-5.333-2.392-5.333-5.333 2.392-5.333 5.333-5.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M23.333 14.667c-2.617 0-4.971 1.167-6.558 3.008-1.367-0.225-2.975-0.342-4.775-0.342-5.075 0-8.438 0.9-10.279 2.758-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h14.667c1.308 3.129 4.4 5.333 8 5.333 4.779 0 8.667-3.887 8.667-8.667s-3.887-8.667-8.667-8.667zM2.667 25.333c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629 1.196 0 2.292 0.054 3.267 0.163-0.387 0.983-0.6 2.054-0.6 3.171 0 0.688 0.079 1.358 0.233 2h-12.233zM23.333 30.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M27.333 22.667h-0.667v-0.667c0-1.837-1.496-3.333-3.333-3.333s-3.333 1.496-3.333 3.333v0.667h-0.667c-0.367 0-0.667 0.3-0.667 0.667v4c0 0.367 0.3 0.667 0.667 0.667h8c0.367 0 0.667-0.3 0.667-0.667v-4c0-0.367-0.3-0.667-0.667-0.667zM21.333 22c0-1.104 0.896-2 2-2s2 0.896 2 2v0.667h-4v-0.667zM26.667 26.667h-6.667v-2.667h6.667v2.667z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										17
									
								
								src/icons/IconRequest.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M23.988 13.863c0 0 0 0 0 0 0-0.004 0-0.008-0.004-0.012 0 0 0 0 0-0.004s0-0.004 0-0.008c0 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004 0-0.004 0-0.004 0-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0 0 0-0.004s0-0.004-0.004-0.008c0 0 0 0 0-0.004s-0.004-0.004-0.004-0.008c0 0 0 0 0 0-0.050-0.121-0.133-0.229-0.246-0.304 0 0 0 0 0 0-0.004-0.004-0.012-0.008-0.017-0.012 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0-0.004 0 0 0 0 0-0.004 0-0.004-0.004-0.012-0.008-0.017-0.008 0 0 0 0 0 0-0.133-0.071-0.279-0.092-0.421-0.071 0 0 0 0 0 0-0.004 0-0.008 0-0.008 0s0 0 0 0c-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0c0 0 0 0 0 0-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0.004c0 0 0 0-0.004 0s-0.004 0-0.008 0.004c0 0 0 0 0 0-0.004 0-0.008 0-0.008 0.004 0 0 0 0 0 0-0.008 0-0.012 0.004-0.021 0.008 0 0 0 0 0 0-0.067 0.021-0.133 0.058-0.192 0.1l-14.679 10.662c-0.208 0.154-0.313 0.413-0.262 0.667s0.242 0.458 0.492 0.521l4.946 1.238 1.238 4.946c0.067 0.262 0.287 0.462 0.554 0.5 0.029 0.004 0.063 0.008 0.092 0.008 0.238 0 0.458-0.125 0.579-0.337l2.383-4.175 4.804 1.796c0.204 0.075 0.433 0.050 0.613-0.075s0.288-0.329 0.288-0.55v-14.654c0-0.050-0.004-0.1-0.012-0.15zM10.213 24.367l9.7-7.054-6.171 7.933-3.529-0.879zM15.579 29.563l-0.854-3.408 6.033-7.758-3.358 7.975-1.821 3.192zM18.883 26.288l3.783-8.988v10.404l-3.783-1.417z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M28.488 3.513c-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.762-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658c0 1.858 0.65 3.667 1.829 5.096 1.163 1.408 2.788 2.383 4.571 2.746l0.529-2.613c-2.471-0.504-4.263-2.7-4.263-5.229 0-2.942 2.392-5.333 5.333-5.333 1.8 0 3.462 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.146 0 9.333 4.188 9.333 9.333 0 2.225-0.938 4.367-2.571 5.875l1.808 1.958c1.071-0.988 1.913-2.163 2.504-3.488 0.613-1.371 0.925-2.833 0.925-4.35 0-3.2-1.25-6.217-3.512-8.483z" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <!--     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z" | ||||||
|  |     /> --> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										17
									
								
								src/icons/IconRequested.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M31.904 16.087c0.063-0.471 0.096-0.946 0.096-1.421 0-3.204-1.25-6.221-3.512-8.488-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.763-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658 0.833 4.146 2.342 5.658c1.513 1.508 3.521 2.342 5.658 2.342h8.033c1.542 2.404 4.238 4 7.3 4 4.779 0 8.667-3.887 8.667-8.667 0-0.992-0.167-1.946-0.475-2.833 0.175-0.567 0.304-1.154 0.379-1.746zM8 22.667c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333c1.8 0 3.463 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.063 0 9.196 4.050 9.329 9.083-1.558-1.496-3.671-2.417-5.996-2.417-4.779 0-8.667 3.887-8.667 8.667 0 0.688 0.079 1.358 0.233 2h-6.9zM23.333 28c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M22 22.392l-2.667-2.667-0.942 0.942 3.137 3.137c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l5.804-5.804-0.942-0.942-5.333 5.333z" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <!--     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z" | ||||||
|  |     /> --> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										13
									
								
								src/icons/IconSearch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     viewBox="0 0 32 32" | ||||||
|  |     style="transition-duration: 0s;" | ||||||
|  |   > | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M21.388 21.141c-0.045 0.035-0.089 0.073-0.132 0.116s-0.080 0.085-0.116 0.132c-1.677 1.617-3.959 2.611-6.473 2.611-2.577 0-4.909-1.043-6.6-2.733s-2.733-4.023-2.733-6.6 1.043-4.909 2.733-6.6 4.023-2.733 6.6-2.733 4.909 1.043 6.6 2.733 2.733 4.023 2.733 6.6c0 2.515-0.993 4.796-2.612 6.475zM28.943 27.057l-4.9-4.9c1.641-2.053 2.624-4.657 2.624-7.491 0-3.313-1.344-6.315-3.515-8.485s-5.172-3.515-8.485-3.515-6.315 1.344-8.485 3.515-3.515 5.172-3.515 8.485 1.344 6.315 3.515 8.485 5.172 3.515 8.485 3.515c2.833 0 5.437-0.983 7.491-2.624l4.9 4.9c0.521 0.521 1.365 0.521 1.885 0s0.521-1.365 0-1.885z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										24
									
								
								src/icons/IconSettings.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="24" | ||||||
|  |     height="24" | ||||||
|  |     viewBox="0 0 24 24" | ||||||
|  |     fill="none" | ||||||
|  |     stroke="currentColor" | ||||||
|  |     stroke-width="2" | ||||||
|  |     stroke-linecap="round" | ||||||
|  |     stroke-linejoin="round" | ||||||
|  |     style="transition: stroke-width 0.5s ease" | ||||||
|  |   > | ||||||
|  |     <line x1="4" y1="21" x2="4" y2="14"></line> | ||||||
|  |     <line x1="4" y1="10" x2="4" y2="3"></line> | ||||||
|  |     <line x1="12" y1="21" x2="12" y2="12"></line> | ||||||
|  |     <line x1="12" y1="8" x2="12" y2="3"></line> | ||||||
|  |     <line x1="20" y1="21" x2="20" y2="16"></line> | ||||||
|  |     <line x1="20" y1="12" x2="20" y2="3"></line> | ||||||
|  |     <line x1="1" y1="14" x2="7" y2="14"></line> | ||||||
|  |     <line x1="9" y1="8" x2="15" y2="8"></line> | ||||||
|  |     <line x1="17" y1="16" x2="23" y2="16"></line> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										18
									
								
								src/icons/IconShow.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       d="M29.333 6.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v18.667c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-18.667c0-1.471-1.196-2.667-2.667-2.667zM29.333 28h-26.667v-18.667h26.667v18.667z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       d="M4.667 26.667h17.333c0.367 0 0.667-0.3 0.667-0.667v-14.667c0-0.367-0.3-0.667-0.667-0.667h-17.333c-0.367 0-0.667 0.3-0.667 0.667v14.667c0 0.367 0.3 0.667 0.667 0.667zM5.333 12h16v13.333h-16v-13.333z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       d="M26 16c1.104 0 2-0.896 2-2s-0.896-2-2-2-2 0.896-2 2 0.896 2 2 2zM26 13.333c0.367 0 0.667 0.3 0.667 0.667s-0.3 0.667-0.667 0.667-0.667-0.3-0.667-0.667 0.3-0.667 0.667-0.667z" | ||||||
|  |     ></path> | ||||||
|  |     <path d="M24 24h4v1.333h-4v-1.333z"></path> | ||||||
|  |     <path d="M24 21.333h4v1.333h-4v-1.333z"></path> | ||||||
|  |     <path | ||||||
|  |       d="M22.917 2.229l-0.688-1.146-6.854 4.112-5.508-4.129-0.8 1.067 5.867 4.4c0.117 0.088 0.258 0.133 0.4 0.133 0.117 0 0.238-0.033 0.342-0.096l7.242-4.342z" | ||||||
|  |     ></path> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										18
									
								
								src/icons/IconShrink.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     id="icon-full-screen-exit" | ||||||
|  |     viewBox="0 0 32 32" | ||||||
|  |     width="100%" | ||||||
|  |     height="100%" | ||||||
|  |   > | ||||||
|  |     <path | ||||||
|  |       d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       d="M26 7.725l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       d="M11.333 17.333h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z" | ||||||
|  |     ></path> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										8
									
								
								src/icons/IconThumbsDown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M28 13.333c0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2 0-1.208-0.546-2.329-1.442-3.075 0.071-0.3 0.108-0.612 0.108-0.925 0-2.204-1.796-4-4-4h-3.621c-3.842 0-7.683 0.946-11.104 2.729 0 0-0.004 0-0.004 0.004-2.429 1.279-3.938 3.792-3.938 6.563v4.671c0 1.137 0.263 2.275 0.763 3.292 0.504 1.025 1.246 1.925 2.146 2.613 1.237 0.942 2.417 1.542 3.554 2.117 2.175 1.104 3.896 1.979 5.412 4.962 0.313 0.783 0.817 1.417 1.475 1.846 0.708 0.462 1.529 0.633 2.321 0.483 0.921-0.175 1.717-0.767 2.246-1.671 0.508-0.871 0.758-2.004 0.742-3.358 0-1.283-0.296-2.863-1.029-4.25h2.375c2.204 0 4-1.796 4-4 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.533-1.292 0.533-2zM22.667 6.667h-2.667v1.333h4c0.208 0 0.404 0.046 0.587 0.137 0.454 0.221 0.746 0.683 0.746 1.196 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.613 0.442-0.992 0.446 0 0-0.004 0-0.004 0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.325 0 0.637 0.121 0.879 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.213-0.554 0.333-0.883 0.333h-2.667v1.333h2.667c0.379 0 0.742 0.163 0.996 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.733-0.6 1.333-1.333 1.333h-5.333c-0.6 0-1.125 0.4-1.283 0.979s0.083 1.192 0.6 1.5c1.379 0.829 2.008 2.887 2.008 4.45 0 0.004 0 0.012 0 0.017 0.021 1.633-0.475 2.321-0.813 2.387-0.254 0.046-0.629-0.188-0.833-0.725-0.017-0.046-0.033-0.087-0.058-0.129-1.917-3.813-4.304-5.025-6.617-6.2-1.033-0.525-2.1-1.067-3.146-1.863-1.162-0.883-1.858-2.296-1.858-3.779v-4.675c0-1.775 0.963-3.388 2.508-4.2 3.042-1.587 6.454-2.429 9.871-2.429h3.621c0.733 0 1.333 0.6 1.333 1.333 0 0.229-0.058 0.45-0.163 0.646-0.238 0.425-0.688 0.688-1.171 0.688z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										8
									
								
								src/icons/IconThumbsUp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|  |     <path | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       d="M27.462 16.667c0.346-0.6 0.538-1.292 0.538-2 0-2.204-1.796-4-4-4h-2.375c0.733-1.387 1.029-2.967 1.029-4.25 0.017-1.358-0.233-2.487-0.742-3.358-0.525-0.904-1.321-1.5-2.246-1.671-0.788-0.15-1.613 0.025-2.321 0.483-0.654 0.425-1.163 1.063-1.475 1.846-1.517 2.983-3.237 3.858-5.412 4.962-1.137 0.579-2.313 1.175-3.554 2.117-0.904 0.688-1.646 1.588-2.146 2.612-0.5 1.017-0.763 2.154-0.763 3.292v4.671c0 2.771 1.508 5.283 3.938 6.563 0 0 0.004 0 0.004 0.004 3.421 1.788 7.263 2.729 11.104 2.729h3.625c2.204 0 4-1.796 4-4 0-0.317-0.038-0.625-0.108-0.925 0.896-0.746 1.442-1.867 1.442-3.075 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2s-0.188-1.4-0.538-2zM23.837 26.021c0.108 0.196 0.163 0.417 0.163 0.646 0 0.733-0.6 1.333-1.333 1.333h-3.621c-3.412 0-6.825-0.837-9.871-2.429-1.55-0.817-2.508-2.425-2.508-4.2v-4.671c0-1.483 0.696-2.896 1.858-3.779 1.050-0.796 2.117-1.338 3.146-1.863 2.308-1.175 4.696-2.387 6.617-6.2 0.021-0.042 0.042-0.083 0.058-0.129 0.2-0.533 0.579-0.771 0.833-0.725 0.337 0.063 0.833 0.75 0.813 2.388 0 0.004 0 0.013 0 0.017 0 1.563-0.629 3.621-2.008 4.45-0.512 0.308-0.758 0.921-0.6 1.5 0.158 0.575 0.683 0.975 1.283 0.975h5.333c0.733 0 1.333 0.6 1.333 1.333 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.617 0.446-0.996 0.446h-2.667v1.333h2.667c0.325 0 0.642 0.121 0.883 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.212-0.554 0.333-0.879 0.333 0 0-0.004 0-0.004 0v0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.375 0 0.738 0.163 0.992 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.512-0.292 0.975-0.746 1.196-0.183 0.092-0.383 0.137-0.587 0.137h-4v1.333h2.667c0.483 0 0.933 0.262 1.171 0.688z" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										40
									
								
								src/icons/IconUpcoming.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | |||||||
|  | <template> | ||||||
|  |   <svg id="icon-calendar4" viewBox="0 0 32 32" width="100%" height="100%"> | ||||||
|  |     <path fill="inherit" d="M0 1.333h6.667v2.667h-6.667v-2.667z"></path> | ||||||
|  |     <path fill="inherit" d="M12 1.333h8v2.667h-8v-2.667z"></path> | ||||||
|  |     <path fill="inherit" d="M25.333 1.333h6.667v2.667h-6.667v-2.667z"></path> | ||||||
|  |     <path fill="inherit" d="M8 0h2.667v9.333h-2.667v-9.333z"></path> | ||||||
|  |     <path fill="inherit" d="M21.333 0h2.667v9.333h-2.667v-9.333z"></path> | ||||||
|  |     <path fill="inherit" d="M12 5.333h8v2.667h-8v-2.667z"></path> | ||||||
|  |     <path | ||||||
|  |       fill="inherit" | ||||||
|  |       d="M29.333 5.333h-4v2.667h4v21.333h-20v-6c0-0.367-0.3-0.667-0.667-0.667h-6v-14.667h4v-2.667h-4c-1.471 0-2.667 1.196-2.667 2.667v14.667c0 5.146 4.188 9.333 9.333 9.333h20c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM2.8 24h5.2v5.2c-2.608-0.533-4.667-2.592-5.2-5.2z" | ||||||
|  |     ></path> | ||||||
|  |     <path fill="inherit" d="M14.667 12h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M18.667 12h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M22.667 12h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M10.667 14.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M14.667 14.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M18.667 14.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M22.667 14.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M6.667 14.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M10.667 17.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M14.667 17.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M18.667 17.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M22.667 17.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M6.667 17.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M10.667 20h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M14.667 20h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M18.667 20h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M22.667 20h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M6.667 20h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M10.667 22.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M14.667 22.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M18.667 22.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M22.667 22.667h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M10.667 25.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M14.667 25.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M18.667 25.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |     <path fill="inherit" d="M22.667 25.333h2.667v1.333h-2.667v-1.333z"></path> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										70
									
								
								src/icons/tmdb-logo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     version="1.1" | ||||||
|  |     width="100%" | ||||||
|  |     height="100%" | ||||||
|  |     viewBox="0 0 225 199" | ||||||
|  |     fill="none" | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     style="outline-offset: -5px" | ||||||
|  |   > | ||||||
|  |     <title>TMDB Logo</title> | ||||||
|  |  | ||||||
|  |     <path | ||||||
|  |       d="M202.545 185.929C215.58 185.929 224.402 177.107 224.402 164.071V21.8571C224.402 8.82143 215.58 0 202.545 0H21.8571C8.82143 0 0 8.82143 0 21.8571V198.937L11.2143 185.937V21.8571C11.2241 15.9833 15.9833 11.2241 21.8571 11.2143H202.545C208.418 11.2241 213.178 15.9833 213.188 21.8571V164.071C213.178 169.945 208.418 174.704 202.545 174.714H38.4911L27.2768 185.929L27.2054 185.839" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M42.1677 67.911H44.0133C44.9484 67.911 45.699 67.7137 46.265 67.3192C46.8556 66.9247 47.1509 66.2589 47.1509 65.3219C47.1509 64.3849 46.8556 63.7192 46.265 63.3247C45.699 62.9301 44.9484 62.7329 44.0133 62.7329H42.1677V67.911ZM44.0133 57.5548C45.3914 57.5548 46.6218 57.7644 47.7046 58.1836C48.8119 58.5781 49.747 59.1329 50.5099 59.8479C51.2727 60.5384 51.851 61.3644 52.2448 62.326C52.6631 63.263 52.8723 64.2616 52.8723 65.3219C52.8723 66.8014 52.5278 68.0466 51.8387 69.0575C51.1743 70.0685 50.2884 70.8575 49.181 71.4247L58.04 83.2603L58.1686 83.4452H51.9025L51.7649 83.2603L44.0133 73.089H42.1677V83.4452H41.9832H37V83.2603V57.7397L37.1846 57.5548H44.0133Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M76.6677 57.5548H76.8523V62.7329H76.6677H64.3021V67.3562H73.3456H73.5302V72.5342H73.3456H64.3021V78.0857H77.0368H77.2214V83.4452H77.0368H59.1344V83.2603L59.0419 57.5548H59.3189H76.6677Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M94.7735 72.3493L96.8775 74.1986C97.4435 73.1137 97.7264 71.8808 97.7264 70.5C97.7264 69.3411 97.5173 68.2685 97.0989 67.2822C96.7052 66.2959 96.1392 65.4452 95.401 64.7301C94.6873 63.9904 93.8507 63.411 92.8909 62.9918C91.9312 62.5726 90.8977 62.363 89.7903 62.363C88.6829 62.363 87.6494 62.5726 86.6897 62.9918C85.73 63.411 84.881 63.9904 84.1427 64.7301C83.4291 65.4452 82.8631 66.2959 82.4448 67.2822C82.051 68.2685 81.8542 69.3411 81.8542 70.5C81.8542 71.6589 82.051 72.7315 82.4448 73.7178C82.8631 74.7041 83.4291 75.5671 84.1427 76.3068C84.881 77.0219 85.73 77.589 86.6897 78.0082C87.6494 78.4274 88.6829 78.637 89.7903 78.637C90.9961 78.637 92.1281 78.3904 93.1862 77.8973L91.2668 76.2329V75.863L94.4043 72.3493H94.7735ZM103.079 79.9315L99.9412 83.4452H99.5721L97.505 81.6699C96.3976 82.4096 95.1918 82.989 93.8876 83.4082C92.6079 83.8027 91.2422 84 89.7903 84C87.8955 84 86.1114 83.6548 84.438 82.9644C82.7893 82.274 81.3497 81.3247 80.1193 80.1164C78.8889 78.8836 77.9169 77.4534 77.2032 75.826C76.4896 74.174 76.1328 72.3986 76.1328 70.5C76.1328 68.6014 76.4896 66.8384 77.2032 65.211C77.9169 63.5589 78.8889 62.1288 80.1193 60.9205C81.3497 59.6877 82.7893 58.726 84.438 58.0356C86.1114 57.3452 87.8955 57 89.7903 57C91.6851 57 93.4569 57.3452 95.1057 58.0356C96.779 58.726 98.2309 59.6877 99.4613 60.9205C100.692 62.1288 101.664 63.5589 102.377 65.211C103.091 66.8384 103.448 68.6014 103.448 70.5C103.448 71.9055 103.251 73.237 102.857 74.4945C102.464 75.7274 101.91 76.8863 101.196 77.9712L103.079 79.5616V79.9315Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M110.657 57.5548H110.842V71.2397C110.842 72.4973 111.002 73.5945 111.322 74.5315C111.642 75.4438 112.085 76.2082 112.651 76.8247C113.217 77.4164 113.881 77.8726 114.644 78.1932C115.407 78.489 116.231 78.637 117.117 78.637C118.003 78.637 118.827 78.489 119.59 78.1932C120.353 77.8726 121.017 77.4164 121.583 76.8247C122.149 76.2082 122.592 75.4438 122.912 74.5315C123.232 73.5945 123.392 72.4973 123.392 71.2397V57.5548H123.577H128.375H128.56V71.9795C128.56 73.8781 128.252 75.5795 127.637 77.0836C127.046 78.563 126.234 79.8205 125.201 80.8562C124.167 81.8671 122.949 82.6438 121.546 83.1863C120.168 83.7288 118.692 84 117.117 84C115.542 84 114.053 83.7288 112.651 83.1863C111.272 82.6438 110.067 81.8671 109.033 80.8562C108 79.8205 107.175 78.563 106.56 77.0836C105.969 75.5795 105.674 73.8781 105.674 71.9795V57.5548H105.859H110.657Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M149.055 57.5548H149.239V62.7329H149.055H136.689V67.3562H145.733H145.917V72.5342H145.733H136.689V78.0236H149.424H149.608V83.4452H149.424H131.521V83.2603V57.5548H131.706H149.055Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M161.497 64.0274C161.079 63.4603 160.611 63.0411 160.094 62.7699C159.602 62.4986 159.122 62.363 158.655 62.363C158.015 62.363 157.547 62.5356 157.252 62.8808C156.957 63.2014 156.809 63.6452 156.809 64.2123C156.809 64.7055 157.08 65.137 157.621 65.5068C158.163 65.8767 158.827 66.2466 159.614 66.6164C160.427 66.9863 161.3 67.4055 162.235 67.874C163.195 68.3178 164.069 68.8849 164.856 69.5753C165.668 70.2658 166.345 71.1041 166.886 72.0904C167.428 73.0767 167.698 74.2726 167.698 75.6781C167.698 76.7877 167.477 77.8479 167.034 78.8589C166.615 79.8699 166.013 80.7575 165.225 81.5219C164.462 82.2616 163.552 82.8658 162.494 83.3342C161.435 83.7781 160.279 84 159.024 84C158.064 84 157.129 83.8767 156.219 83.6301C155.308 83.3836 154.471 83.0137 153.709 82.5205C152.946 82.0274 152.269 81.4356 151.678 80.7452C151.112 80.0301 150.669 79.2041 150.349 78.2671L154.594 75.4192H154.964C155.431 76.6521 156.034 77.5027 156.772 77.9712C157.535 78.4151 158.286 78.637 159.024 78.637C159.959 78.637 160.71 78.3781 161.276 77.8603C161.866 77.3425 162.161 76.6151 162.161 75.6781C162.161 74.9384 161.891 74.3096 161.349 73.7918C160.808 73.274 160.131 72.7932 159.319 72.3493C158.532 71.8808 157.658 71.4247 156.698 70.9808C155.763 70.537 154.89 70.0192 154.078 69.4274C153.29 68.811 152.626 68.0959 152.084 67.2822C151.543 66.4438 151.272 65.4205 151.272 64.2123C151.272 63.1521 151.457 62.1781 151.826 61.2904C152.22 60.4027 152.749 59.6507 153.413 59.0342C154.078 58.3932 154.853 57.9 155.739 57.5548C156.649 57.1849 157.621 57 158.655 57C160.23 57 161.571 57.3329 162.678 57.9986C163.786 58.6644 164.844 59.6877 165.853 61.0685L161.866 64.0274H161.497Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M187.815 57.5548H188V62.5479V62.7329H180.248V83.2603V83.4452H175.081V83.2603V62.7329H167.329V62.5479V57.5548H167.514H187.815Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M61.9051 108.406L49.9478 94.7412H48V126.338H54.149V108.97L61.9051 117.327L69.6613 108.97L69.6263 126.338H75.7666V94.7412H73.8625L61.9051 108.406Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M96.831 94.7412C76.4049 94.7412 76.4049 126.338 96.831 126.338C117.257 126.338 117.257 94.7412 96.831 94.7412ZM96.831 119.995C84.9585 119.995 84.9585 101.048 96.831 101.048C108.703 101.048 108.703 119.995 96.831 119.995Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M158.109 95.6987H152.364V126.338H158.109V95.6987Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M169.222 120.21V114.082H179.357V107.954H169.222V101.827H180.551V95.6987H162.896V126.338H181.088V120.21H169.222Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M132.262 112.402L124.095 95.6987H116.938L131.565 127.295H132.959L147.577 95.6987H140.42L132.262 112.402Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M42.425 121.5H42.6V126.4H42.425H37.7V126.225V121.5H37.875H42.425Z" | ||||||
|  |       fill="#01D277" | ||||||
|  |     /> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||