Compare commits
	
		
			115 Commits
		
	
	
		
			v1.0.0
			...
			feat/botto
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2665a27803 | |||
| 74b96225c6 | |||
| f180b7f39b | |||
| a2fbfcb13c | |||
| d640f7f882 | |||
| d43c12b103 | |||
| 38c3792675 | |||
| ac2785abd5 | |||
| 1ff6a0e831 | |||
| 7a3b709404 | |||
| 
						 | 
					d63cb4ac52 | ||
| b6ee1cf906 | |||
| 60201b1b67 | |||
| a8b8603649 | |||
| e193528fe9 | |||
| 73afb34964 | |||
| 65bbc453e6 | |||
| 
						 | 
					188477ab64 | ||
| 
						 | 
					a31bfb6b39 | ||
| 681ed69ef0 | |||
| b771428b4d | |||
| fc0103ee5d | |||
| 55067b81b8 | |||
| dfe2b5df09 | |||
| dc0c435163 | |||
| 9d1ac56b9a | |||
| fc2c3664d9 | |||
| 0bd45ed777 | |||
| 3912766982 | |||
| 3becce2a6c | |||
| 20b8692c91 | |||
| 14ac780aa5 | |||
| d836870612 | |||
| bc6f706e4a | |||
| 6ac6a9b039 | |||
| 85be80d712 | |||
| 105be1e411 | |||
| 010830243e | |||
| 923dc46dc7 | |||
| f2ef5366f5 | |||
| 20380a4587 | |||
| 069ef2c458 | |||
| 2f430b2d8f | |||
| f7a579a438 | |||
| b9ddd998bc | |||
| ae59d02df2 | |||
| ec205bab0c | |||
| ed49d825b8 | |||
| a9db8be46a | |||
| 1caa3c7fae | |||
| 2ea4bffd49 | |||
| 5ae52f59fc | |||
| a7e6d25d3f | |||
| 83751a4e3e | |||
| 0e9daab187 | |||
| 4390491873 | |||
| d620a4cc2e | |||
| 32669e5bef | |||
| 6edad3991f | |||
| 50acf0bedc | |||
| d4369ec7a4 | |||
| c16543099e | |||
| f2a65d755c | |||
| 1fd48edd42 | |||
| 68e45303c6 | |||
| 532993e9dd | |||
| d19d72ce0c | |||
| d1820a08cf | |||
| bc73665b12 | |||
| 9edb19569a | |||
| 7802a89d15 | |||
| 915260f41b | |||
| 0d57e9a03b | |||
| 582207d453 | |||
| b1b08bfa04 | |||
| 14e883672d | |||
| 7a405140db | |||
| 35497f5bd2 | |||
| 91b19785d6 | |||
| a301d21cc2 | |||
| a2a4b9a553 | |||
| 45f45559fd | |||
| 458256132a | |||
| 0f2c166e1c | |||
| 1c7a688cb8 | |||
| 6269f178e9 | |||
| 3e7527ee19 | |||
| 2236316863 | |||
| cc2fded193 | |||
| f32e0a8ab0 | |||
| ec6e6d2ba0 | |||
| ca85635b03 | |||
| 32257dc64e | |||
| 6bba319735 | |||
| dcce972fdc | |||
| 32e25fb983 | |||
| e7882869e6 | |||
| d0a251f69a | |||
| 9bc7f29162 | |||
| 3ff963f007 | |||
| bcfce66ec0 | |||
| 33e3ee3489 | |||
| e3502a7690 | |||
| 8d09ba4d07 | |||
| ba670d06aa | |||
| a11ad2f651 | |||
| 755bd116d5 | |||
| 9e33784781 | |||
| 470bcdd72e | |||
| d56a7d4dfe | |||
| b46e586c92 | |||
| 563eb3f1ef | |||
| 98644513ad | |||
| 3033db02b8 | |||
| 70a6ed189b | 
							
								
								
									
										44
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
---
 | 
			
		||||
kind: pipeline
 | 
			
		||||
type: docker
 | 
			
		||||
name: seasoned build
 | 
			
		||||
 | 
			
		||||
platform:
 | 
			
		||||
  os: linux
 | 
			
		||||
  arch: amd64
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
- name: frontend_install
 | 
			
		||||
  image: node:13.6.0
 | 
			
		||||
  commands:
 | 
			
		||||
    - node -v
 | 
			
		||||
    - yarn --version
 | 
			
		||||
- name: deploy
 | 
			
		||||
  image: appleboy/drone-ssh
 | 
			
		||||
  pull: true
 | 
			
		||||
  secrets:
 | 
			
		||||
    - ssh_key
 | 
			
		||||
  when:
 | 
			
		||||
    event:
 | 
			
		||||
      - push
 | 
			
		||||
    branch:
 | 
			
		||||
      - master
 | 
			
		||||
      - drone-test
 | 
			
		||||
    status: success
 | 
			
		||||
  settings:
 | 
			
		||||
    host: 10.0.0.114
 | 
			
		||||
    username: root
 | 
			
		||||
    key:
 | 
			
		||||
      from_secret: ssh_key
 | 
			
		||||
    command_timeout: 600s
 | 
			
		||||
    script:
 | 
			
		||||
      - /home/kevin/deploy/seasoned.sh
 | 
			
		||||
 | 
			
		||||
trigger:
 | 
			
		||||
  branch:
 | 
			
		||||
    - master
 | 
			
		||||
  event:
 | 
			
		||||
    include:
 | 
			
		||||
      - pull_request
 | 
			
		||||
      - push
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "tabWidth": 2,
 | 
			
		||||
  "useTabs": false,
 | 
			
		||||
  "semi": true,
 | 
			
		||||
  "singleQuote": false,
 | 
			
		||||
  "bracketSpacing": true,
 | 
			
		||||
  "arrowParens": "avoid",
 | 
			
		||||
  "vueIndentScriptAndStyle": false,
 | 
			
		||||
  "trailingComma": "none"
 | 
			
		||||
}
 | 
			
		||||
@@ -21,10 +21,6 @@
 | 
			
		||||
      <symbol id="icon_now_playing" viewBox="0 0 30 30">
 | 
			
		||||
        <title>Now Playing</title>
 | 
			
		||||
        <path d="M27.9847266,7.50322266 C25.9822852,4.03494141 22.749082,1.55390625 18.8806055,0.517382812 C15.0121875,-0.519257812 10.9716797,0.0127148437 7.50322266,2.01527344 C4.03482422,4.01777344 1.55390625,7.25097656 0.517382812,11.1194531 C-0.519140625,14.9878711 0.0128320312,19.0284961 2.01527344,22.4967773 C4.01765625,25.9650586 7.25097656,28.4460937 11.1193945,29.4826172 C12.4111523,29.8287891 13.7219531,30 15.0244336,30 C17.6224219,30 20.1866016,29.3186133 22.4968359,27.9847852 C25.9651172,25.9823437 28.4461523,22.7491406 29.4826758,18.8806641 C30.5192578,15.0121289 29.987168,10.9716211 27.9847266,7.50322266 Z M27.9743555,18.476543 C27.0457617,21.9421289 24.8231836,24.8387109 21.715957,26.6326172 C18.6088477,28.426582 14.989043,28.9030664 11.523457,27.9745898 C8.0578125,27.0459961 5.16128906,24.823418 3.36732422,21.7161914 C1.57341797,18.609082 1.096875,14.9892188 2.02552734,11.5235742 C2.95417969,8.05798828 5.17675781,5.16152344 8.28392578,3.3675 C10.35375,2.17248047 12.6505664,1.56210937 14.9782031,1.56210937 C16.1448047,1.56210937 17.3195508,1.71550781 18.4763672,2.02552734 C21.9419531,2.95412109 24.8385352,5.17669922 26.6324414,8.28392578 C28.4264063,11.3910937 28.9030078,15.0108984 27.9743555,18.476543 Z M22.1940234,13.5850781 L12.5538281,8.01925781 C12.0422461,7.72388672 11.4314648,7.72400391 10.9198828,8.01919922 C10.4083008,8.31451172 10.1028516,8.84355469 10.1028516,9.43423828 L10.1028516,20.5658789 C10.1028516,21.1565625 10.4082422,21.6855469 10.9198828,21.980918 C11.1756445,22.1286328 11.4561328,22.2024023 11.7367383,22.2024023 C12.0174023,22.2024023 12.2980078,22.128457 12.5537695,21.9808594 L22.194082,16.4150977 C22.7056055,16.119668 23.0109375,15.5906836 23.0109375,15 C23.0109375,14.409375 22.7055469,13.8803906 22.1940234,13.5850781 Z M21.4132031,15.0629297 L11.7729492,20.6286914 C11.7611719,20.6355469 11.7366211,20.649668 11.7005273,20.6286914 C11.6643164,20.6077734 11.6643164,20.5795312 11.6643164,20.5659375 L11.6643164,9.43429687 C11.6643164,9.42070312 11.6643164,9.39246094 11.7005273,9.37154297 C11.714707,9.36333984 11.7270703,9.36052734 11.7376172,9.36052734 C11.7540234,9.36052734 11.7658594,9.36738281 11.7730664,9.37154297 L21.4132617,14.9373633 C21.4250391,14.9441602 21.4494727,14.9582812 21.4494727,15.0001172 C21.4494727,15.0419531 21.4249219,15.0561328 21.4132031,15.0629297 Z M24.2169727,7.87734375 C22.3601953,5.47863281 19.5689648,3.86707031 16.5588867,3.45580078 C16.1321484,3.39738281 15.7380469,3.69638672 15.6796289,4.12371094 C15.6213281,4.55091797 15.920332,4.94455078 16.3475391,5.00296875 C18.9556641,5.35927734 21.3738867,6.75544922 22.9822266,8.83318359 C23.1360937,9.03193359 23.3668945,9.13599609 23.6001562,9.13599609 C23.7670898,9.13599609 23.9353125,9.08273437 24.0774609,8.97257813 C24.418418,8.70867187 24.4808789,8.21830078 24.2169727,7.87734375 Z" fill-rule="nonzero"></path>
 | 
			
		||||
      </symbol>
 | 
			
		||||
            <symbol id="icon_top_rated" viewBox="0 0 30 30">
 | 
			
		||||
        <title>Top Rated</title>
 | 
			
		||||
        <path d="M24.7750847,5.22491532 C24.7021599,5.15199056 24.6169531,5.09595364 24.52407,5.05757218 C24.4304192,5.01919073 24.3313951,5 24.2323709,5 L8.84447835,5.00076763 C8.41997947,5.00076763 8.07684927,5.34466546 8.07684927,5.76839671 C8.07684927,6.19289559 8.4207471,6.53602579 8.84447835,6.53602579 L22.3785467,6.53525816 L5.22510723,23.6894653 C4.92496426,23.9896082 4.92496426,24.4747498 5.22510723,24.7748928 C5.3747949,24.9245804 5.57130794,24.9998081 5.76782099,24.9998081 C5.96433403,24.9998081 6.16084708,24.9245804 6.31053475,24.7748928 L23.4647418,7.62068568 L23.4647418,21.1539864 C23.4647418,21.5784853 23.807872,21.9216155 24.2323709,21.9216155 C24.6568698,21.9216155 25,21.5784853 25,21.1539864 L25,5.76762908 C25,5.66860493 24.9808093,5.56958078 24.9424278,5.47593003 C24.9040464,5.38304691 24.8480094,5.29784008 24.7750847,5.22491532 Z"></path>
 | 
			
		||||
      </symbol>
 | 
			
		||||
            <symbol id="icon_popular" viewBox="0 0 30 30">
 | 
			
		||||
        <title>Popular</title>
 | 
			
		||||
@@ -125,9 +121,8 @@
 | 
			
		||||
      l246.17-246.175C512.959,136.021,512.959,129.804,509.121,125.966z" fill-rule="nozero" transform="scale(1.4)"></path>
 | 
			
		||||
    </symbol>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="text/javascript" src="dist/build.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="/build.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
 | 
			
		||||
  <script src="https://cdn.ravenjs.com/3.23.1/vue/raven.min.js" crossorigin="anonymous"></script>
 | 
			
		||||
  <!-- <script>Raven.config('https://c1fa1a17de3d4b24abcd05161648fe4d@sentry.io/300063').install();</script> -->
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^0.18.1",
 | 
			
		||||
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
 | 
			
		||||
    "chart.js": "^2.9.2",
 | 
			
		||||
    "connect-history-api-fallback": "^1.3.0",
 | 
			
		||||
    "express": "^4.16.1",
 | 
			
		||||
    "vue": "^2.5.2",
 | 
			
		||||
@@ -29,7 +30,7 @@
 | 
			
		||||
    "@babel/runtime": "^7.4.5",
 | 
			
		||||
    "babel-loader": "^8.0.6",
 | 
			
		||||
    "cross-env": "^3.0.0",
 | 
			
		||||
    "css-loader": "^0.25.0",
 | 
			
		||||
    "css-loader": "^3.4.2",
 | 
			
		||||
    "documentation": "^11.0.0",
 | 
			
		||||
    "file-loader": "^0.9.0",
 | 
			
		||||
    "node-sass": "^4.5.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										86
									
								
								src/App.vue
									
									
									
									
									
								
							@@ -1,37 +1,32 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div id="app">
 | 
			
		||||
 | 
			
		||||
    <!-- Header and hamburger navigation -->
 | 
			
		||||
    <navigation></navigation>
 | 
			
		||||
 | 
			
		||||
    <!-- Header with search field -->
 | 
			
		||||
 | 
			
		||||
    <!-- TODO move this to the navigation component -->
 | 
			
		||||
    <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>
 | 
			
		||||
 | 
			
		||||
    <movie-popup
 | 
			
		||||
      v-if="moviePopupIsVisible"
 | 
			
		||||
      :id="popupID"
 | 
			
		||||
      :type="popupType"
 | 
			
		||||
    ></movie-popup>
 | 
			
		||||
 | 
			
		||||
    <darkmode-toggle />
 | 
			
		||||
 | 
			
		||||
    <!-- Display the component assigned to the given route (default: home) -->
 | 
			
		||||
    <router-view class="content" :key="$route.fullPath"></router-view>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Vue from 'vue'
 | 
			
		||||
import Navigation from '@/components/Navigation'
 | 
			
		||||
import MoviePopup from '@/components/MoviePopup'
 | 
			
		||||
import SearchInput from '@/components/SearchInput'
 | 
			
		||||
import DarkmodeToggle from '@/components/ui/darkmodeToggle'
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import Navigation from "@/components/Navigation";
 | 
			
		||||
import MoviePopup from "@/components/MoviePopup";
 | 
			
		||||
import SearchInput from "@/components/SearchInput";
 | 
			
		||||
import DarkmodeToggle from "@/components/ui/darkmodeToggle";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'app',
 | 
			
		||||
  name: "app",
 | 
			
		||||
  components: {
 | 
			
		||||
    Navigation,
 | 
			
		||||
    MoviePopup,
 | 
			
		||||
@@ -40,32 +35,35 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      query: '',
 | 
			
		||||
      query: "",
 | 
			
		||||
      moviePopupIsVisible: false,
 | 
			
		||||
      popupID: 0,
 | 
			
		||||
      popupType: 'movie'
 | 
			
		||||
    }
 | 
			
		||||
      popupType: "movie"
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    let that = this
 | 
			
		||||
    let that = this;
 | 
			
		||||
    Vue.prototype.$popup = {
 | 
			
		||||
      get isOpen() {
 | 
			
		||||
        return that.moviePopupIsVisible
 | 
			
		||||
        return that.moviePopupIsVisible;
 | 
			
		||||
      },
 | 
			
		||||
      open: (id, type) => {
 | 
			
		||||
        this.popupID = id || this.popupID
 | 
			
		||||
        this.popupType = type || this.popupType
 | 
			
		||||
        this.moviePopupIsVisible = true
 | 
			
		||||
        console.log('opened')
 | 
			
		||||
        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)
 | 
			
		||||
        this.moviePopupIsVisible = false;
 | 
			
		||||
        console.log("closed");
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    console.log(
 | 
			
		||||
      "MoviePopup registered at this.$popup and has state: ",
 | 
			
		||||
      this.$popup.isOpen
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@@ -95,23 +93,27 @@ html {
 | 
			
		||||
body {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  font-family: 'Roboto', sans-serif;
 | 
			
		||||
  font-family: "Roboto", sans-serif;
 | 
			
		||||
  line-height: 1.6;
 | 
			
		||||
  background: $background-color;
 | 
			
		||||
  color: $text-color;
 | 
			
		||||
  transition: background-color .5s ease, color .5s ease;
 | 
			
		||||
  transition: background-color 0.5s ease, color 0.5s ease;
 | 
			
		||||
  &.hidden {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
h1,h2,h3 {
 | 
			
		||||
  transition: color .5s ease;
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3 {
 | 
			
		||||
  transition: color 0.5s ease;
 | 
			
		||||
}
 | 
			
		||||
a:any-link {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
input, textarea, button{
 | 
			
		||||
  font-family: 'Roboto', sans-serif;
 | 
			
		||||
input,
 | 
			
		||||
textarea,
 | 
			
		||||
button {
 | 
			
		||||
  font-family: "Roboto", sans-serif;
 | 
			
		||||
}
 | 
			
		||||
figure {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
@@ -123,6 +125,10 @@ img{
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-scroll {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wrapper {
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
@@ -142,14 +148,16 @@ img{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// router view transition
 | 
			
		||||
.fade-enter-active, .fade-leave-active {
 | 
			
		||||
.fade-enter-active,
 | 
			
		||||
.fade-leave-active {
 | 
			
		||||
  transition-property: opacity;
 | 
			
		||||
  transition-duration: 0.25s;
 | 
			
		||||
}
 | 
			
		||||
.fade-enter-active {
 | 
			
		||||
  transition-delay: 0.25s;
 | 
			
		||||
}
 | 
			
		||||
.fade-enter, .fade-leave-active {
 | 
			
		||||
  opacity: 0
 | 
			
		||||
.fade-enter,
 | 
			
		||||
.fade-leave-active {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										235
									
								
								src/api.js
									
									
									
									
									
								
							
							
						
						
									
										235
									
								
								src/api.js
									
									
									
									
									
								
							@@ -2,6 +2,7 @@ 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
 | 
			
		||||
@@ -10,6 +11,13 @@ 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 - - - 
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -18,12 +26,18 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX
 | 
			
		||||
 * @param {boolean} [credits=false] Include credits
 | 
			
		||||
 * @returns {object} Tmdb response
 | 
			
		||||
 */
 | 
			
		||||
const getMovie = (id, credits=false) => {
 | 
			
		||||
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())
 | 
			
		||||
@@ -36,9 +50,12 @@ const getMovie = (id, credits=false) => {
 | 
			
		||||
 * @param {boolean} [credits=false] Include credits
 | 
			
		||||
 * @returns {object} Tmdb response
 | 
			
		||||
 */
 | 
			
		||||
const getShow = (id, credits=false) => {
 | 
			
		||||
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)
 | 
			
		||||
  }
 | 
			
		||||
@@ -48,6 +65,24 @@ const getShow = (id, credits=false) => {
 | 
			
		||||
    .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
 | 
			
		||||
@@ -96,12 +131,19 @@ const getUserRequests = (page=1) => {
 | 
			
		||||
 * @param {number} [page=1]
 | 
			
		||||
 * @returns {object} Tmdb response
 | 
			
		||||
 */
 | 
			
		||||
const searchTmdb = (query, page=1) => {
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
  return fetch(url.href)
 | 
			
		||||
  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 })
 | 
			
		||||
}
 | 
			
		||||
@@ -135,14 +177,21 @@ const searchTorrents = (query, authorization_token) => {
 | 
			
		||||
const addMagnet = (magnet, name, tmdb_id) => {
 | 
			
		||||
  const url = new URL('v1/pirate/add', SEASONED_URL)
 | 
			
		||||
 | 
			
		||||
  const body = {
 | 
			
		||||
  const body = JSON.stringify({
 | 
			
		||||
    magnet: magnet,
 | 
			
		||||
    name: name,
 | 
			
		||||
    tmdb_id: tmdb_id
 | 
			
		||||
  })
 | 
			
		||||
  const headers = {
 | 
			
		||||
    'Content-Type': 'application/json',
 | 
			
		||||
    authorization: storage.token
 | 
			
		||||
  }
 | 
			
		||||
  const headers = { authorization: storage.token }
 | 
			
		||||
 | 
			
		||||
  return fetch(url.href, { method: 'POST', headers, body })
 | 
			
		||||
  return fetch(url.href, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers,
 | 
			
		||||
      body
 | 
			
		||||
    })
 | 
			
		||||
    .then(resp => resp.json())
 | 
			
		||||
    .catch(error => { console.error(`api error adding magnet: ${name} ${error}`); throw error })
 | 
			
		||||
}
 | 
			
		||||
@@ -203,32 +252,156 @@ const getRequestStatus = (id, type, authorization_token=undefined) => {
 | 
			
		||||
    .catch(err => Promise.reject(err))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - Authenticate with plex - - -
 | 
			
		||||
 | 
			
		||||
const plexAuthenticate = (username, password) => {
 | 
			
		||||
  const url = new URL('https://plex.tv/api/v2/users/signin')
 | 
			
		||||
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 = {
 | 
			
		||||
    'Content-Type': 'application/json',
 | 
			
		||||
    'X-Plex-Platform': 'Linux',
 | 
			
		||||
    'X-Plex-Version': 'v2.0.24',
 | 
			
		||||
    'X-Plex-Platform-Version': '4.13.0-36-generic',
 | 
			
		||||
    'X-Plex-Device-Name': 'Tautulli',
 | 
			
		||||
    'X-Plex-Client-Identifier': '123'
 | 
			
		||||
    'Authorization': authorization_token,
 | 
			
		||||
    'Content-Type': 'application/json'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let formData = new FormData()
 | 
			
		||||
  formData.set('login', username)
 | 
			
		||||
  formData.set('password', password)
 | 
			
		||||
  formData.set('rememberMe', false)
 | 
			
		||||
  return fetch(url.href, { headers })
 | 
			
		||||
    .then(resp => resp.json())
 | 
			
		||||
    .then(response => response.link)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  return axios({
 | 
			
		||||
// - - - Seasoned user endpoints - - -
 | 
			
		||||
 | 
			
		||||
const register = (username, password) => {
 | 
			
		||||
  const url = new URL('v1/user', SEASONED_URL)
 | 
			
		||||
  const options = {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
      url: url.href,
 | 
			
		||||
      headers: headers,
 | 
			
		||||
      data: formData
 | 
			
		||||
    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
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => { console.error(`api error authentication plex: ${username}`); 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 })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -295,6 +468,7 @@ const elasticSearchMoviesAndShows = (query) => {
 | 
			
		||||
export {
 | 
			
		||||
  getMovie,
 | 
			
		||||
  getShow,
 | 
			
		||||
  getPerson,
 | 
			
		||||
  getTmdbMovieListByName,
 | 
			
		||||
  searchTmdb,
 | 
			
		||||
  getUserRequests,
 | 
			
		||||
@@ -302,8 +476,15 @@ export {
 | 
			
		||||
  searchTorrents,
 | 
			
		||||
  addMagnet,
 | 
			
		||||
  request,
 | 
			
		||||
  watchLink,
 | 
			
		||||
  getRequestStatus,
 | 
			
		||||
  plexAuthenticate,
 | 
			
		||||
  linkPlexAccount,
 | 
			
		||||
  unlinkPlexAccount,
 | 
			
		||||
  register,
 | 
			
		||||
  login,
 | 
			
		||||
  getSettings,
 | 
			
		||||
  updateSettings,
 | 
			
		||||
  fetchChart,
 | 
			
		||||
  getEmoji,
 | 
			
		||||
  elasticSearchMoviesAndShows
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,74 @@
 | 
			
		||||
<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));
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
 | 
			
		||||
  &:before {
 | 
			
		||||
  &::before {
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    height: calc(100vh - var(--header-size));
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    background: $background-40;
 | 
			
		||||
  }
 | 
			
		||||
  &__title {
 | 
			
		||||
   padding-top: 40vh;
 | 
			
		||||
    font-size: 2rem;
 | 
			
		||||
    margin-top: 30vh;
 | 
			
		||||
    font-size: 2.5rem;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
 | 
			
		||||
    @include tablet-min {
 | 
			
		||||
      font-size: 2.3rem;
 | 
			
		||||
      font-size: 3.5rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										316
									
								
								src/components/ActivityPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								src/components/ActivityPage.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,316 @@
 | 
			
		||||
<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>
 | 
			
		||||
@@ -12,74 +12,76 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import LandingBanner from '@/components/LandingBanner'
 | 
			
		||||
import ListHeader from '@/components/ListHeader'
 | 
			
		||||
import ResultsList from '@/components/ResultsList'
 | 
			
		||||
import Loader from '@/components/ui/Loader'
 | 
			
		||||
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'
 | 
			
		||||
import { getTmdbMovieListByName, getRequests } from "@/api";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'home',
 | 
			
		||||
  name: "home",
 | 
			
		||||
  components: { LandingBanner, ResultsList, ListHeader, Loader },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      imageFile: 'dist/pulp-fiction.jpg',
 | 
			
		||||
      imageFile: "/pulp-fiction.jpg",
 | 
			
		||||
      requests: [],
 | 
			
		||||
      nowplaying: [],
 | 
			
		||||
      upcoming: [],
 | 
			
		||||
      popular: []
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    lists() {
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          title: 'Requests',
 | 
			
		||||
          route: 'request',
 | 
			
		||||
          title: "Requests",
 | 
			
		||||
          route: "request",
 | 
			
		||||
          data: this.requests
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          title: 'Now playing',
 | 
			
		||||
          route: 'now_playing',
 | 
			
		||||
          title: "Now playing",
 | 
			
		||||
          route: "now_playing",
 | 
			
		||||
          data: this.nowplaying
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          title: 'Upcoming',
 | 
			
		||||
          route: 'upcoming',
 | 
			
		||||
          title: "Upcoming",
 | 
			
		||||
          route: "upcoming",
 | 
			
		||||
          data: this.upcoming
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          title: 'Popular',
 | 
			
		||||
          route: 'popular',
 | 
			
		||||
          title: "Popular",
 | 
			
		||||
          route: "popular",
 | 
			
		||||
          data: this.popular
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    fetchRequests() {
 | 
			
		||||
      getRequests()
 | 
			
		||||
        .then(results => this.requests = results.results)
 | 
			
		||||
      getRequests().then(results => (this.requests = results.results));
 | 
			
		||||
    },
 | 
			
		||||
    fetchNowPlaying() {
 | 
			
		||||
      getTmdbMovieListByName('now_playing')
 | 
			
		||||
        .then(results => this.nowplaying = results.results)
 | 
			
		||||
      getTmdbMovieListByName("now_playing").then(
 | 
			
		||||
        results => (this.nowplaying = results.results)
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    fetchUpcoming() {
 | 
			
		||||
      getTmdbMovieListByName('upcoming')
 | 
			
		||||
        .then(results => this.upcoming = results.results)
 | 
			
		||||
      getTmdbMovieListByName("upcoming").then(
 | 
			
		||||
        results => (this.upcoming = results.results)
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    fetchPopular() {
 | 
			
		||||
      getTmdbMovieListByName('popular')
 | 
			
		||||
        .then(results => this.popular = results.results)
 | 
			
		||||
      getTmdbMovieListByName("popular").then(
 | 
			
		||||
        results => (this.popular = results.results)
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.fetchRequests()
 | 
			
		||||
    this.fetchNowPlaying()
 | 
			
		||||
    this.fetchUpcoming()
 | 
			
		||||
    this.fetchPopular()
 | 
			
		||||
  }
 | 
			
		||||
    this.fetchRequests();
 | 
			
		||||
    this.fetchNowPlaying();
 | 
			
		||||
    this.fetchUpcoming();
 | 
			
		||||
    this.fetchPopular();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <header v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }">
 | 
			
		||||
    <div class="container">
 | 
			
		||||
      <h1 class="title">Request new movies or tv shows for plex</h1>
 | 
			
		||||
      <strong class="subtitle">Made with Vue.js</strong>
 | 
			
		||||
      <h1 class="title">Request movies or tv shows</h1>
 | 
			
		||||
      <strong class="subtitle"
 | 
			
		||||
        >Create a profile to track and view requests</strong
 | 
			
		||||
      >
 | 
			
		||||
    </div>
 | 
			
		||||
  </header>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -17,15 +19,15 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      imageFile: 'dist/pulp-fiction.jpg'
 | 
			
		||||
    }
 | 
			
		||||
      imageFile: "/pulp-fiction.jpg"
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  beforeMount() {
 | 
			
		||||
    if (this.image && this.image.length > 0) {
 | 
			
		||||
      this.imageFile = this.image
 | 
			
		||||
    }
 | 
			
		||||
      this.imageFile = this.image;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@@ -55,13 +57,13 @@ header {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background-color: $background-70;
 | 
			
		||||
    transition: background-color .5s ease;
 | 
			
		||||
    transition: background-color 0.5s ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .container {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    transition: color .5s ease;
 | 
			
		||||
    transition: color 0.5s ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .title {
 | 
			
		||||
@@ -73,7 +75,7 @@ header {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
 | 
			
		||||
    @include tablet-min {
 | 
			
		||||
      font-size: 28px;
 | 
			
		||||
      font-size: 2.5rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -85,7 +87,7 @@ header {
 | 
			
		||||
    margin: 5px 0;
 | 
			
		||||
 | 
			
		||||
    @include tablet-min {
 | 
			
		||||
      font-size: 16px;
 | 
			
		||||
      font-size: 1.3rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,17 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <header :class="{ 'sticky': sticky }">
 | 
			
		||||
  <header :class="{ sticky: sticky }">
 | 
			
		||||
    <h2>{{ title }}</h2>
 | 
			
		||||
 | 
			
		||||
    <span v-if="info" class="result-count">{{ info }}</span>
 | 
			
		||||
    <router-link v-else-if="link" :to="link" class='view-more'>
 | 
			
		||||
    <div v-if="info instanceof Array" class="flex flex-direction-column">
 | 
			
		||||
      <span v-for="item in info" class="info">{{ item }}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <span v-else class="info">{{ info }}</span>
 | 
			
		||||
    <router-link
 | 
			
		||||
      v-if="link"
 | 
			
		||||
      :to="link"
 | 
			
		||||
      class="view-more"
 | 
			
		||||
      :aria-label="`View all ${title}`"
 | 
			
		||||
    >
 | 
			
		||||
      View All
 | 
			
		||||
    </router-link>
 | 
			
		||||
  </header>
 | 
			
		||||
@@ -19,10 +27,10 @@ export default {
 | 
			
		||||
    sticky: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: false
 | 
			
		||||
      default: true
 | 
			
		||||
    },
 | 
			
		||||
    info: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      type: [String, Array],
 | 
			
		||||
      required: false
 | 
			
		||||
    },
 | 
			
		||||
    link: {
 | 
			
		||||
@@ -30,48 +38,56 @@ export default {
 | 
			
		||||
      required: false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import './src/scss/variables';
 | 
			
		||||
@import './src/scss/media-queries';
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
@import "./src/scss/main";
 | 
			
		||||
 | 
			
		||||
header {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  min-height: 45px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  padding: 1.8rem 12px;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding-left: 0.75rem;
 | 
			
		||||
  padding-right: 0.75rem;
 | 
			
		||||
 | 
			
		||||
  @include tablet-min {
 | 
			
		||||
    min-height: 65px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.sticky {
 | 
			
		||||
    background-color: $background-color;
 | 
			
		||||
 | 
			
		||||
    position: sticky;
 | 
			
		||||
    position: -webkit-sticky;
 | 
			
		||||
    top: $header-size;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    z-index: 4;
 | 
			
		||||
 | 
			
		||||
    padding-bottom: 1rem;
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
    @include tablet-min {
 | 
			
		||||
      top: $header-size;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  h2 {
 | 
			
		||||
    font-size: 18px;
 | 
			
		||||
    font-size: 1.4rem;
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
    text-transform: capitalize;
 | 
			
		||||
    line-height: 18px;
 | 
			
		||||
    line-height: 1.4rem;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .view-more {
 | 
			
		||||
    font-size: 13px;
 | 
			
		||||
    font-size: 0.9rem;
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
    letter-spacing: .5px;
 | 
			
		||||
    letter-spacing: 0.5px;
 | 
			
		||||
    color: $text-color-70;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    transition: color .5s ease;
 | 
			
		||||
    transition: color 0.5s ease;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
 | 
			
		||||
    &:after {
 | 
			
		||||
@@ -82,20 +98,20 @@ header {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .result-count {
 | 
			
		||||
  .info {
 | 
			
		||||
    font-size: 13px;
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
    letter-spacing: .5px;
 | 
			
		||||
    letter-spacing: 0.5px;
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @include tablet-min {
 | 
			
		||||
    padding-left: 1.25rem;;
 | 
			
		||||
    padding-left: 1.25rem;
 | 
			
		||||
  }
 | 
			
		||||
  @include desktop-lg-min {
 | 
			
		||||
    padding-left: 1.75rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <list-header :title="listTitle" :info="resultCount" :sticky="true" />
 | 
			
		||||
  <div class="page-container">
 | 
			
		||||
    <list-header :title="listTitle" :info="info" :sticky="true" />
 | 
			
		||||
 | 
			
		||||
    <results-list :results="results" v-if="results" />
 | 
			
		||||
 | 
			
		||||
@@ -13,87 +12,107 @@
 | 
			
		||||
  </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'
 | 
			
		||||
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' ],
 | 
			
		||||
      legalTmdbLists: ["now_playing", "upcoming", "popular"],
 | 
			
		||||
      results: [],
 | 
			
		||||
      page: 1,
 | 
			
		||||
      totalPages: 0,
 | 
			
		||||
      totalResults: 0
 | 
			
		||||
    }
 | 
			
		||||
      totalResults: 0,
 | 
			
		||||
      loading: true
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    listTitle() {
 | 
			
		||||
      if (this.results.length === 0)
 | 
			
		||||
        return ''
 | 
			
		||||
      if (this.results.length === 0) return "";
 | 
			
		||||
 | 
			
		||||
      const routeListName = this.$route.params.name
 | 
			
		||||
      console.log('routelistname', routeListName)
 | 
			
		||||
      return routeListName.includes('_') ? routeListName.split('_').join(' ') : routeListName
 | 
			
		||||
      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() {
 | 
			
		||||
      if (this.results.length === 0)
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
      const loadedResults = this.results.length
 | 
			
		||||
      const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
 | 
			
		||||
      return `${loadedResults} of ${totalResults} results`
 | 
			
		||||
      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.page++
 | 
			
		||||
      console.log(this.$route);
 | 
			
		||||
      this.loading = true;
 | 
			
		||||
      this.page++;
 | 
			
		||||
 | 
			
		||||
      window.history.replaceState({}, 'search', `/#/${this.$route.fullPath}?page=${this.page}`)
 | 
			
		||||
      this.init()
 | 
			
		||||
      window.history.replaceState(
 | 
			
		||||
        {},
 | 
			
		||||
        "search",
 | 
			
		||||
        `/#/${this.$route.fullPath}?page=${this.page}`
 | 
			
		||||
      );
 | 
			
		||||
      this.init();
 | 
			
		||||
    },
 | 
			
		||||
    init() {
 | 
			
		||||
      const routeListName = this.$route.params.name
 | 
			
		||||
      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
 | 
			
		||||
          })
 | 
			
		||||
      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
 | 
			
		||||
          })
 | 
			
		||||
        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')
 | 
			
		||||
        console.log("404 this is not a tmdb list");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    if (this.results.length === 0)
 | 
			
		||||
      this.init()
 | 
			
		||||
    if (this.results.length === 0) this.init();
 | 
			
		||||
 | 
			
		||||
    store.dispatch('documentTitle/updateTitle', `${this.$router.history.current.name} ${this.$route.params.name}`)
 | 
			
		||||
  }
 | 
			
		||||
    store.dispatch(
 | 
			
		||||
      "documentTitle/updateTitle",
 | 
			
		||||
      `${this.$router.history.current.name} ${this.$route.params.name}`
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
 | 
			
		||||
@include mobile-only {
 | 
			
		||||
  .page-container {
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fullwidth-button {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 1rem 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +1,60 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <section class="movie">
 | 
			
		||||
 | 
			
		||||
    <!-- HEADER w/ POSTER -->
 | 
			
		||||
    <header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }" :class="compact ? 'compact' : ''" @click="compact=!compact">
 | 
			
		||||
      <div class="movie__wrap movie__wrap--header">
 | 
			
		||||
    <header
 | 
			
		||||
      ref="header"
 | 
			
		||||
      :class="compact ? 'compact' : ''"
 | 
			
		||||
      @click="compact = !compact"
 | 
			
		||||
    >
 | 
			
		||||
      <figure class="movie__poster">
 | 
			
		||||
          <img v-if="movie && poster === null"
 | 
			
		||||
            class="movies-item__img is-loaded"
 | 
			
		||||
            alt="movie poster image"
 | 
			
		||||
            src="~assets/no-image.png">
 | 
			
		||||
          <img v-else-if="poster === undefined"
 | 
			
		||||
            class="movies-item__img grey"
 | 
			
		||||
            alt="movie poster image">
 | 
			
		||||
            <!-- src="~assets/placeholder.png"> -->
 | 
			
		||||
          <img v-else
 | 
			
		||||
            class="movies-item__img is-loaded"
 | 
			
		||||
            alt="movie poster image"
 | 
			
		||||
            :src="ASSET_URL + ASSET_SIZES[0] + poster">
 | 
			
		||||
        <img
 | 
			
		||||
          class="movie-item__img is-loaded"
 | 
			
		||||
          ref="poster-image"
 | 
			
		||||
          src="~assets/placeholder.png"
 | 
			
		||||
        />
 | 
			
		||||
      </figure>
 | 
			
		||||
 | 
			
		||||
        <div class="movie__title">
 | 
			
		||||
          <h1 v-if="movie">{{ movie.title }}</h1>
 | 
			
		||||
      <h1 class="movie__title" v-if="movie">{{ movie.title }}</h1>
 | 
			
		||||
      <loading-placeholder v-else :count="1" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </header>
 | 
			
		||||
 | 
			
		||||
    <!-- Siderbar and movie info -->
 | 
			
		||||
    <div class="movie__main">
 | 
			
		||||
      <div class="movie__wrap movie__wrap--main">
 | 
			
		||||
 | 
			
		||||
        <!-- SIDEBAR ACTIONS -->
 | 
			
		||||
        <div class="movie__actions" v-if="movie">
 | 
			
		||||
 | 
			
		||||
          <sidebar-list-element :iconRef="'#iconNot_exsits'" :active="matched"
 | 
			
		||||
            :iconRefActive="'#iconExists'" :textActive="'Already in plex 🎉'">
 | 
			
		||||
 | 
			
		||||
          <sidebar-list-element
 | 
			
		||||
            :iconRef="'#iconNot_exsits'"
 | 
			
		||||
            :active="matched"
 | 
			
		||||
            :iconRefActive="'#iconExists'"
 | 
			
		||||
            :textActive="'Already in plex 🎉'"
 | 
			
		||||
          >
 | 
			
		||||
            Not yet in plex
 | 
			
		||||
          </sidebar-list-element>
 | 
			
		||||
          <sidebar-list-element @click="sendRequest" :iconRef="'#iconSent'"
 | 
			
		||||
            :active="requested" :textActive="'Requested to be downloaded'">
 | 
			
		||||
 | 
			
		||||
          <sidebar-list-element
 | 
			
		||||
            @click="sendRequest"
 | 
			
		||||
            :iconRef="'#iconSent'"
 | 
			
		||||
            :active="requested"
 | 
			
		||||
            :textActive="'Requested to be downloaded'"
 | 
			
		||||
          >
 | 
			
		||||
            Request to be downloaded?
 | 
			
		||||
          </sidebar-list-element>
 | 
			
		||||
          <sidebar-list-element v-if="admin" @click="showTorrents=!showTorrents"
 | 
			
		||||
            :iconRef="'#icon_torrents'" :active="showTorrents"
 | 
			
		||||
            :supplementaryText="numberOfTorrentResults">
 | 
			
		||||
 | 
			
		||||
          <sidebar-list-element
 | 
			
		||||
            v-if="isPlexAuthenticated && matched"
 | 
			
		||||
            @click="openInPlex"
 | 
			
		||||
            :iconString="'⏯ '"
 | 
			
		||||
          >
 | 
			
		||||
            Watch in plex now!
 | 
			
		||||
          </sidebar-list-element>
 | 
			
		||||
 | 
			
		||||
          <sidebar-list-element
 | 
			
		||||
            v-if="admin"
 | 
			
		||||
            @click="showTorrents = !showTorrents"
 | 
			
		||||
            :iconRef="'#icon_torrents'"
 | 
			
		||||
            :active="showTorrents"
 | 
			
		||||
            :supplementaryText="numberOfTorrentResults"
 | 
			
		||||
          >
 | 
			
		||||
            Search for torrents
 | 
			
		||||
          </sidebar-list-element>
 | 
			
		||||
          <sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'">
 | 
			
		||||
@@ -56,83 +64,129 @@
 | 
			
		||||
 | 
			
		||||
        <!-- Loading placeholder -->
 | 
			
		||||
        <div class="movie__actions text-input__loading" v-else>
 | 
			
		||||
          <div class="movie__actions-link" v-for="_ in admin ? Array(4) : Array(3)">
 | 
			
		||||
            <div class="movie__actions-text text-input__loading--line" style="margin:9px; margin-left: -3px;"></div>
 | 
			
		||||
          <div
 | 
			
		||||
            class="movie__actions-link"
 | 
			
		||||
            v-for="_ in admin ? Array(4) : Array(3)"
 | 
			
		||||
          >
 | 
			
		||||
            <div
 | 
			
		||||
              class="movie__actions-text text-input__loading--line"
 | 
			
		||||
              style="margin: 9px; margin-left: -3px"
 | 
			
		||||
            ></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <!-- MOVIE INFO -->
 | 
			
		||||
        <div class="movie__info">
 | 
			
		||||
          <div class="movie__description" v-if="movie"> {{ movie.overview }}</div>
 | 
			
		||||
 | 
			
		||||
          <!-- Loading placeholder -->
 | 
			
		||||
          <div
 | 
			
		||||
            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="12" />
 | 
			
		||||
            <loading-placeholder :count="5" />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="movie__details" v-if="movie">
 | 
			
		||||
            <div v-if="movie.year" class="movie__details-block">
 | 
			
		||||
              <h2 class="movie__details-title">Release Date</h2>
 | 
			
		||||
              <div class="movie__details-text">{{ movie.year }}</div>
 | 
			
		||||
            <div v-if="movie.year">
 | 
			
		||||
              <h2 class="title">Release Date</h2>
 | 
			
		||||
              <div class="text">{{ movie.year }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
             <div v-if="movie.rank" class="movie__details-block">
 | 
			
		||||
              <h2 class="movie__details-title">Rating</h2>
 | 
			
		||||
              <div class="movie__details-text">{{ movie.rank }}</div>
 | 
			
		||||
            <div v-if="movie.rating">
 | 
			
		||||
              <h2 class="title">Rating</h2>
 | 
			
		||||
              <div class="text">{{ movie.rating }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div v-if="movie.type == 'show'" class="movie__details-block">
 | 
			
		||||
              <h2 class="movie__details-title">Seasons</h2>
 | 
			
		||||
              <div class="movie__details-text">{{ movie.seasons }}</div>
 | 
			
		||||
            <div v-if="movie.type == 'show'">
 | 
			
		||||
              <h2 class="title">Seasons</h2>
 | 
			
		||||
              <div class="text">{{ movie.seasons }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div v-if="movie.genres" class="movie__details-block">
 | 
			
		||||
              <h2 class="movie__details-title">Genres</h2>
 | 
			
		||||
              <div class="movie__details-text">{{ nestedDataToString(movie.genres) }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div v-if="movie.genres">
 | 
			
		||||
              <h2 class="title">Genres</h2>
 | 
			
		||||
              <div class="text">{{ movie.genres.join(", ") }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div v-if="movie.type == 'show'">
 | 
			
		||||
              <h2 class="title">Production status</h2>
 | 
			
		||||
              <div class="text">{{ movie.production_status }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div v-if="movie.type == 'show'">
 | 
			
		||||
              <h2 class="title">Runtime</h2>
 | 
			
		||||
              <div class="text">{{ movie.runtime[0] }} minutes</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- TODO: change this classname, this is general  -->
 | 
			
		||||
 | 
			
		||||
        <div class="movie__admin" v-if="movie && movie.credits">
 | 
			
		||||
          <h2 class="movie__details-title">Cast</h2>
 | 
			
		||||
          <div style="display: flex; flex-wrap: wrap;">
 | 
			
		||||
            <person v-for="cast in movie.credits.cast" :info="cast"
 | 
			
		||||
              style="flex-basis: 0;"></person>
 | 
			
		||||
          <div style="display: flex; flex-wrap: wrap">
 | 
			
		||||
            <person
 | 
			
		||||
              v-for="cast in movie.credits.cast"
 | 
			
		||||
              :info="cast"
 | 
			
		||||
              style="flex-basis: 0"
 | 
			
		||||
            ></person>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- TORRENT LIST -->
 | 
			
		||||
      <TorrentList v-if="movie" :show="showTorrents" :query="title" :tmdb_id="id"
 | 
			
		||||
                   :admin="admin"></TorrentList>
 | 
			
		||||
      <TorrentList
 | 
			
		||||
        v-if="movie"
 | 
			
		||||
        :show="showTorrents"
 | 
			
		||||
        :query="title"
 | 
			
		||||
        :tmdb_id="id"
 | 
			
		||||
        :admin="admin"
 | 
			
		||||
      ></TorrentList>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import storage from '@/storage'
 | 
			
		||||
import img from '@/directives/v-image'
 | 
			
		||||
import TorrentList from './TorrentList'
 | 
			
		||||
import Person from './Person'
 | 
			
		||||
import SidebarListElement from './ui/sidebarListElem'
 | 
			
		||||
import store from '@/store'
 | 
			
		||||
import LoadingPlaceholder from './ui/LoadingPlaceholder'
 | 
			
		||||
import storage from "@/storage";
 | 
			
		||||
import img from "@/directives/v-image";
 | 
			
		||||
import TorrentList from "./TorrentList";
 | 
			
		||||
import Person from "./Person";
 | 
			
		||||
import SidebarListElement from "./ui/sidebarListElem";
 | 
			
		||||
import store from "@/store";
 | 
			
		||||
import LoadingPlaceholder from "./ui/LoadingPlaceholder";
 | 
			
		||||
 | 
			
		||||
import { getMovie, getShow, request, getRequestStatus } from '@/api'
 | 
			
		||||
import {
 | 
			
		||||
  getMovie,
 | 
			
		||||
  getPerson,
 | 
			
		||||
  getShow,
 | 
			
		||||
  request,
 | 
			
		||||
  getRequestStatus,
 | 
			
		||||
  watchLink
 | 
			
		||||
} from "@/api";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: ['id', 'type'],
 | 
			
		||||
  // props: ['id', 'type'],
 | 
			
		||||
  props: {
 | 
			
		||||
    id: {
 | 
			
		||||
      required: true,
 | 
			
		||||
      type: Number
 | 
			
		||||
    },
 | 
			
		||||
    type: {
 | 
			
		||||
      required: false,
 | 
			
		||||
      type: String
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
 | 
			
		||||
  directives: { img: img }, // TODO decide to remove or use
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      ASSET_URL: 'https://image.tmdb.org/t/p/',
 | 
			
		||||
      ASSET_SIZES: ['w500', 'w780', 'original'],
 | 
			
		||||
      ASSET_URL: "https://image.tmdb.org/t/p/",
 | 
			
		||||
      ASSET_SIZES: ["w500", "w780", "original"],
 | 
			
		||||
      movie: undefined,
 | 
			
		||||
      title: undefined,
 | 
			
		||||
      poster: undefined,
 | 
			
		||||
@@ -140,88 +194,201 @@ export default {
 | 
			
		||||
      matched: false,
 | 
			
		||||
      userLoggedIn: storage.sessionId ? true : false,
 | 
			
		||||
      requested: false,
 | 
			
		||||
      admin: localStorage.getItem('admin'),
 | 
			
		||||
      admin: localStorage.getItem("admin") == "true" ? true : false,
 | 
			
		||||
      showTorrents: false,
 | 
			
		||||
      compact: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    parseResponse(movie) {
 | 
			
		||||
      this.movie = { ...movie }
 | 
			
		||||
      this.title = movie.title
 | 
			
		||||
      this.poster = movie.poster
 | 
			
		||||
      this.backdrop = movie.backdrop
 | 
			
		||||
      this.matched = movie.existsInPlex
 | 
			
		||||
      this.checkIfRequested(movie)
 | 
			
		||||
        .then(status => this.requested = status)
 | 
			
		||||
 | 
			
		||||
      store.dispatch('documentTitle/updateTitle', movie.title)
 | 
			
		||||
    },
 | 
			
		||||
    async checkIfRequested(movie) {
 | 
			
		||||
      return await getRequestStatus(movie.id, movie.type)
 | 
			
		||||
    },
 | 
			
		||||
    nestedDataToString(data) {
 | 
			
		||||
      let nestedArray = []
 | 
			
		||||
      data.forEach(item => nestedArray.push(item));
 | 
			
		||||
      return nestedArray.join(', ');
 | 
			
		||||
    },
 | 
			
		||||
    sendRequest(){
 | 
			
		||||
      request(this.id, this.type, storage.token)
 | 
			
		||||
        .then(resp => {
 | 
			
		||||
          if (resp.success) {
 | 
			
		||||
            this.requested = true
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    openTmdb(){
 | 
			
		||||
      const tmdbType = this.type === 'show' ? 'tv' : this.type
 | 
			
		||||
      window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id
 | 
			
		||||
    },
 | 
			
		||||
      compact: false,
 | 
			
		||||
      loading: true,
 | 
			
		||||
      truncatedDescription: true
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    id: function (val) {
 | 
			
		||||
      if (this.type === 'movie') {
 | 
			
		||||
      if (this.type === "movie") {
 | 
			
		||||
        this.fetchMovie(val);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.fetchShow(val)
 | 
			
		||||
        this.fetchShow(val);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    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: {
 | 
			
		||||
    numberOfTorrentResults: () => {
 | 
			
		||||
      let numTorrents = store.getters['torrentModule/resultCount']
 | 
			
		||||
      return numTorrents !== null ? numTorrents + ' results' : null
 | 
			
		||||
      let numTorrents = store.getters["torrentModule/resultCount"];
 | 
			
		||||
      return numTorrents !== null ? numTorrents + " results" : null;
 | 
			
		||||
    },
 | 
			
		||||
    isPlexAuthenticated: () => {
 | 
			
		||||
      return store.getters["userModule/isPlexAuthenticated"];
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    parseResponse(movie) {
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
      this.movie = { ...movie };
 | 
			
		||||
      this.title = movie.title;
 | 
			
		||||
      this.poster = movie.poster;
 | 
			
		||||
      this.backdrop = movie.backdrop;
 | 
			
		||||
      this.matched = movie.exists_in_plex || false;
 | 
			
		||||
      this.checkIfRequested(movie).then(status => (this.requested = status));
 | 
			
		||||
 | 
			
		||||
      store.dispatch("documentTitle/updateTitle", movie.title);
 | 
			
		||||
      this.setPosterSrc();
 | 
			
		||||
    },
 | 
			
		||||
    async checkIfRequested(movie) {
 | 
			
		||||
      return await getRequestStatus(movie.id, movie.type);
 | 
			
		||||
    },
 | 
			
		||||
    setPosterSrc() {
 | 
			
		||||
      const poster = this.$refs["poster-image"];
 | 
			
		||||
      if (this.poster == null) {
 | 
			
		||||
        poster.src = "/no-image.png";
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`;
 | 
			
		||||
    },
 | 
			
		||||
    sendRequest() {
 | 
			
		||||
      request(this.id, this.type, storage.token).then(resp => {
 | 
			
		||||
        if (resp.success) {
 | 
			
		||||
          this.requested = true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    openInPlex() {
 | 
			
		||||
      watchLink(this.title, this.movie.year, storage.token).then(
 | 
			
		||||
        watchLink => (window.location = watchLink)
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    openTmdb() {
 | 
			
		||||
      const tmdbType = this.type === "show" ? "tv" : this.type;
 | 
			
		||||
      window.location.href =
 | 
			
		||||
        "https://www.themoviedb.org/" + tmdbType + "/" + this.id;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.prevDocumentTitle = store.getters["documentTitle/title"];
 | 
			
		||||
 | 
			
		||||
    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() {
 | 
			
		||||
    store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle)
 | 
			
		||||
  },
 | 
			
		||||
  created(){
 | 
			
		||||
    this.prevDocumentTitle = store.getters['documentTitle/title']
 | 
			
		||||
 | 
			
		||||
    if (this.type === 'movie') {
 | 
			
		||||
      getMovie(this.id)
 | 
			
		||||
        .then(this.parseResponse)
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
          this.$router.push({ name: '404' });
 | 
			
		||||
        })
 | 
			
		||||
    } else {
 | 
			
		||||
      getShow(this.id)
 | 
			
		||||
        .then(this.parseResponse)
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
          this.$router.push({ name: '404' });
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log('admin: ', this.admin)
 | 
			
		||||
  }
 | 
			
		||||
    store.dispatch("documentTitle/updateTitle", this.prevDocumentTitle);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/loading-placeholder";
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
@import "./src/scss/main";
 | 
			
		||||
 | 
			
		||||
header {
 | 
			
		||||
  $duration: 0.2s;
 | 
			
		||||
  height: 250px;
 | 
			
		||||
  transform: scaleY(1);
 | 
			
		||||
  transition: height $duration ease;
 | 
			
		||||
  transform-origin: top;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  background-repeat: no-repeat;
 | 
			
		||||
  background-position: 50% 50%;
 | 
			
		||||
  background-color: $background-color;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  @include tablet-min {
 | 
			
		||||
    height: 350px;
 | 
			
		||||
  }
 | 
			
		||||
  &:before {
 | 
			
		||||
    content: "";
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    z-index: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: $background-dark-85;
 | 
			
		||||
  }
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    &.compact {
 | 
			
		||||
      height: 100px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.movie__poster {
 | 
			
		||||
  display: none;
 | 
			
		||||
 | 
			
		||||
  @include desktop {
 | 
			
		||||
    background: $background-color;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    width: calc(45% - 40px);
 | 
			
		||||
    top: 40px;
 | 
			
		||||
    left: 40px;
 | 
			
		||||
 | 
			
		||||
    > img {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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 {
 | 
			
		||||
  &__wrap {
 | 
			
		||||
@@ -242,49 +409,6 @@ export default {
 | 
			
		||||
      color: $text-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__header {
 | 
			
		||||
    $duration: 0.2s;
 | 
			
		||||
    height: 250px;
 | 
			
		||||
    transform: scaleY(1);
 | 
			
		||||
    transition: height $duration ease;
 | 
			
		||||
    transform-origin: top;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
    background-position: 50% 50%;
 | 
			
		||||
    background-color: $background-color;
 | 
			
		||||
    @include tablet-min {
 | 
			
		||||
      height: 350px;
 | 
			
		||||
    }
 | 
			
		||||
    &:before {
 | 
			
		||||
      content: "";
 | 
			
		||||
      display: block;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      z-index: 0;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      background: $background-dark-85;
 | 
			
		||||
    }
 | 
			
		||||
    &.compact {
 | 
			
		||||
      height: 100px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__poster {
 | 
			
		||||
    display: none;
 | 
			
		||||
    @include tablet-min {
 | 
			
		||||
      background: $background-color;
 | 
			
		||||
      height: 0;
 | 
			
		||||
      display: block;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      width: calc(45% - 40px);
 | 
			
		||||
      top: 40px;
 | 
			
		||||
      left: 40px;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__img {
 | 
			
		||||
    display: block;
 | 
			
		||||
@@ -360,24 +484,35 @@ export default {
 | 
			
		||||
    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 {
 | 
			
		||||
      &-block {
 | 
			
		||||
        float: left;
 | 
			
		||||
      }
 | 
			
		||||
      &-block:not(:last-child) {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
    > div {
 | 
			
		||||
      margin-bottom: 20px;
 | 
			
		||||
      margin-right: 20px;
 | 
			
		||||
      @include tablet-min {
 | 
			
		||||
        margin-bottom: 30px;
 | 
			
		||||
        margin-right: 30px;
 | 
			
		||||
      }
 | 
			
		||||
      }
 | 
			
		||||
      &-title {
 | 
			
		||||
      & .title {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        font-weight: 400;
 | 
			
		||||
        text-transform: uppercase;
 | 
			
		||||
@@ -387,12 +522,13 @@ export default {
 | 
			
		||||
          font-size: 16px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      &-text {
 | 
			
		||||
      & .text {
 | 
			
		||||
        font-weight: 300;
 | 
			
		||||
        font-size: 14px;
 | 
			
		||||
        margin-top: 5px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__admin {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
 
 | 
			
		||||
@@ -32,9 +32,11 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  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");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,119 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <li class="movies-item" :class="{'shortList': shortList}">
 | 
			
		||||
    <a class="movies-item__link" :class="{'no-image': noImage}" @click.prevent="openMoviePopup(movie.id, movie.type)">
 | 
			
		||||
  <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"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <!-- TODO change to picture element -->
 | 
			
		||||
      <figure class="movies-item__poster">
 | 
			
		||||
        <img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt="">
 | 
			
		||||
        <img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
 | 
			
		||||
      </figure>
 | 
			
		||||
      <div class="movies-item__content">
 | 
			
		||||
        <p class="movies-item__title">{{ movie.title }}</p>
 | 
			
		||||
        <p class="movies-item__title">{{ movie.year }}</p>
 | 
			
		||||
      <div 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>
 | 
			
		||||
    </a>
 | 
			
		||||
  </li>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import img from '../directives/v-image'
 | 
			
		||||
import img from "../directives/v-image";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: ['movie', 'shortList'],
 | 
			
		||||
  props: {
 | 
			
		||||
    movie: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      required: true
 | 
			
		||||
    },
 | 
			
		||||
    shortList: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      required: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  directives: {
 | 
			
		||||
    img: img
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      noImage: false
 | 
			
		||||
      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: {
 | 
			
		||||
    // TODO handle missing images better and load diff sizes based on screen size
 | 
			
		||||
    poster() {
 | 
			
		||||
      if (this.movie.poster) {
 | 
			
		||||
        return 'https://image.tmdb.org/t/p/w500' + this.movie.poster
 | 
			
		||||
      } else {
 | 
			
		||||
        this.noImage = true
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    openMoviePopup(id, type) {
 | 
			
		||||
      this.$popup.open(id, type)
 | 
			
		||||
    }
 | 
			
		||||
      this.$popup.open(id, type);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
@import "./src/scss/main";
 | 
			
		||||
 | 
			
		||||
.movies-item {
 | 
			
		||||
.movie-item {
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  width: 50%;
 | 
			
		||||
  background-color: $background-color;
 | 
			
		||||
@@ -56,6 +121,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
  @include tablet-min {
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
    width: 33%;
 | 
			
		||||
  }
 | 
			
		||||
  @include tablet-landscape-min {
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
@@ -67,37 +133,42 @@ export default {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @include desktop-lg-min {
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
    width: 12.5%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__link{
 | 
			
		||||
  &:hover &__info > p {
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__poster {
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    color: $text-color-70;
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
  }
 | 
			
		||||
  &__content{
 | 
			
		||||
    padding-top: 15px;
 | 
			
		||||
  }
 | 
			
		||||
  &__poster{
 | 
			
		||||
    transition: transform 0.5s ease, box-shadow 0.3s ease;
 | 
			
		||||
    transform: translateZ(0);
 | 
			
		||||
  }
 | 
			
		||||
  &__img{
 | 
			
		||||
 | 
			
		||||
    > img {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      transform: scale(0.97) translateZ(0);
 | 
			
		||||
    transition: opacity 0.5s ease, transform 0.5s ease;
 | 
			
		||||
      transition: opacity 1s ease, transform 0.5s ease;
 | 
			
		||||
      &.is-loaded {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
        transform: scale(1);
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
  &__link:not(.no-image):hover &__poster{
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        transform: scale(1.03);
 | 
			
		||||
        box-shadow: 0 0 10px rgba($dark, 0.1);
 | 
			
		||||
      }
 | 
			
		||||
  &__title{
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__info {
 | 
			
		||||
    padding-top: 15px;
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
 | 
			
		||||
    > p {
 | 
			
		||||
      color: $text-color-70;
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      font-size: 11px;
 | 
			
		||||
      letter-spacing: 0.5px;
 | 
			
		||||
@@ -110,8 +181,69 @@ export default {
 | 
			
		||||
        font-size: 14px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  &__link:hover &__title{
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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,7 +1,11 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
  <nav class="nav">
 | 
			
		||||
      <router-link class="nav__logo" :to="{name: 'home'}" exact title="Vue.js — TMDb App">
 | 
			
		||||
    <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>
 | 
			
		||||
@@ -23,8 +27,14 @@
 | 
			
		||||
        </router-link>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
      <li class="nav__item mobile-only"></li>
 | 
			
		||||
 | 
			
		||||
      <li class="nav__item nav__item--profile">
 | 
			
		||||
          <router-link class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn">
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -33,7 +43,11 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </router-link>
 | 
			
		||||
 | 
			
		||||
          <router-link class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn">
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -44,35 +58,36 @@
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </nav>
 | 
			
		||||
 | 
			
		||||
    <div class="spacer"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import storage from '@/storage'
 | 
			
		||||
import storage from "@/storage";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      listTypes: storage.homepageLists,
 | 
			
		||||
      userLoggedIn: localStorage.getItem('token') ? true : false
 | 
			
		||||
    }
 | 
			
		||||
      userLoggedIn: localStorage.getItem("token") ? true : false
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setUserStatus() {
 | 
			
		||||
      this.userLoggedIn = localStorage.getItem('token') ? true : false;
 | 
			
		||||
      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');
 | 
			
		||||
      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);
 | 
			
		||||
  }
 | 
			
		||||
    eventHub.$on("setUserStatus", this.setUserStatus);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@@ -83,44 +98,44 @@ export default {
 | 
			
		||||
  width: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spacer {
 | 
			
		||||
  @include mobile-only {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: $header-size;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav {
 | 
			
		||||
  transition: background .5s ease;
 | 
			
		||||
  transition: background 0.5s ease;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 50px;
 | 
			
		||||
  height: var(--header-size);
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
  display: block;
 | 
			
		||||
  color: $text-color;
 | 
			
		||||
  background-color: $background-color-secondary;
 | 
			
		||||
 | 
			
		||||
  @include tablet-min {
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: unset;
 | 
			
		||||
    width: 95px;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
  }
 | 
			
		||||
  &__logo {
 | 
			
		||||
    width: 55px;
 | 
			
		||||
    width: 95px;
 | 
			
		||||
    height: $header-size;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    background: $background-nav-logo;
 | 
			
		||||
    @include tablet-min{
 | 
			
		||||
      width: 95px;
 | 
			
		||||
 | 
			
		||||
    @include mobile-only {
 | 
			
		||||
      align-items: flex-start;
 | 
			
		||||
      padding-top: 0.5rem;
 | 
			
		||||
      width: 55px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-image {
 | 
			
		||||
      width: 35px;
 | 
			
		||||
      height: 31px;
 | 
			
		||||
      fill: $green;
 | 
			
		||||
      transition: transform 0.5s ease;
 | 
			
		||||
 | 
			
		||||
      @include tablet-min {
 | 
			
		||||
        width: 45px;
 | 
			
		||||
        height: 40px;
 | 
			
		||||
@@ -135,7 +150,7 @@ export default {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    width: 55px;
 | 
			
		||||
    height: 50px;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 1.5rem;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
@@ -198,14 +213,20 @@ export default {
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 50px;
 | 
			
		||||
    border-top: 1px solid $background-color;
 | 
			
		||||
 | 
			
		||||
    @include mobile-only {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: unset;
 | 
			
		||||
      bottom: var(--header-size);
 | 
			
		||||
      height: min-content;
 | 
			
		||||
      flex-wrap: wrap;
 | 
			
		||||
      font-size: 0;
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      visibility: hidden;
 | 
			
		||||
      background-color: $background-95;
 | 
			
		||||
      text-align: left;
 | 
			
		||||
 | 
			
		||||
      &--active {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
        visibility: visible;
 | 
			
		||||
@@ -221,12 +242,12 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__item {
 | 
			
		||||
    transition: background .5s ease, color .5s ease, border .5s ease;
 | 
			
		||||
    transition: background 0.5s ease, color 0.5s ease, border 0.5s ease;
 | 
			
		||||
    background-color: $background-color-secondary;
 | 
			
		||||
    color: $text-color-70;
 | 
			
		||||
 | 
			
		||||
    @include mobile-only {
 | 
			
		||||
      flex: 0 0 50%;
 | 
			
		||||
      flex: 0 0 33.3%;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      border-bottom: 1px solid $background-color;
 | 
			
		||||
      &:nth-child(odd) {
 | 
			
		||||
@@ -251,7 +272,8 @@ export default {
 | 
			
		||||
        border-left: 1px solid $background-color;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:hover, .is-active {
 | 
			
		||||
    &:hover,
 | 
			
		||||
    .is-active {
 | 
			
		||||
      color: $text-color;
 | 
			
		||||
      background-color: $background-color;
 | 
			
		||||
    }
 | 
			
		||||
@@ -299,14 +321,14 @@ export default {
 | 
			
		||||
        height: 20px;
 | 
			
		||||
        margin-bottom: 5px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    &-title {
 | 
			
		||||
      margin-top: 5px;
 | 
			
		||||
      display: block;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    &:hover &-icon, &.is-active &-icon {
 | 
			
		||||
    &:hover &-icon,
 | 
			
		||||
    &.is-active &-icon {
 | 
			
		||||
      fill: $text-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,10 @@
 | 
			
		||||
  <section class="profile">
 | 
			
		||||
    <div class="profile__content" v-if="userLoggedIn">
 | 
			
		||||
      <header class="profile__header">
 | 
			
		||||
        <h2 class="profile__title">{{ emoji }} Welcome {{ userName }}</h2>
 | 
			
		||||
        <h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
 | 
			
		||||
 | 
			
		||||
        <div class="button--group">
 | 
			
		||||
          <seasoned-button @click="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
 | 
			
		||||
          <seasoned-button @click="toggleSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
 | 
			
		||||
 | 
			
		||||
          <seasoned-button @click="logOut">Log out</seasoned-button>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -43,7 +43,6 @@ export default {
 | 
			
		||||
  data(){
 | 
			
		||||
    return{
 | 
			
		||||
      userLoggedIn: '',
 | 
			
		||||
      userName: '',
 | 
			
		||||
      emoji: '',
 | 
			
		||||
      results: undefined,
 | 
			
		||||
      totalResults: undefined,
 | 
			
		||||
@@ -58,32 +57,21 @@ export default {
 | 
			
		||||
      const loadedResults = this.results.length
 | 
			
		||||
      const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
 | 
			
		||||
      return `${loadedResults} of ${totalResults} results`
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    username: () => store.getters['userModule/username']
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    createSession(token){
 | 
			
		||||
      axios.get(`https://api.themoviedb.org/3/authentication/session/new?api_key=${storage.apiKey}&request_token=${token}`)
 | 
			
		||||
      .then(function(resp){
 | 
			
		||||
          let data = resp.data;
 | 
			
		||||
          if(data.success){
 | 
			
		||||
            let id = data.session_id;
 | 
			
		||||
            localStorage.setItem('session_id', id);
 | 
			
		||||
            eventHub.$emit('setUserStatus');
 | 
			
		||||
            this.userLoggedIn = true;
 | 
			
		||||
            this.getUserInfo();
 | 
			
		||||
          }
 | 
			
		||||
      }.bind(this));
 | 
			
		||||
    },
 | 
			
		||||
    getUserInfo(){
 | 
			
		||||
      this.userName = localStorage.getItem('username'); 
 | 
			
		||||
    },
 | 
			
		||||
    toggleSettings() {
 | 
			
		||||
      this.showSettings = this.showSettings ? false : true;
 | 
			
		||||
 | 
			
		||||
      if (this.showSettings) {
 | 
			
		||||
        this.$router.replace({ query: { settings: true} })
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$router.replace({ name: 'profile' })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    logOut(){
 | 
			
		||||
      localStorage.clear();
 | 
			
		||||
      eventHub.$emit('setUserStatus');
 | 
			
		||||
      this.$router.push({ name: 'home' });
 | 
			
		||||
      this.$router.push('logout')
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created(){
 | 
			
		||||
@@ -91,7 +79,8 @@ export default {
 | 
			
		||||
      this.userLoggedIn = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.userLoggedIn = true;
 | 
			
		||||
      this.getUserInfo();
 | 
			
		||||
 | 
			
		||||
      this.showSettings = window.location.toString().includes('settings=true')
 | 
			
		||||
 | 
			
		||||
      getUserRequests()
 | 
			
		||||
        .then(results => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,23 +2,20 @@
 | 
			
		||||
  <section>
 | 
			
		||||
    <h1>Register new user</h1>
 | 
			
		||||
 | 
			
		||||
    <seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
 | 
			
		||||
    <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="requestNewUser"/>
 | 
			
		||||
 | 
			
		||||
    <seasoned-input placeholder="repeat password" icon="Keyhole" type="password"
 | 
			
		||||
      :value.sync="passwordRepeat" @enter="requestNewUser"/>
 | 
			
		||||
 | 
			
		||||
    <seasoned-button @click="requestNewUser">Register</seasoned-button>
 | 
			
		||||
    <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 axios from 'axios'
 | 
			
		||||
import { register } from '@/api'
 | 
			
		||||
import SeasonedButton from '@/components/ui/SeasonedButton'
 | 
			
		||||
import SeasonedInput from '@/components/ui/SeasonedInput'
 | 
			
		||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
 | 
			
		||||
@@ -34,60 +31,47 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    requestNewUser(){
 | 
			
		||||
      let { username, password, passwordRepeat } = this
 | 
			
		||||
    submit() {
 | 
			
		||||
      this.messages = [];
 | 
			
		||||
      let { username, password, passwordRepeat } = this;
 | 
			
		||||
 | 
			
		||||
      let verifyCredentials = this.checkCredentials(username, password, passwordRepeat);
 | 
			
		||||
      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
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (verifyCredentials.verified) {
 | 
			
		||||
        axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
 | 
			
		||||
          username: username,
 | 
			
		||||
          password: password
 | 
			
		||||
        })
 | 
			
		||||
        .then(resp => {
 | 
			
		||||
          let data = resp.data;
 | 
			
		||||
      this.registerUser(username, password)
 | 
			
		||||
    },
 | 
			
		||||
    registerUser(username, password) {
 | 
			
		||||
      register(username, password, true)
 | 
			
		||||
        .then(data => {
 | 
			
		||||
          if (data.success){
 | 
			
		||||
            localStorage.setItem('token', data.token);
 | 
			
		||||
            localStorage.setItem('username', username);
 | 
			
		||||
            localStorage.setItem('admin', data.admin)
 | 
			
		||||
            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 => {
 | 
			
		||||
          this.messages.push({ type: 'error', title: 'Unexpected error', message: error.response.data.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 })
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      } 
 | 
			
		||||
      else {
 | 
			
		||||
        this.messages.push({ type: 'warning', title: 'Parse error', message: verifyCredentials.reason })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    checkCredentials(username, password, passwordRepeat) {
 | 
			
		||||
      if (!username || username.length === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          verified: false,
 | 
			
		||||
          reason: 'Fill inn username'
 | 
			
		||||
        }
 | 
			
		||||
      } 
 | 
			
		||||
      else if (!password || !passwordRepeat) {
 | 
			
		||||
        return {
 | 
			
		||||
          verified: false,
 | 
			
		||||
          reason: "Fill inn both password fields"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      else if (password !== passwordRepeat) {
 | 
			
		||||
        return {
 | 
			
		||||
          verified: false,
 | 
			
		||||
          reason: 'Passwords do not match'
 | 
			
		||||
        }
 | 
			
		||||
      } 
 | 
			
		||||
      else {
 | 
			
		||||
        return {
 | 
			
		||||
          verified: true,
 | 
			
		||||
          reason: 'Verified credentials'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    logOut(){
 | 
			
		||||
      localStorage.clear();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
  <div class="page-container">
 | 
			
		||||
    <list-header :title="title" :info="resultCount" :sticky="true" />
 | 
			
		||||
 | 
			
		||||
    <results-list :results="results" />
 | 
			
		||||
@@ -8,16 +8,34 @@
 | 
			
		||||
      <seasoned-button @click="loadMore">load more</seasoned-button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <loader v-if="!results.length" />
 | 
			
		||||
    <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'
 | 
			
		||||
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 },
 | 
			
		||||
@@ -37,60 +55,79 @@ export default {
 | 
			
		||||
      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`
 | 
			
		||||
      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) {
 | 
			
		||||
      searchTmdb(query, page)
 | 
			
		||||
        .then(this.parseResponse)
 | 
			
		||||
    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)
 | 
			
		||||
        this.results.push(...data.results);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.results = data.results
 | 
			
		||||
        this.results = data.results;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.totalPages = data.total_pages
 | 
			
		||||
      this.totalResults = data.total_results || data.results.length
 | 
			
		||||
      this.totalPages = data.total_pages;
 | 
			
		||||
      this.totalResults = data.total_results || data.results.length;
 | 
			
		||||
 | 
			
		||||
      this.loading = false
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
    },
 | 
			
		||||
    loadMore() {
 | 
			
		||||
      this.page++
 | 
			
		||||
      this.page++;
 | 
			
		||||
 | 
			
		||||
      window.history.replaceState({}, 'search', `/#/search?query=${this.query}&page=${this.page}`)
 | 
			
		||||
      this.search()
 | 
			
		||||
      window.history.replaceState(
 | 
			
		||||
        {},
 | 
			
		||||
        "search",
 | 
			
		||||
        `/#/search?query=${this.query}&page=${this.page}`
 | 
			
		||||
      );
 | 
			
		||||
      this.search();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    const { query, page } = this.$route.query
 | 
			
		||||
    const { query, page, adult, media_type } = this.$route.query;
 | 
			
		||||
 | 
			
		||||
    if (!query) {
 | 
			
		||||
      // abort
 | 
			
		||||
      console.error('abort, no query')
 | 
			
		||||
      console.error("abort, no query");
 | 
			
		||||
    }
 | 
			
		||||
    this.query = decodeURIComponent(query)
 | 
			
		||||
    this.page = page ? page : 1
 | 
			
		||||
    this.title = `Search results: ${this.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()
 | 
			
		||||
    this.search();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
 | 
			
		||||
@include mobile-only {
 | 
			
		||||
  .page-container {
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fullwidth-button {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 1rem 0;
 | 
			
		||||
@@ -98,5 +135,4 @@ export default {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,176 +1,290 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    
 | 
			
		||||
  <!-- <div> -->
 | 
			
		||||
  <div class="search">
 | 
			
		||||
    <input
 | 
			
		||||
      ref="input"
 | 
			
		||||
      type="text"
 | 
			
		||||
        placeholder="Search for a movie or show"
 | 
			
		||||
      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" />
 | 
			
		||||
      @keydown.down="navigateDown"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
      <svg class="search--icon" fill="currentColor"><use xlink:href="#iconSearch"></use></svg>
 | 
			
		||||
    <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="dropdown--results">
 | 
			
		||||
        <div class="filter">
 | 
			
		||||
          <h2>Filter your search:</h2>
 | 
			
		||||
 | 
			
		||||
          <ul v-for="(item, index) in elasticSearchResults"
 | 
			
		||||
              @click="$popup.open(item.id, item.type)"
 | 
			
		||||
              :class="{ active: index + 1 === selectedResult}">
 | 
			
		||||
                
 | 
			
		||||
              {{ item.name }}
 | 
			
		||||
          </ul>
 | 
			
		||||
          <div class="filter-items">
 | 
			
		||||
            <toggle-button
 | 
			
		||||
              :options="searchTypes"
 | 
			
		||||
              :selected.sync="selectedSearchType"
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <label
 | 
			
		||||
              >Adult
 | 
			
		||||
              <input type="checkbox" value="adult" v-model="adult" />
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <seasoned-button class="end-section" fullWidth="true" 
 | 
			
		||||
          @click="focus = false" :active="elasticSearchResults.length + 1 === selectedResult">
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  </div> -->
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import SeasonedButton from '@/components/ui/SeasonedButton'
 | 
			
		||||
import SeasonedButton from "@/components/ui/SeasonedButton";
 | 
			
		||||
import ToggleButton from "@/components/ui/ToggleButton";
 | 
			
		||||
 | 
			
		||||
import { elasticSearchMoviesAndShows } from '@/api'
 | 
			
		||||
import config from '@/config.json'
 | 
			
		||||
import { elasticSearchMoviesAndShows } from "@/api";
 | 
			
		||||
import config from "@/config.json";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'SearchInput',
 | 
			
		||||
  name: "SearchInput",
 | 
			
		||||
  components: {
 | 
			
		||||
    SeasonedButton
 | 
			
		||||
    SeasonedButton,
 | 
			
		||||
    ToggleButton
 | 
			
		||||
  },
 | 
			
		||||
  props: ['value'],
 | 
			
		||||
  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: '',
 | 
			
		||||
      elasticSearchResults: [],
 | 
			
		||||
      selectedResult: 0
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    focus: function (val) {
 | 
			
		||||
      if (val === true) {
 | 
			
		||||
        window.addEventListener('scroll', this.disableFocus)
 | 
			
		||||
        window.addEventListener("scroll", this.disableFocus);
 | 
			
		||||
      } else {
 | 
			
		||||
        window.removeEventListener('scroll', this.disableFocus)
 | 
			
		||||
        this.scrollDistance = 0
 | 
			
		||||
        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
 | 
			
		||||
    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)
 | 
			
		||||
    console.log("scroll eventlistener not removed, destroying!");
 | 
			
		||||
    window.removeEventListener("scroll", this.disableFocus);
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    navigateDown() {
 | 
			
		||||
      this.focus = true
 | 
			
		||||
      this.selectedResult++
 | 
			
		||||
      this.focus = true;
 | 
			
		||||
      this.selectedResult++;
 | 
			
		||||
    },
 | 
			
		||||
    navigateUp() {
 | 
			
		||||
      this.focus = true
 | 
			
		||||
      this.selectedResult--
 | 
			
		||||
      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);
 | 
			
		||||
      this.selectedResult = 0;
 | 
			
		||||
      this.$emit("input", this.query);
 | 
			
		||||
 | 
			
		||||
      if (!this.focus) {
 | 
			
		||||
        this.focus = true;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      elasticSearchMoviesAndShows(this.query)
 | 
			
		||||
      .then(resp => {
 | 
			
		||||
        const data = resp.hits.hits
 | 
			
		||||
      elasticSearchMoviesAndShows(this.query).then(resp => {
 | 
			
		||||
        const data = resp.hits.hits;
 | 
			
		||||
 | 
			
		||||
        this.elasticSearchResults = data.map(item => {
 | 
			
		||||
          const index = item._index.slice(0, -1)
 | 
			
		||||
          if (index === 'movie' || item._source.original_title) {
 | 
			
		||||
        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,
 | 
			
		||||
              type: 'movie'
 | 
			
		||||
            }
 | 
			
		||||
          } else if (index === 'show' || item._source.original_name) {
 | 
			
		||||
              adult: item._source.adult,
 | 
			
		||||
              type: "movie"
 | 
			
		||||
            };
 | 
			
		||||
          } else if (index === "show" || item._source.original_name) {
 | 
			
		||||
            return {
 | 
			
		||||
              name: item._source.original_name,
 | 
			
		||||
              id: item._source.id,
 | 
			
		||||
              type: 'show'
 | 
			
		||||
              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;
 | 
			
		||||
        }
 | 
			
		||||
        })
 | 
			
		||||
        console.log(this.elasticSearchResults)
 | 
			
		||||
      })
 | 
			
		||||
        filteredResults.push(result);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (this.adult == false) {
 | 
			
		||||
        filteredResults = filteredResults.filter(
 | 
			
		||||
          result => result.adult == false
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return filteredResults;
 | 
			
		||||
    },
 | 
			
		||||
    handleSubmit() {
 | 
			
		||||
      let searchResults = this.elasticSearchResults
 | 
			
		||||
      let searchResults = this.elasticSearchResults;
 | 
			
		||||
 | 
			
		||||
      if (this.selectedResult > searchResults.length) {
 | 
			
		||||
        this.focus = false
 | 
			
		||||
        this.selectedResult = 0
 | 
			
		||||
        this.focus = false;
 | 
			
		||||
        this.selectedResult = 0;
 | 
			
		||||
      } else if (this.selectedResult > 0) {
 | 
			
		||||
        const resultItem = searchResults[this.selectedResult - 1]
 | 
			
		||||
        this.$popup.open(resultItem.id, resultItem.type)
 | 
			
		||||
        const resultItem = searchResults[this.selectedResult - 1];
 | 
			
		||||
        this.$popup.open(resultItem.id, resultItem.type);
 | 
			
		||||
      } else {
 | 
			
		||||
        const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'))
 | 
			
		||||
        this.$router.push({ name: 'search', query: { query: encodedQuery }});
 | 
			
		||||
        this.focus = false
 | 
			
		||||
        this.selectedResult = 0
 | 
			
		||||
        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!')
 | 
			
		||||
        console.log("THIS WAS FUCKOING OPEN!");
 | 
			
		||||
      } else {
 | 
			
		||||
        this.focus = false
 | 
			
		||||
        this.focus = false;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    disableFocus(_) {
 | 
			
		||||
      this.focus = false
 | 
			
		||||
    }
 | 
			
		||||
      this.focus = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
@import './src/scss/main';
 | 
			
		||||
 | 
			
		||||
@import "./src/scss/main";
 | 
			
		||||
 | 
			
		||||
.fade-enter-active {
 | 
			
		||||
  transition: opacity .2s;
 | 
			
		||||
  transition: opacity 0.2s;
 | 
			
		||||
}
 | 
			
		||||
.fade-leave-active {
 | 
			
		||||
  transition: opacity .2s;
 | 
			
		||||
  transition: opacity 0.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;
 | 
			
		||||
@@ -188,7 +302,11 @@ export default {
 | 
			
		||||
    width: calc(100%);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--results {
 | 
			
		||||
  .not-found {
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &-results {
 | 
			
		||||
    padding-left: 60px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
@@ -214,7 +332,9 @@ export default {
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      color: $text-color-50;
 | 
			
		||||
 | 
			
		||||
      &.active, &:hover, &:active {
 | 
			
		||||
      &.active,
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:active {
 | 
			
		||||
        color: $text-color;
 | 
			
		||||
        border-bottom: 2px solid $text-color;
 | 
			
		||||
      }
 | 
			
		||||
@@ -227,13 +347,13 @@ export default {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  z-index: 5;
 | 
			
		||||
  z-index: 16;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  background-color: $background-color-secondary;
 | 
			
		||||
 | 
			
		||||
  // TODO check if this is for mobile
 | 
			
		||||
  width: calc(100% - 110px);
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  right: 55px;
 | 
			
		||||
 | 
			
		||||
  @include tablet-min {
 | 
			
		||||
@@ -244,23 +364,26 @@ export default {
 | 
			
		||||
 | 
			
		||||
  input {
 | 
			
		||||
    display: block;
 | 
			
		||||
    height: calc($header-size - 1.5rem);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 13px 20px 13px 45px;
 | 
			
		||||
    padding: 13px 0 13px 45px;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    margin-bottom: auto;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    background-color: $background-color-secondary;
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
    font-size: 19px;
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
    transition: background-color .5s ease, color .5s ease;
 | 
			
		||||
    transition: background-color 0.5s ease, color 0.5s ease;
 | 
			
		||||
 | 
			
		||||
    @include tablet-min {
 | 
			
		||||
      height: calc($header-size);
 | 
			
		||||
      padding: 13px 30px 13px 60px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--icon{
 | 
			
		||||
  &-icon {
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    fill: $text-color-50;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@
 | 
			
		||||
    <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">
 | 
			
		||||
@@ -11,9 +13,13 @@
 | 
			
		||||
              :value.sync="plexPassword" @submit="authenticatePlex" />
 | 
			
		||||
 | 
			
		||||
            <seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
 | 
			
		||||
 | 
			
		||||
          <seasoned-messages :messages.sync="messages" />
 | 
			
		||||
          </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'>
 | 
			
		||||
 | 
			
		||||
@@ -44,12 +50,13 @@
 | 
			
		||||
</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 { plexAuthenticate } from '@/api'
 | 
			
		||||
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: { SeasonedInput, SeasonedButton, SeasonedMessages },
 | 
			
		||||
@@ -60,7 +67,21 @@ export default {
 | 
			
		||||
      plexUsername: null,
 | 
			
		||||
      plexPassword: null,
 | 
			
		||||
      newPassword: null,
 | 
			
		||||
      newPasswordRepeat: 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: {
 | 
			
		||||
@@ -70,26 +91,37 @@ export default {
 | 
			
		||||
    changePassword() {
 | 
			
		||||
      return
 | 
			
		||||
    },
 | 
			
		||||
    authenticatePlex() {
 | 
			
		||||
    async authenticatePlex() {
 | 
			
		||||
      let username = this.plexUsername
 | 
			
		||||
      let password = this.plexPassword
 | 
			
		||||
 | 
			
		||||
      plexAuthenticate(username, password)
 | 
			
		||||
      .then(resp => {
 | 
			
		||||
        const data = resp.data
 | 
			
		||||
        this.messages.push({ type: 'success', title: 'Authenticated with plex', message: 'Successfully linked plex account with seasoned request' })
 | 
			
		||||
      const response = await linkPlexAccount(username, password)
 | 
			
		||||
 | 
			
		||||
        console.log('response from plex:', data.username)
 | 
			
		||||
      this.messages.push({
 | 
			
		||||
        type: response.success ? 'success' : 'error',
 | 
			
		||||
        title: response.success ? 'Authenticated with plex' : 'Something went wrong',
 | 
			
		||||
        message: response.message
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => {
 | 
			
		||||
        console.error(error);
 | 
			
		||||
 | 
			
		||||
        this.messages.push({ type: 'error', title: 'Something went wrong', message: error.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(){
 | 
			
		||||
    if (localStorage.getItem('token')){
 | 
			
		||||
    const token = localStorage.getItem('token') || false;
 | 
			
		||||
    if (token){
 | 
			
		||||
      this.userLoggedIn = true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -129,7 +161,11 @@ a {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.settings {
 | 
			
		||||
   padding: 35px;
 | 
			
		||||
  padding: 3rem;
 | 
			
		||||
 | 
			
		||||
  @include mobile-only {
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
   &__header {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
@@ -147,7 +183,7 @@ a {
 | 
			
		||||
      display: block;
 | 
			
		||||
      height: 1px;
 | 
			
		||||
      border: 0;
 | 
			
		||||
      border-bottom: 1px solid rgba(8, 28, 36, 0.05);
 | 
			
		||||
      border-bottom: 1px solid $text-color-50;
 | 
			
		||||
      margin-top: 30px;
 | 
			
		||||
      margin-bottom: 70px;
 | 
			
		||||
      margin-left: 20px;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,25 +2,29 @@
 | 
			
		||||
  <section>
 | 
			
		||||
    <h1>Sign in</h1>
 | 
			
		||||
 | 
			
		||||
    <seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
 | 
			
		||||
    <seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="signin"/>
 | 
			
		||||
 | 
			
		||||
    <seasoned-button @click="signin">sign in</seasoned-button>
 | 
			
		||||
    <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>
 | 
			
		||||
 | 
			
		||||
    <seasoned-messages :messages.sync="messages"></seasoned-messages>
 | 
			
		||||
  </section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
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 },
 | 
			
		||||
@@ -35,28 +39,39 @@ export default {
 | 
			
		||||
    setValue(l, t) {
 | 
			
		||||
      this[l] = t
 | 
			
		||||
    },
 | 
			
		||||
    signin(){
 | 
			
		||||
    submit() {
 | 
			
		||||
      this.messages = [];
 | 
			
		||||
      let username = this.username;
 | 
			
		||||
      let password = this.password;
 | 
			
		||||
 | 
			
		||||
      axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, {
 | 
			
		||||
        username: username,
 | 
			
		||||
        password: password
 | 
			
		||||
      })
 | 
			
		||||
      .then(resp => {
 | 
			
		||||
        let data = resp.data;
 | 
			
		||||
      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', username);
 | 
			
		||||
          localStorage.setItem('admin', data.admin);
 | 
			
		||||
            localStorage.setItem('username', jwtData['username']);
 | 
			
		||||
            localStorage.setItem('admin', jwtData['admin'] || false);
 | 
			
		||||
 | 
			
		||||
            eventHub.$emit('setUserStatus');
 | 
			
		||||
            this.$router.push({ name: 'profile' })
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
        if (error.message.endsWith('401')) {
 | 
			
		||||
          this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
 | 
			
		||||
          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 })
 | 
			
		||||
 
 | 
			
		||||
@@ -20,9 +20,11 @@
 | 
			
		||||
 | 
			
		||||
    <div v-if="listLoaded">
 | 
			
		||||
      <div v-if="torrents.length > 0">
 | 
			
		||||
        <ul class="filter">
 | 
			
		||||
        <!-- <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>
 | 
			
		||||
        </ul> -->
 | 
			
		||||
 | 
			
		||||
        <toggle-button :options="release_types" :selected.sync="selectedRelaseType" class="toggle"></toggle-button>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <table>
 | 
			
		||||
@@ -97,9 +99,10 @@ import { searchTorrents, addMagnet } from '@/api'
 | 
			
		||||
 | 
			
		||||
import SeasonedButton from '@/components/ui/SeasonedButton'
 | 
			
		||||
import SeasonedInput from '@/components/ui/SeasonedInput'
 | 
			
		||||
import ToggleButton from '@/components/ui/ToggleButton'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: { SeasonedButton, SeasonedInput },
 | 
			
		||||
  components: { SeasonedButton, SeasonedInput, ToggleButton },
 | 
			
		||||
  props: {
 | 
			
		||||
    query: {
 | 
			
		||||
      type: String,
 | 
			
		||||
@@ -110,7 +113,7 @@ export default {
 | 
			
		||||
      require: true
 | 
			
		||||
    },
 | 
			
		||||
    tmdb_type: String,
 | 
			
		||||
    admin: String,
 | 
			
		||||
    admin: Boolean,
 | 
			
		||||
    show: Boolean
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
@@ -133,6 +136,11 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
    store.dispatch('torrentModule/reset')
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    selectedRelaseType: function(newValue) {
 | 
			
		||||
      this.applyFilter(newValue)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    selectedSortableClass(headerName) {
 | 
			
		||||
      return headerName === this.prevCol ? 'active' : ''
 | 
			
		||||
@@ -147,27 +155,31 @@ export default {
 | 
			
		||||
    expand(event, name) {
 | 
			
		||||
      const existingExpandedElement = document.getElementsByClassName('expanded')[0]
 | 
			
		||||
 | 
			
		||||
      const clickedElement = event.target.parentNode;
 | 
			
		||||
      const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0]
 | 
			
		||||
 | 
			
		||||
      if (existingExpandedElement) {
 | 
			
		||||
        console.log('exists')
 | 
			
		||||
        const expandedSibling = event.target.parentNode.nextSibling.className === 'expanded'
 | 
			
		||||
 | 
			
		||||
        existingExpandedElement.remove()
 | 
			
		||||
        const table = document.getElementsByTagName('table')[0]
 | 
			
		||||
        table.style.display = 'block'
 | 
			
		||||
 | 
			
		||||
        if (expandedSibling) {
 | 
			
		||||
          console.log('sibling is here')
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log('expand event', event)
 | 
			
		||||
      const nameRow = document.createElement('tr')
 | 
			
		||||
      const nameCol = document.createElement('td')
 | 
			
		||||
      nameRow.className = 'expanded'
 | 
			
		||||
      nameRow.dataset[scopedStyleDataVariable] = "";
 | 
			
		||||
      nameCol.innerText = name
 | 
			
		||||
      nameCol.dataset[scopedStyleDataVariable] = "";
 | 
			
		||||
 | 
			
		||||
      nameRow.appendChild(nameCol)
 | 
			
		||||
 | 
			
		||||
      event.target.parentNode.insertAdjacentElement('afterend', nameRow)
 | 
			
		||||
      clickedElement.insertAdjacentElement('afterend', nameRow)
 | 
			
		||||
    },
 | 
			
		||||
    sendTorrent(magnet, name, event){
 | 
			
		||||
      this.$notifications.info({
 | 
			
		||||
@@ -177,7 +189,6 @@ export default {
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      event.target.parentNode.classList.add('active')
 | 
			
		||||
 | 
			
		||||
      addMagnet(magnet, name, this.tmdb_id)
 | 
			
		||||
      .catch((resp) => { console.log('error:', resp.data) })
 | 
			
		||||
      .then((resp) => {
 | 
			
		||||
@@ -193,7 +204,6 @@ export default {
 | 
			
		||||
      if (this.prevCol === col && sameDirection === false) {
 | 
			
		||||
        this.direction = !this.direction
 | 
			
		||||
      }
 | 
			
		||||
      console.log('col and more', col, sameDirection)
 | 
			
		||||
 | 
			
		||||
      switch (col) {
 | 
			
		||||
        case 'name':
 | 
			
		||||
@@ -279,14 +289,13 @@ export default {
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
.expanded {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin: 0 1rem;
 | 
			
		||||
  padding: 0.25rem 1rem;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  border-left: 1px solid $text-color;
 | 
			
		||||
  border-right: 1px solid $text-color;
 | 
			
		||||
  border-bottom: 1px solid $text-color;
 | 
			
		||||
 | 
			
		||||
  td {
 | 
			
		||||
    // border-left: 1px solid $c-dark;
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
    padding: 0.5rem 0.15rem;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
@@ -298,8 +307,14 @@ export default {
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
@import "./src/scss/elements";
 | 
			
		||||
 | 
			
		||||
.toggle {
 | 
			
		||||
  max-width: unset !important;
 | 
			
		||||
  margin: 1rem 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
  background-color: $background-color;
 | 
			
		||||
  padding: 0 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.torrentHeader {
 | 
			
		||||
@@ -348,7 +363,6 @@ table {
 | 
			
		||||
.table__content, .table__header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0 1rem;
 | 
			
		||||
  border-left: 1px solid $text-color;
 | 
			
		||||
  border-right: 1px solid $text-color;
 | 
			
		||||
  border-bottom: 1px solid $text-color;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="seasoned-button">
 | 
			
		||||
    <button type="button" class="button" @click="emit('click')" :class="{ active: active }"><slot></slot></button>
 | 
			
		||||
  </div>
 | 
			
		||||
  <button type="button" @click="emit('click')" :class="{ active: active }">
 | 
			
		||||
    <slot></slot>
 | 
			
		||||
  </button>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
@@ -9,7 +9,11 @@
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'seasonedButton',
 | 
			
		||||
  props: {
 | 
			
		||||
    active: Boolean
 | 
			
		||||
    active: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false,
 | 
			
		||||
      required: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    emit() {
 | 
			
		||||
@@ -23,32 +27,39 @@ export default {
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
 | 
			
		||||
.button{
 | 
			
		||||
button {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  border: 1px solid $text-color;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: 300;
 | 
			
		||||
  font-size: 11px;
 | 
			
		||||
  line-height: 2;
 | 
			
		||||
  height: 45px;
 | 
			
		||||
  font-weight: 300;
 | 
			
		||||
  line-height: 1.5;
 | 
			
		||||
  letter-spacing: 0.5px;
 | 
			
		||||
  padding: 5px 20px 4px 20px;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  min-height: 45px;
 | 
			
		||||
  padding: 5px 10px 4px 10px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  margin-right: 0.3rem;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  color: $text-color;
 | 
			
		||||
  background: $background-color-secondary;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  outline: none;
 | 
			
		||||
  transition: background 0.5s ease, color 0.5s ease, border-color .5s ease;
 | 
			
		||||
 | 
			
		||||
  @include tablet-min{
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
  @include desktop {
 | 
			
		||||
    font-size: 0.8rem;
 | 
			
		||||
    padding: 6px 20px 5px 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  body:not(.touch) &:hover, &:focus, &:active, &.active {
 | 
			
		||||
  &:focus, &:active, &.active {
 | 
			
		||||
    background: $text-color;
 | 
			
		||||
    color: $background-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (hover: hover) {
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background: $text-color;
 | 
			
		||||
      color: $background-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@
 | 
			
		||||
    <div class="message" v-for="(message, index) in reversedMessages" :class="message.type || 'warning'" :key="index">
 | 
			
		||||
      <span class="pinstripe"></span>
 | 
			
		||||
      <div>
 | 
			
		||||
        <h2>{{ message.title || defaultTitles[message.type] }}</h2>
 | 
			
		||||
        <span>{{ message.message }}</span>
 | 
			
		||||
        <h2 class="title">{{ message.title || defaultTitles[message.type] }}</h2>
 | 
			
		||||
        <span v-if="message.message" class="message">{{ message.message }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <button class="dismiss" @click="clicked(message)">X</button>
 | 
			
		||||
@@ -41,20 +41,15 @@ export default {
 | 
			
		||||
      const removedMessage = [...this.messages].filter(mes => mes !== e)
 | 
			
		||||
      this.$emit('update:messages', removedMessage)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  // watch: {
 | 
			
		||||
  //   messages(propState, oldState) {
 | 
			
		||||
  //     const newMessage = propState.filter(msg => !this.localMessages.includes(msg))
 | 
			
		||||
  //     console.log('newMessage', newMessage)
 | 
			
		||||
  //     this.localMessages = this.localMessages.concat(newMessage)
 | 
			
		||||
  //   }
 | 
			
		||||
  // }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
 | 
			
		||||
.fade-enter-active {
 | 
			
		||||
  transition: opacity .4s;
 | 
			
		||||
}
 | 
			
		||||
@@ -68,7 +63,6 @@ export default {
 | 
			
		||||
.message {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 35rem;
 | 
			
		||||
  height: 75px;
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
@@ -76,12 +70,12 @@ export default {
 | 
			
		||||
  color: $text-color-70;
 | 
			
		||||
 | 
			
		||||
  > div {
 | 
			
		||||
    margin: 6px 24px;
 | 
			
		||||
    margin: 10px 24px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  h2 {
 | 
			
		||||
  .title {
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
    letter-spacing: 0.25px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
@@ -89,16 +83,30 @@ export default {
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
    transition: color .5s ease;
 | 
			
		||||
  }
 | 
			
		||||
  span {
 | 
			
		||||
  .message {
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
    color: $text-color-70;
 | 
			
		||||
    transition: color .5s ease;
 | 
			
		||||
    margin: 0.2rem 0 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @include mobile-only {
 | 
			
		||||
    > div {
 | 
			
		||||
      margin: 6px 6px;
 | 
			
		||||
      line-height: 1.3rem;
 | 
			
		||||
    }
 | 
			
		||||
    h2 {
 | 
			
		||||
      font-size: 1.1rem;
 | 
			
		||||
    }
 | 
			
		||||
    span {
 | 
			
		||||
      font-size: 0.9rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .pinstripe {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 0.5rem;
 | 
			
		||||
    // background-color: $color-error-highlight;
 | 
			
		||||
    background-color: $color-error-highlight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dismiss {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										100
									
								
								src/components/ui/ToggleButton.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/components/ui/ToggleButton.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="toggle-container">
 | 
			
		||||
    <button v-for="option in options" class="toggle-button" @click="toggle(option)"
 | 
			
		||||
      :class="toggleValue === option ? 'selected' : null"
 | 
			
		||||
    >{{ option }}</button>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    options: {
 | 
			
		||||
      Array,
 | 
			
		||||
      required: true
 | 
			
		||||
    },
 | 
			
		||||
    selected: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: undefined
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      toggleValue: this.selected || this.options[0]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeMount() {
 | 
			
		||||
    this.toggle(this.toggleValue)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    toggle(toggleValue) {
 | 
			
		||||
      this.toggleValue = toggleValue;
 | 
			
		||||
      if (this.selected !== undefined) {
 | 
			
		||||
        this.$emit('update:selected', toggleValue)
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$emit('change', toggleValue)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/variables";
 | 
			
		||||
 | 
			
		||||
$background: $background-ui;
 | 
			
		||||
$background-selected: $background-color-secondary;
 | 
			
		||||
 | 
			
		||||
.toggle-container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 15rem;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  // padding: 0.2rem;
 | 
			
		||||
  background-color: $background;
 | 
			
		||||
  border: 2px solid $background;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  border-left: 4px solid $background;
 | 
			
		||||
  border-right: 4px solid $background;
 | 
			
		||||
 | 
			
		||||
  .toggle-button {
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
    line-height: 1rem;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 0.5rem 0;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    color: $text-color;
 | 
			
		||||
    // background-color: $text-color-5;
 | 
			
		||||
    background-color: $background;
 | 
			
		||||
    text-transform: capitalize;
 | 
			
		||||
 | 
			
		||||
    &.selected {
 | 
			
		||||
      color: $text-color;
 | 
			
		||||
      // background-color: $background-color-secondary;
 | 
			
		||||
      background-color: $background-selected;
 | 
			
		||||
      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>
 | 
			
		||||
@@ -1,36 +1,38 @@
 | 
			
		||||
<template>
 | 
			
		||||
 | 
			
		||||
  <div class="darkToggle">
 | 
			
		||||
    <span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      darkmode: window.getComputedStyle(document.body).colorScheme.includes('dark')
 | 
			
		||||
    }
 | 
			
		||||
      darkmode: this.supported
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    toggleDarkmode() {
 | 
			
		||||
      this.darkmode = !this.darkmode;
 | 
			
		||||
      document.body.className = this.darkmode ? 'dark' : 'light'
 | 
			
		||||
      document.body.className = this.darkmode ? "dark" : "light";
 | 
			
		||||
    },
 | 
			
		||||
    supported() {
 | 
			
		||||
      const computedStyle = window.getComputedStyle(document.body);
 | 
			
		||||
      if (computedStyle["colorScheme"] != null)
 | 
			
		||||
        return computedStyle.colorScheme.includes("dark");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    darkmodeToggleIcon() {
 | 
			
		||||
      return this.darkmode ? '🌝' : '🌚'
 | 
			
		||||
      return this.darkmode ? "🌝" : "🌚";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/scss/media-queries";
 | 
			
		||||
.darkToggle {
 | 
			
		||||
  height: 25px;
 | 
			
		||||
  width: 25px;
 | 
			
		||||
@@ -41,7 +43,11 @@ export default {
 | 
			
		||||
  margin-right: 2px;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
 | 
			
		||||
  @include mobile-only {
 | 
			
		||||
    margin-bottom: 5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,18 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <a @click="$emit('click')"><li>
 | 
			
		||||
      <figure :class="activeClassIfActive">
 | 
			
		||||
        <svg><use :xlink:href="iconRefNameIfActive"/></svg>
 | 
			
		||||
    <a @click="$emit('click')">
 | 
			
		||||
      <li>
 | 
			
		||||
        <figure v-if="iconRef" :class="activeClassIfActive">
 | 
			
		||||
          <svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg>
 | 
			
		||||
        </figure>
 | 
			
		||||
 | 
			
		||||
      <span :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
 | 
			
		||||
        <span class="text" :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
 | 
			
		||||
 | 
			
		||||
        <span v-if="supplementaryText" class="supplementary-text">
 | 
			
		||||
          {{ supplementaryText }}
 | 
			
		||||
        </span>
 | 
			
		||||
    </li></a>
 | 
			
		||||
      </li>
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +23,7 @@ export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    iconRef: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true
 | 
			
		||||
      required: false
 | 
			
		||||
    },
 | 
			
		||||
    iconRefActive: {
 | 
			
		||||
      type: String,
 | 
			
		||||
@@ -44,7 +46,7 @@ export default {
 | 
			
		||||
    iconRefNameIfActive() {
 | 
			
		||||
      const { iconRefActive, iconRef, active } = this
 | 
			
		||||
 | 
			
		||||
      if ((iconRefActive && iconRef) & active) {
 | 
			
		||||
      if ((iconRefActive && iconRef) && active) {
 | 
			
		||||
        return iconRefActive
 | 
			
		||||
      }
 | 
			
		||||
      return iconRef
 | 
			
		||||
@@ -83,39 +85,53 @@ li {
 | 
			
		||||
  border-bottom: 1px solid $text-color-5;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: $text-color-70;
 | 
			
		||||
    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, figure > svg {
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    height: 18px;
 | 
			
		||||
  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;
 | 
			
		||||
      }
 | 
			
		||||
    &:hover &-icon {
 | 
			
		||||
      fill: $text-color-70;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
    &.active > svg {
 | 
			
		||||
      fill: $green;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ const setDocumentTitle = (state) => {
 | 
			
		||||
export default {
 | 
			
		||||
  namespaced: true,
 | 
			
		||||
  state: {
 | 
			
		||||
    emoji: '🍕',
 | 
			
		||||
    titlePrefix: 'request',
 | 
			
		||||
    emoji: '',
 | 
			
		||||
    titlePrefix: 'seasoned',
 | 
			
		||||
    title: undefined
 | 
			
		||||
  },
 | 
			
		||||
  getters: {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										112
									
								
								src/modules/userModule.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/modules/userModule.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
import { getSettings } from '@/api'
 | 
			
		||||
 | 
			
		||||
function setLocalStorageByKey(key, value) {
 | 
			
		||||
  if (value instanceof Object || value instanceof Array) {
 | 
			
		||||
    value = JSON.stringify(value)
 | 
			
		||||
  }
 | 
			
		||||
  const buff = Buffer.from(value)
 | 
			
		||||
  const encodedValue = buff.toString('base64')
 | 
			
		||||
  localStorage.setItem(key, encodedValue)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getLocalStorageByKey(key) {
 | 
			
		||||
  const encodedValue = localStorage.getItem(key)
 | 
			
		||||
  if (encodedValue == null) {
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
  const buff = new Buffer(encodedValue, 'base64')
 | 
			
		||||
  const value = buff.toString('utf-8')
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    return JSON.parse(value)
 | 
			
		||||
  } catch {
 | 
			
		||||
    return value
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ifMissingSettingsAndTokenExistsFetchSettings = 
 | 
			
		||||
  () => getLocalStorageByKey('token') ? getSettings() : null
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  namespaced: true,
 | 
			
		||||
  state: {
 | 
			
		||||
    admin: false,
 | 
			
		||||
    settings: undefined,
 | 
			
		||||
    username: undefined,
 | 
			
		||||
    plex_userid: undefined
 | 
			
		||||
  },
 | 
			
		||||
  getters: {
 | 
			
		||||
    admin: (state) => {
 | 
			
		||||
      return state.admin
 | 
			
		||||
    },
 | 
			
		||||
    settings: (state, foo, bar) => {
 | 
			
		||||
      console.log('is this called?')
 | 
			
		||||
      const settings = state.settings || getLocalStorageByKey('settings')
 | 
			
		||||
      if (settings instanceof Object) {
 | 
			
		||||
        return settings
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ifMissingSettingsAndTokenExistsFetchSettings()
 | 
			
		||||
      return undefined
 | 
			
		||||
    },
 | 
			
		||||
    username: (state) => {
 | 
			
		||||
      const settings = state.settings || getLocalStorageByKey('settings')
 | 
			
		||||
 | 
			
		||||
      if (settings instanceof Object && settings.hasOwnProperty('user_name')) {
 | 
			
		||||
        return settings.user_name
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ifMissingSettingsAndTokenExistsFetchSettings()
 | 
			
		||||
      return undefined
 | 
			
		||||
    },
 | 
			
		||||
    plex_userid: (state) => {
 | 
			
		||||
      const settings = state.settings || getLocalStorageByKey('settings')
 | 
			
		||||
      console.log('plex_userid from store', settings)
 | 
			
		||||
 | 
			
		||||
      if (settings instanceof Object && settings.hasOwnProperty('plex_userid')) {
 | 
			
		||||
        return settings.plex_userid
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ifMissingSettingsAndTokenExistsFetchSettings()
 | 
			
		||||
      return undefined
 | 
			
		||||
    },
 | 
			
		||||
    isPlexAuthenticated: (state) => {
 | 
			
		||||
      const settings = state.settings || getLocalStorageByKey('settings')
 | 
			
		||||
      if (settings == null)
 | 
			
		||||
        return false
 | 
			
		||||
 | 
			
		||||
      const hasPlexId = settings['plex_userid']
 | 
			
		||||
      return hasPlexId != null ? true : false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mutations: {
 | 
			
		||||
    SET_ADMIN: (state, isAdmin) => {
 | 
			
		||||
      state.admin = isAdmin
 | 
			
		||||
    },
 | 
			
		||||
    SET_USERNAME: (state, username) => {
 | 
			
		||||
      state.username = username
 | 
			
		||||
      console.log('username')
 | 
			
		||||
      setLocalStorageByKey('username', username)
 | 
			
		||||
    },
 | 
			
		||||
    SET_SETTINGS: (state, settings) => {
 | 
			
		||||
      state.settings = settings
 | 
			
		||||
      console.log('settings')
 | 
			
		||||
      setLocalStorageByKey('settings', settings)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  actions: {
 | 
			
		||||
    setAdmin: ({commit}, isAdmin) => {
 | 
			
		||||
      if (!(isAdmin instanceof Object)) {
 | 
			
		||||
        throw "Parameter is not a boolean value."
 | 
			
		||||
      }
 | 
			
		||||
      commit('SET_ADMIN', isAdmin)
 | 
			
		||||
    },
 | 
			
		||||
    setSettings: ({commit}, settings) => {
 | 
			
		||||
      console.log('settings input', settings)
 | 
			
		||||
      if (!(settings instanceof Object)) {
 | 
			
		||||
        throw "Parameter is not a object."
 | 
			
		||||
      }
 | 
			
		||||
      commit('SET_SETTINGS', settings)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,9 +11,16 @@ let routes = [
 | 
			
		||||
    path: '/',
 | 
			
		||||
    component: (resolve) => require(['./components/Home.vue'], resolve)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'activity',
 | 
			
		||||
    path: '/activity',
 | 
			
		||||
    meta: { requiresAuth: true },
 | 
			
		||||
    component: (resolve) => require(['./components/ActivityPage.vue'], resolve)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'profile',
 | 
			
		||||
    path: '/profile',
 | 
			
		||||
    meta: { requiresAuth: true },
 | 
			
		||||
    component: (resolve) => require(['./components/Profile.vue'], resolve)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@@ -41,11 +48,13 @@ let routes = [
 | 
			
		||||
  {
 | 
			
		||||
    name: 'settings',
 | 
			
		||||
    path: '/settings',
 | 
			
		||||
    meta: { requiresAuth: true },
 | 
			
		||||
    component: (resolve) => require(['./components/Settings.vue'], resolve)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'signin',
 | 
			
		||||
    path: '/signin',
 | 
			
		||||
    alias: '/login',
 | 
			
		||||
    component: (resolve) => require(['./components/Signin.vue'], resolve)
 | 
			
		||||
  },
 | 
			
		||||
  // {
 | 
			
		||||
@@ -60,6 +69,17 @@ let routes = [
 | 
			
		||||
    path: '/404',
 | 
			
		||||
    component: (resolve) => require(['./components/404.vue'], resolve)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'logout',
 | 
			
		||||
    path: '/logout',
 | 
			
		||||
    component: {
 | 
			
		||||
      template: '<div></div>',
 | 
			
		||||
      created() {
 | 
			
		||||
        localStorage.clear();
 | 
			
		||||
        this.$router.push({ name: 'home' });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '*',
 | 
			
		||||
    redirect: '/'
 | 
			
		||||
@@ -71,7 +91,7 @@ let routes = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router =  new VueRouter({
 | 
			
		||||
  mode: 'hash',
 | 
			
		||||
  mode: 'history',
 | 
			
		||||
  base: '/',
 | 
			
		||||
  routes,
 | 
			
		||||
  linkActiveClass: 'is-active'
 | 
			
		||||
@@ -85,6 +105,13 @@ router.beforeEach((to, from, next) => {
 | 
			
		||||
    document.querySelector('.nav__hamburger').classList.remove('nav__hamburger--active');
 | 
			
		||||
    document.querySelector('.nav__list').classList.remove('nav__list--active');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (to.matched.some(record => record.meta.requiresAuth)) {
 | 
			
		||||
    if (localStorage.getItem('token') == null) {
 | 
			
		||||
      next({ path: '/signin' });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
 | 
			
		||||
.noselect {
 | 
			
		||||
  -webkit-touch-callout: none; /* iOS Safari */
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
@@ -22,3 +21,29 @@
 | 
			
		||||
    margin-left: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex {
 | 
			
		||||
  display: flex;
 | 
			
		||||
 | 
			
		||||
  &-direction-column {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &-direction-row {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &-align-items-center {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.position {
 | 
			
		||||
  &-relative {
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &-absolute {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,31 @@ $tablet-p-width: 768px;
 | 
			
		||||
$tablet-l-width: 1024px;
 | 
			
		||||
$desktop-width:  1200px;
 | 
			
		||||
$desktop-l-width: 1600px;
 | 
			
		||||
$mobile-width: 768px;
 | 
			
		||||
 | 
			
		||||
@mixin desktop {
 | 
			
		||||
	@media (min-width: #{$mobile-width + 1px}) {
 | 
			
		||||
    @content;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin mobile {
 | 
			
		||||
	@media (max-width: #{$mobile-width}) {
 | 
			
		||||
    @content;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.desktop-only {
 | 
			
		||||
	@include mobile {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mobile-only {
 | 
			
		||||
	@include desktop {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Media
 | 
			
		||||
@mixin mobile-only{
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,15 @@
 | 
			
		||||
  --text-color-secondary: orange;
 | 
			
		||||
  --background-color: #f8f8f8;
 | 
			
		||||
  --background-color-secondary: #ffffff;
 | 
			
		||||
  --background-ui: #edeef0;
 | 
			
		||||
  --background-95: rgba(255, 255, 255, 0.95);
 | 
			
		||||
  --background-70: rgba(255, 255, 255, 0.7);
 | 
			
		||||
  --background-40: rgba(255, 255, 255, 0.4);
 | 
			
		||||
  --background-nav-logo: #081c24;
 | 
			
		||||
 | 
			
		||||
  --background-nav-logo: #081c24;
 | 
			
		||||
  --color-green: #01d277;
 | 
			
		||||
  --color-green-90: rgba(1, 210, 119, .9);
 | 
			
		||||
  --color-green-90: rgba(1, 210, 119, 0.9);
 | 
			
		||||
  --color-green-70: rgba(1, 210, 119, 0.73);
 | 
			
		||||
  --color-teal: #091c24;
 | 
			
		||||
  --color-black: #081c24;
 | 
			
		||||
  --white: #fff;
 | 
			
		||||
@@ -29,7 +31,7 @@
 | 
			
		||||
  --color-success-text: #fff;
 | 
			
		||||
  --color-success-highlight: rgb(0, 100, 66);
 | 
			
		||||
  --color-error: rgba(220, 48, 35, 0.8);
 | 
			
		||||
  --color-error-highlight: #DC3023;
 | 
			
		||||
  --color-error-highlight: #dc3023;
 | 
			
		||||
 | 
			
		||||
  --header-size: 75px;
 | 
			
		||||
}
 | 
			
		||||
@@ -42,17 +44,18 @@
 | 
			
		||||
    --text-color-50: rgba(255, 255, 255, 0.5);
 | 
			
		||||
    --text-color-5: rgba(255, 255, 255, 0.05);
 | 
			
		||||
    --text-color-secondary: orange;
 | 
			
		||||
    --background-color: #1e1f22;
 | 
			
		||||
    --background-color-secondary: #111111;
 | 
			
		||||
    --background-95: rgba(30, 31, 34, 0.95);
 | 
			
		||||
    --background-70: rgba(30, 31, 34, 0.8);
 | 
			
		||||
    --background-40: rgba(30, 31, 34, 0.4);
 | 
			
		||||
    --background-color: rgba(17, 17, 17, 1);
 | 
			
		||||
    --background-color-secondary: rgba(6, 7, 8, 1);
 | 
			
		||||
    --background-ui: #202125;
 | 
			
		||||
    --background-95: rgba(17, 17, 17, 0.95);
 | 
			
		||||
    --background-70: rgba(17, 17, 17, 0.8);
 | 
			
		||||
    --background-40: rgba(17, 17, 17, 0.4);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@include mobile-only {
 | 
			
		||||
  :root {
 | 
			
		||||
    --header-size: 50px;
 | 
			
		||||
    --header-size: calc(50px + 1.5rem);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -61,6 +64,7 @@ $header-size: var(--header-size);
 | 
			
		||||
$dark: rgb(30, 31, 34);
 | 
			
		||||
$green: var(--color-green);
 | 
			
		||||
$green-90: var(--color-green-90);
 | 
			
		||||
$green-70: var(--color-green-70);
 | 
			
		||||
$teal: #091c24;
 | 
			
		||||
$black: #081c24;
 | 
			
		||||
$black-80: rgba(0, 0, 0, 0.8);
 | 
			
		||||
@@ -74,6 +78,7 @@ $text-color-5: var(--text-color-5) !default;
 | 
			
		||||
$text-color-secondary: var(--text-color-secondary) !default;
 | 
			
		||||
$background-color: var(--background-color) !default;
 | 
			
		||||
$background-color-secondary: var(--background-color-secondary) !default;
 | 
			
		||||
$background-ui: var(--background-ui) !default;
 | 
			
		||||
$background-95: var(--background-95) !default;
 | 
			
		||||
$background-70: var(--background-70) !default;
 | 
			
		||||
$background-40: var(--background-40) !default;
 | 
			
		||||
@@ -99,11 +104,12 @@ $color-error-highlight: var(--color-error-highlight) !default;
 | 
			
		||||
  --text-color-50: rgba(255, 255, 255, 0.5);
 | 
			
		||||
  --text-color-5: rgba(255, 255, 255, 0.05);
 | 
			
		||||
  --text-color-secondary: orange;
 | 
			
		||||
  --background-color: #1e1f22;
 | 
			
		||||
  --background-color-secondary: #111111;
 | 
			
		||||
  --background-95: rgba(30, 31, 34, 0.95);
 | 
			
		||||
  --background-70: rgba(30, 31, 34, 0.7);
 | 
			
		||||
  --color-teal: #091c24;
 | 
			
		||||
  --background-color: rgba(17, 17, 17, 1);
 | 
			
		||||
  --background-color-secondary: rgba(6, 7, 8, 1);
 | 
			
		||||
  --background-ui: #202125;
 | 
			
		||||
  --background-95: rgba(17, 17, 17, 0.95);
 | 
			
		||||
  --background-70: rgba(17, 17, 17, 0.8);
 | 
			
		||||
  --background-40: rgba(17, 17, 17, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.light {
 | 
			
		||||
@@ -111,13 +117,11 @@ $color-error-highlight: var(--color-error-highlight) !default;
 | 
			
		||||
  --text-color-70: rgba(8, 28, 36, 0.7);
 | 
			
		||||
  --text-color-50: rgba(8, 28, 36, 0.5);
 | 
			
		||||
  --text-color-5: rgba(8, 28, 36, 0.05);
 | 
			
		||||
  --text-color-inverted: #fff;
 | 
			
		||||
  --text-color-secondary: orange;
 | 
			
		||||
  --background-color: #f8f8f8;
 | 
			
		||||
  --background-color-secondary: #ffffff;
 | 
			
		||||
  --background-ui: #edeef0;
 | 
			
		||||
  --background-95: rgba(255, 255, 255, 0.95);
 | 
			
		||||
  --background-70: rgba(255, 255, 255, 0.7);
 | 
			
		||||
  --background-nav-logo: #081c24;
 | 
			
		||||
  --color-green: #01d277;
 | 
			
		||||
  --color-teal: #091c24;
 | 
			
		||||
  --background-40: rgba(255, 255, 255, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,19 @@
 | 
			
		||||
import Vue from 'vue'
 | 
			
		||||
import Vuex from 'vuex'
 | 
			
		||||
 | 
			
		||||
import torrentModule from './modules/torrentModule'
 | 
			
		||||
import darkmodeModule from './modules/darkmodeModule'
 | 
			
		||||
import documentTitle from './modules/documentTitle'
 | 
			
		||||
import torrentModule from './modules/torrentModule'
 | 
			
		||||
import userModule from './modules/userModule'
 | 
			
		||||
 | 
			
		||||
Vue.use(Vuex)
 | 
			
		||||
 | 
			
		||||
const store = new Vuex.Store({
 | 
			
		||||
  modules: {
 | 
			
		||||
    torrentModule,
 | 
			
		||||
    darkmodeModule,
 | 
			
		||||
    documentTitle
 | 
			
		||||
    documentTitle,
 | 
			
		||||
    torrentModule,
 | 
			
		||||
    userModule
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								src/utils.js
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								src/utils.js
									
									
									
									
									
								
							@@ -7,7 +7,17 @@ const sortableSize = (string) => {
 | 
			
		||||
 | 
			
		||||
  const exponent = UNITS.indexOf(unit) * 3
 | 
			
		||||
	return numStr * (Math.pow(10, exponent))
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const parseJwt = (token) => {
 | 
			
		||||
    var base64Url = token.split('.')[1];
 | 
			
		||||
    var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
 | 
			
		||||
    var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
 | 
			
		||||
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
 | 
			
		||||
    }).join(''));
 | 
			
		||||
 | 
			
		||||
    return JSON.parse(jsonPayload);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export { sortableSize }
 | 
			
		||||
export { sortableSize, parseJwt }
 | 
			
		||||
		Reference in New Issue
	
	Block a user