59 Commits

Author SHA1 Message Date
2665a27803 Lift mobile navigation some for chin height 2022-01-09 16:23:29 +01:00
74b96225c6 Testing out bottom align mobile navigation content 2022-01-09 16:00:02 +01:00
f180b7f39b Updated header text and font size 2022-01-09 15:58:04 +01:00
a2fbfcb13c Removed /dist prefix from built js file 2022-01-03 20:29:57 +01:00
d640f7f882 Removed /dist prefix from all image paths 2022-01-03 20:29:03 +01:00
d43c12b103 Prettierrc file 2022-01-03 17:50:55 +01:00
38c3792675 Add 'is-loaded' class after image intersects viewport 2022-01-03 17:50:12 +01:00
ac2785abd5 Increased opacity delay 2022-01-03 17:49:35 +01:00
1ff6a0e831 Linting 2022-01-03 17:49:22 +01:00
7a3b709404 Update router to use history not hash mode. 2021-05-18 10:21:00 +02:00
KevinMidboe
d63cb4ac52 Merge branch 'master' of github.com:kevinmidboe/seasoned 2020-04-09 23:01:25 +02:00
b6ee1cf906 Profile replaces route with query settings=true when enabled. 2020-04-09 23:00:50 +02:00
60201b1b67 Login and register pages now checks inputs for errors. throwError parameter on login and register functions allows us to receive the request object not just the decoded json. 2020-04-09 21:39:29 +02:00
a8b8603649 /login is alias of signin component. 2020-04-09 20:59:49 +02:00
e193528fe9 Routes with meta requiresAuth redirects to login page if token not set in localstorage 2020-04-09 20:58:58 +02:00
73afb34964 Logout route that clears localstorage for anything set clientside. 2020-04-09 20:53:24 +02:00
65bbc453e6 seasoned messages looks better when messages contains only title. 2020-04-09 20:27:11 +02:00
KevinMidboe
188477ab64 404 page now has button to navigate to previous page. 2020-04-09 19:58:50 +02:00
KevinMidboe
a31bfb6b39 Updated seasonedbutton to not have a wrapping div. 2020-04-09 19:55:57 +02:00
681ed69ef0 Removed padding on right side of search input and removed unused comment. 2020-02-25 13:44:48 +01:00
b771428b4d Changed placeholder for earch input 2020-02-25 13:44:31 +01:00
fc0103ee5d Change the document title prefix from request to seasoned 2020-02-25 12:12:07 +01:00
55067b81b8 Merge branch 'master' of github.com:KevinMidboe/seasoned 2020-02-25 12:09:45 +01:00
dfe2b5df09 Removed default emoji prefix of document title. 2020-02-25 12:09:13 +01:00
dc0c435163 If settings dont exist, return false for isAuthenticated. 2020-02-21 23:03:31 +01:00
9d1ac56b9a Also check localstorage for settings if not found in state. 2020-02-21 22:58:49 +01:00
fc2c3664d9 Resolved merge conflict. 2020-02-21 22:52:36 +01:00
0bd45ed777 New sidebarelement for users that are logged inn. Now they can be redirected directly to the movie in plex. 2020-02-21 22:51:39 +01:00
3912766982 Reverted active logic for seasonedButton. 2020-02-20 14:09:08 +01:00
3becce2a6c Moved isPlexAuthenticated from movie component to userModule. 2020-02-20 14:08:46 +01:00
20b8692c91 Forgot to toggle isActive when clicked. 2020-02-20 13:56:56 +01:00
14ac780aa5 Should not overwrite prop data. Copy and set to internal data attribute. 2020-02-20 13:55:04 +01:00
d836870612 Toggle active boolean to set class on buttons. 2020-02-20 13:41:39 +01:00
bc6f706e4a New mediaquery to check if hover is available then only style hover when it is. This solves sticky hover styling on mobile. 2020-02-20 13:33:08 +01:00
6ac6a9b039 Readded noselect class to description. 2020-02-20 10:41:46 +01:00
85be80d712 Removed unused code for poster image. 2020-02-20 10:41:21 +01:00
105be1e411 noselect was not the issue, bug in css-loader. 2020-02-20 00:47:55 +01:00
010830243e noselect class was preventing taps on mobile. 2020-02-20 00:25:59 +01:00
923dc46dc7 Removed setTimeout 2020-02-20 00:24:34 +01:00
f2ef5366f5 Merge pull request #48 from KevinMidboe/refactor/image-loading
Refactor/image loading
2020-02-20 00:22:14 +01:00
20380a4587 Merge branch 'master' into refactor/image-loading 2020-02-20 00:21:43 +01:00
069ef2c458 Cleaned up some css, better loading of backdrop, simplified DOM, more meta data for tvshows and added truncating of description. 2020-02-20 00:19:08 +01:00
2f430b2d8f Cleaned up some of the styling for movieslistitem. 2020-02-19 23:54:20 +01:00
f7a579a438 IntersecrionObserver checks ref intersection when mounted. 2020-02-19 23:53:51 +01:00
b9ddd998bc When type person show known for department. 2020-02-19 23:52:25 +01:00
ae59d02df2 Poster image dom simplified. 2020-02-19 23:52:03 +01:00
ec205bab0c Update .drone.yml 2020-02-07 01:19:08 +01:00
ed49d825b8 Merge pull request #44 from KevinMidboe/feature/searchFiltering
Feature/search filtering
2020-01-31 22:51:21 +01:00
a9db8be46a Removed duplicated top_rated icon. 2020-01-31 22:32:12 +01:00
1caa3c7fae Removed unsued comments and added alt tag to images 2020-01-31 22:27:45 +01:00
2ea4bffd49 Update .drone.yml 2020-01-31 22:21:49 +01:00
5ae52f59fc Merge pull request #47 from KevinMidboe/feature/lazy-loading-images
Lazy loading for list items.
2020-01-31 22:18:42 +01:00
a7e6d25d3f Lazy loading for list items.
This is somewhat inefficient because each list item has its own instance
of a intersectionObserver.
Improvements include:
- Poster has placeholder image as source from mount
- When component mounts we attach the observer
- When observerd in viewport find
  - Find the correct image height based on the placeholders height
  - Change src to dynamic poster url
2020-01-31 22:14:13 +01:00
83751a4e3e Update .drone.yml 2020-01-20 19:15:42 +01:00
0e9daab187 Removed unused raven link under body 2020-01-20 19:10:45 +01:00
4390491873 Update .drone.yml 2020-01-20 19:07:48 +01:00
d620a4cc2e Update .drone.yml 2020-01-20 19:07:00 +01:00
1fd48edd42 Set default adult value to true. 2019-12-27 23:31:20 +01:00
68e45303c6 Filtering for search in autocomplete dropdown.
- Accessibility
 - Tabindex updated for search <input> to have priority over nav items.
 - Aria label
- Search icon clickable for searching.
- Filter for adult and searchType.
- When clicking a autocomplete search result, the clicked item is set as
selectedResult.
- Remove duplicates from elastic search result.
- Added filter parameters to our $router.push function.
2019-12-27 22:18:45 +01:00
29 changed files with 1457 additions and 1467 deletions

View File

@@ -1,7 +1,7 @@
---
kind: pipeline
type: exec
name: default
type: docker
name: seasoned build
platform:
os: linux
@@ -9,11 +9,30 @@ platform:
steps:
- name: frontend_install
image: node:13.6.0
commands:
- yarn
- name: frontend_build
commands:
- yarn build
- 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:
@@ -21,3 +40,5 @@ trigger:
event:
include:
- pull_request
- push

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false,
"trailingComma": "none"
}

View File

@@ -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>

View File

@@ -30,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",

View File

@@ -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>
<search-input v-model="query"></search-input>
<!-- 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,39 +35,42 @@ export default {
},
data() {
return {
query: '',
query: "",
moviePopupIsVisible: false,
popupID: 0,
popupType: 'movie'
}
popupType: "movie"
};
},
created(){
let that = this
created() {
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')
this.moviePopupIsVisible = false;
console.log("closed");
}
}
console.log('MoviePopup registered at this.$popup and has state: ', this.$popup.isOpen)
};
console.log(
"MoviePopup registered at this.$popup and has state: ",
this.$popup.isOpen
);
}
}
};
</script>
<style lang="scss" scoped>
@import "./src/scss/media-queries";
@import "./src/scss/variables";
.content {
@include tablet-min{
@include tablet-min {
width: calc(100% - 95px);
margin-top: $header-size;
margin-left: 95px;
@@ -86,38 +84,42 @@ export default {
@import "./src/scss/variables";
@import "./src/scss/media-queries";
*{
* {
box-sizing: border-box;
}
html {
height: 100%;
}
body{
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;
&.hidden{
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{
figure {
padding: 0;
margin: 0;
}
img{
img {
display: block;
// max-width: 100%;
height: auto;
@@ -127,16 +129,16 @@ img{
overflow: hidden;
}
.wrapper{
.wrapper {
position: relative;
}
.header{
.header {
position: fixed;
z-index: 15;
display: flex;
flex-direction: column;
@include tablet-min{
@include tablet-min {
width: calc(100% - 170px);
margin-left: 95px;
border-top: 0;
@@ -146,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>

View File

@@ -252,6 +252,21 @@ const getRequestStatus = (id, type, authorization_token=undefined) => {
.catch(err => Promise.reject(err))
}
const watchLink = (title, year, authorization_token=undefined) => {
const url = new URL('v1/plex/watch-link', SEASONED_URL)
url.searchParams.append('title', title)
url.searchParams.append('year', year)
const headers = {
'Authorization': authorization_token,
'Content-Type': 'application/json'
}
return fetch(url.href, { headers })
.then(resp => resp.json())
.then(response => response.link)
}
// - - - Seasoned user endpoints - - -
const register = (username, password) => {
@@ -271,7 +286,7 @@ const register = (username, password) => {
})
}
const login = (username, password) => {
const login = (username, password, throwError=false) => {
const url = new URL('v1/user/login', SEASONED_URL)
const options = {
method: 'POST',
@@ -280,11 +295,14 @@ const login = (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
.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);
})
}
@@ -458,6 +476,7 @@ export {
searchTorrents,
addMagnet,
request,
watchLink,
getRequestStatus,
linkPlexAccount,
unlinkPlexAccount,

View File

@@ -1,38 +1,74 @@
<template>
<section class="not-found">
<h1 class="not-found__title">Page Not Found</h1>
</section>
<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;
}
}
}

View File

@@ -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(){
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()
created() {
this.fetchRequests();
this.fetchNowPlaying();
this.fetchUpcoming();
this.fetchPopular();
}
}
};
</script>

View File

@@ -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,15 +57,15 @@ 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 {
font-weight: 500;
font-size: 22px;
@@ -72,8 +74,8 @@ header {
color: $text-color;
margin: 0;
@include tablet-min{
font-size: 28px;
@include tablet-min {
font-size: 2.5rem;
}
}
@@ -84,9 +86,9 @@ header {
color: $text-color-70;
margin: 5px 0;
@include tablet-min{
font-size: 16px;
@include tablet-min {
font-size: 1.3rem;
}
}
}
</style>
</style>

View File

@@ -1,15 +1,20 @@
<template>
<header :class="{ 'sticky': sticky }">
<header :class="{ sticky: sticky }">
<h2>{{ title }}</h2>
<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}`">
<router-link
v-if="link"
:to="link"
class="view-more"
:aria-label="`View all ${title}`"
>
View All
</router-link>
</header>
</header>
</template>
<script>
@@ -33,31 +38,38 @@ export default {
required: false
}
}
}
};
</script>
<style lang="scss" scoped>
@import './src/scss/variables';
@import './src/scss/media-queries';
@import './src/scss/main';
@import "./src/scss/variables";
@import "./src/scss/media-queries";
@import "./src/scss/main";
header {
width: 100%;
min-height: 80px;
min-height: 45px;
display: flex;
justify-content: space-between;
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;
@include tablet-min {
top: $header-size;
}
}
h2 {
@@ -72,16 +84,16 @@ header {
.view-more {
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{
&:after {
content: " →";
}
&:hover{
&:hover {
color: $text-color;
}
}
@@ -89,18 +101,17 @@ header {
.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>
</style>

View File

@@ -1,6 +1,5 @@
<template>
<div>
<div class="page-container">
<list-header :title="listTitle" :info="info" :sticky="true" />
<results-list :results="results" v-if="results" />
@@ -13,96 +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,
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]
if (this.results.length === 0) return [null, null];
return [this.pageCount, this.resultCount];
},
resultCount() {
const loadedResults = this.results.length
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
return `${loadedResults} of ${totalResults} results`
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}`
return `Page ${this.page} of ${this.totalPages}`;
}
},
methods: {
loadMore() {
console.log(this.$route)
console.log(this.$route);
this.loading = true;
this.page++
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
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;

View File

@@ -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">
<figure class="movie__poster">
<img v-if="movie && poster === null"
class="movies-item__img is-loaded"
alt="movie poster image"
src="~assets/no-image.png">
<img v-else-if="poster === undefined"
class="movies-item__img grey"
alt="movie poster image">
<!-- src="~assets/placeholder.png"> -->
<img v-else
class="movies-item__img is-loaded"
alt="movie poster image"
:src="ASSET_URL + ASSET_SIZES[0] + poster">
</figure>
<header
ref="header"
:class="compact ? 'compact' : ''"
@click="compact = !compact"
>
<figure class="movie__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>
<loading-placeholder v-else :count="1" />
</div>
</div>
<h1 class="movie__title" v-if="movie">{{ movie.title }}</h1>
<loading-placeholder v-else :count="1" />
</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 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>
<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, getPerson, 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'],
data() {
return {
ASSET_URL: "https://image.tmdb.org/t/p/",
ASSET_SIZES: ["w500", "w780", "original"],
movie: undefined,
title: undefined,
poster: undefined,
@@ -140,92 +194,201 @@ export default {
matched: false,
userLoggedIn: storage.sessionId ? true : false,
requested: false,
admin: localStorage.getItem('admin') == "true" ? true : false,
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.exists_in_plex || false
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) {
return data.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') {
id: function (val) {
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"];
}
},
beforeDestroy() {
store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle)
},
created(){
this.prevDocumentTitle = store.getters['documentTitle/title']
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));
if (this.type === 'movie') {
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)
this.$router.push({ name: "404" });
});
} else if (this.type == "person") {
getPerson(this.id, true)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
this.$router.push({ name: "404" });
});
} else {
getShow(this.id, true)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
this.$router.push({ name: "404" });
});
}
console.log('admin: ', this.admin)
},
beforeDestroy() {
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 {
@@ -238,7 +401,7 @@ export default {
display: flex;
flex-wrap: wrap;
flex-direction: column;
@include tablet-min{
@include tablet-min {
flex-direction: row;
}
@@ -246,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;
@@ -332,56 +452,67 @@ export default {
height: 100%;
}
&__actions {
text-align: center;
width: 100%;
order: 2;
padding: 20px;
border-top: 1px solid $text-color-5;
@include tablet-min {
order: 1;
width: 45%;
padding: 185px 0 40px 40px;
border-top: 0;
}
}
&__info {
width: 100%;
padding: 20px;
&__actions {
text-align: center;
width: 100%;
order: 2;
padding: 20px;
border-top: 1px solid $text-color-5;
@include tablet-min {
order: 1;
@include tablet-min {
order: 2;
padding: 40px;
width: 55%;
margin-left: 45%;
width: 45%;
padding: 185px 0 40px 40px;
border-top: 0;
}
}
&__info {
width: 100%;
padding: 20px;
order: 1;
@include tablet-min {
order: 2;
padding: 40px;
width: 55%;
margin-left: 45%;
}
}
&__info {
margin-left: 0;
}
&__description {
font-weight: 300;
font-size: 13px;
line-height: 1.8;
margin-bottom: 20px;
& .truncated {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
& + .truncate-toggle > i {
transform: rotateY(0deg) rotateZ(180deg);
}
}
&__info {
margin-left: 0;
@include tablet-min {
margin-bottom: 30px;
font-size: 14px;
}
&__description {
font-weight: 300;
font-size: 13px;
line-height: 1.8;
}
&__details {
display: flex;
flex-wrap: wrap;
> div {
margin-bottom: 20px;
margin-right: 20px;
@include tablet-min {
margin-bottom: 30px;
font-size: 14px;
margin-right: 30px;
}
}
&__details {
&-block {
float: left;
}
&-block:not(:last-child) {
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;
@@ -391,34 +522,35 @@ export default {
font-size: 16px;
}
}
&-text {
& .text {
font-weight: 300;
font-size: 14px;
margin-top: 5px;
}
}
&__admin {
}
&__admin {
width: 100%;
padding: 20px;
order: 2;
@include tablet-min {
order: 3;
padding: 40px;
padding-top: 0px;
width: 100%;
padding: 20px;
order: 2;
@include tablet-min {
order: 3;
padding: 40px;
padding-top: 0px;
width: 100%;
}
&-title {
margin: 0;
font-weight: 400;
text-transform: uppercase;
text-align: center;
font-size: 14px;
color: $green;
padding-bottom: 20px;
@include tablet-min {
font-size: 16px;
}
}
}
&-title {
margin: 0;
font-weight: 400;
text-transform: uppercase;
text-align: center;
font-size: 14px;
color: $green;
padding-bottom: 20px;
@include tablet-min {
font-size: 16px;
}
}
}
}
</style>

View File

@@ -1,122 +1,207 @@
<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="">
<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="movies-item__content">
<p class="movies-item__title">{{ movie.title || movie.name }}</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>
</a>
</figure>
<div class="movie-item__info">
<p v-if="movie.title || movie.name">{{ movie.title || movie.name }}</p>
<p v-if="movie.year">{{ movie.year }}</p>
<p v-if="movie.type == 'person'">
Known for: {{ movie.known_for_department }}
</p>
</div>
</li>
</template>
<script>
import img from '../directives/v-image'
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(){
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;
transition: background-color 0.5s ease;
@include tablet-min{
@include tablet-min {
padding: 15px;
width: 33%;
}
@include tablet-landscape-min{
@include tablet-landscape-min {
padding: 15px;
width: 25%;
}
@include desktop-min{
@include desktop-min {
padding: 15px;
width: 20%;
}
@include desktop-lg-min{
@include desktop-lg-min {
padding: 15px;
width: 12.5%;
}
&__link{
&:hover &__info > p {
color: $text-color;
}
&__poster {
text-decoration: none;
color: $text-color-70;
font-weight: 300;
> img {
width: 100%;
opacity: 0;
transform: scale(0.97) translateZ(0);
transition: opacity 1s ease, transform 0.5s ease;
&.is-loaded {
opacity: 1;
transform: scale(1);
}
&:hover {
transform: scale(1.03);
box-shadow: 0 0 10px rgba($dark, 0.1);
}
}
}
&__content{
&__info {
padding-top: 15px;
}
&__poster{
transition: transform 0.5s ease, box-shadow 0.3s ease;
transform: translateZ(0);
}
&__img{
width: 100%;
opacity: 0;
transform: scale(0.97) translateZ(0);
transition: opacity 0.5s ease, transform 0.5s ease;
&.is-loaded{
opacity: 1;
transform: scale(1);
font-weight: 300;
> p {
color: $text-color-70;
margin: 0;
font-size: 11px;
letter-spacing: 0.5px;
transition: color 0.5s ease;
cursor: pointer;
@include mobile-ls-min {
font-size: 12px;
}
@include tablet-min {
font-size: 14px;
}
}
}
&__link:not(.no-image):hover &__poster{
transform: scale(1.03);
box-shadow: 0 0 10px rgba($dark, 0.1);
}
.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;
}
&__title{
margin: 0;
font-size: 11px;
letter-spacing: 0.5px;
transition: color 0.5s ease;
cursor: pointer;
@include mobile-ls-min{
font-size: 12px;
}
@include tablet-min{
font-size: 14px;
}
}
&__link:hover &__title{
color: $text-color;
&:hover {
transform: scale(1);
}
}
</style>
@@ -155,11 +240,10 @@ export default {
progress::-webkit-progress-value {
background-color: $green-70;
border-radius: 4px;
}
progress::-moz-progress-bar {
/* style rules */
background-color: green;
}
}
</style>
</style>

View File

@@ -1,78 +1,93 @@
<template>
<div>
<nav class="nav">
<router-link class="nav__logo" :to="{name: 'home'}" exact title="Vue.js — TMDb App">
<svg class="nav__logo-image">
<use xlink:href="#svgLogo"></use>
</svg>
</router-link>
<nav class="nav">
<router-link
class="nav__logo"
:to="{ name: 'home' }"
exact
title="Vue.js — TMDb App"
>
<svg class="nav__logo-image">
<use xlink:href="#svgLogo"></use>
</svg>
</router-link>
<div class="nav__hamburger" @click="toggleNav">
<div v-for="_ in 3" class="bar"></div>
</div>
<div class="nav__hamburger" @click="toggleNav">
<div v-for="_ in 3" class="bar"></div>
</div>
<ul class="nav__list">
<li class="nav__item" v-for="item in listTypes">
<router-link class="nav__link" :to="'/list/' + item.route">
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use :xlink:href="'#icon_' + item.route"></use>
</svg>
<span class="nav__link-title">{{ item.title }}</span>
</div>
</router-link>
</li>
<ul class="nav__list">
<li class="nav__item" v-for="item in listTypes">
<router-link class="nav__link" :to="'/list/' + item.route">
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use :xlink:href="'#icon_' + item.route"></use>
</svg>
<span class="nav__link-title">{{ item.title }}</span>
</div>
</router-link>
</li>
<li class="nav__item nav__item--profile">
<router-link class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn">
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use>
</svg>
<span class="nav__link-title">Sign in</span>
</div>
</router-link>
<li class="nav__item mobile-only"></li>
<router-link class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn">
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use>
</svg>
<span class="nav__link-title">Profile</span>
</div>
</router-link>
</li>
</ul>
</nav>
<li class="nav__item nav__item--profile">
<router-link
class="nav__link nav__link--profile"
:to="{ name: 'signin' }"
v-if="!userLoggedIn"
>
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use>
</svg>
<span class="nav__link-title">Sign in</span>
</div>
</router-link>
<div class="spacer"></div>
</div>
<router-link
class="nav__link nav__link--profile"
:to="{ name: 'profile' }"
v-if="userLoggedIn"
>
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use>
</svg>
<span class="nav__link-title">Profile</span>
</div>
</router-link>
</li>
</ul>
</nav>
</template>
<script>
import storage from '@/storage'
import storage from "@/storage";
export default {
data(){
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;
setUserStatus() {
this.userLoggedIn = localStorage.getItem("token") ? true : false;
},
toggleNav(){
document.querySelector('.nav__hamburger').classList.toggle('nav__hamburger--active');
document.querySelector('.nav__list').classList.toggle('nav__list--active');
toggleNav() {
document
.querySelector(".nav__hamburger")
.classList.toggle("nav__hamburger--active");
document
.querySelector(".nav__list")
.classList.toggle("nav__list--active");
}
},
created(){
created() {
// TODO move this to state manager
eventHub.$on('setUserStatus', this.setUserStatus);
eventHub.$on("setUserStatus", this.setUserStatus);
}
}
};
</script>
<style lang="scss" scoped>
@@ -83,45 +98,45 @@ 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{
@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{
&-image {
width: 35px;
height: 31px;
fill: $green;
transition: transform 0.5s ease;
@include tablet-min{
@include tablet-min {
width: 45px;
height: 40px;
}
@@ -135,12 +150,12 @@ export default {
position: fixed;
width: 55px;
height: 50px;
top: 0;
bottom: 1.5rem;
right: 0;
cursor: pointer;
z-index: 10;
border-left: 1px solid $background-color;
@include tablet-min{
@include tablet-min {
display: none;
}
.bar {
@@ -172,9 +187,9 @@ export default {
}
}
&--active {
.bar{
.bar {
&:nth-child(1),
&:nth-child(3){
&:nth-child(3) {
width: 0;
}
&:nth-child(2) {
@@ -198,15 +213,21 @@ 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{
&--active {
opacity: 1;
visibility: visible;
}
@@ -221,15 +242,15 @@ 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){
&:nth-child(odd) {
border-right: 1px solid $background-color;
&:last-child {
@@ -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;
}
}

View File

@@ -5,7 +5,7 @@
<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>
@@ -63,11 +63,15 @@ export default {
methods: {
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(){
@@ -76,6 +80,8 @@ export default {
} else {
this.userLoggedIn = true;
this.showSettings = window.location.toString().includes('settings=true')
getUserRequests()
.then(results => {
this.results = results.results

View File

@@ -2,17 +2,14 @@
<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>
@@ -34,57 +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) {
this.registerUser(username, password)
},
registerUser(username, password) {
register(username, password, true)
.then(data => {
if (data.success){
localStorage.setItem('token', data.token);
const jwtData = parseJwt(data.token)
localStorage.setItem('username', jwtData['username']);
localStorage.setItem('admin', jwtData['admin'] || false);
register(username, password)
.then(data => {
if (data.success){
localStorage.setItem('token', data.token);
localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin)
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
}
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
}
})
.catch(error => {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
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();

View File

@@ -1,15 +1,17 @@
<template>
<div>
<div class="page-container">
<list-header :title="title" :info="resultCount" :sticky="true" />
<results-list :results="results" />
<div v-if="page < totalPages" class="fullwidth-button">
<seasoned-button @click="loadMore">load more</seasoned-button>
</div>
<div class="notFound" v-if="results.length == 0 && loading == false">
<h1 class="notFound-title">No results for search: <b>{{ query }}</b></h1>
<h1 class="notFound-title">
No results for search: <b>{{ query }}</b>
</h1>
</div>
<loader v-if="loading" />
@@ -29,11 +31,11 @@
</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 },
@@ -58,59 +60,74 @@ export default {
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, adult=this.adult, mediaType=this.mediaType) {
searchTmdb(query, page, adult, mediaType)
.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, adult, media_type } = 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 || 1
this.adult = adult || this.adult
this.mediaType = media_type || this.mediaType
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;
@@ -118,5 +135,4 @@ export default {
display: flex;
justify-content: center;
}
</style>
</style>

View File

@@ -1,184 +1,290 @@
<template>
<div>
<div class="search">
<input
ref="input"
type="text"
placeholder="Search for a movie or show"
autocorrect="off"
autocapitalize="off"
v-model="query"
@input="handleInput"
@click="focus = true"
@keydown.escape="handleEscape"
@keyup.enter="handleSubmit"
@keydown.up="navigateUp"
@keydown.down="navigateDown" />
<svg class="search--icon" fill="currentColor"><use xlink:href="#iconSearch"></use></svg>
</div>
<!-- <div> -->
<div class="search">
<input
ref="input"
type="text"
placeholder="Search for movie or show"
aria-label="Search input for finding a movie or show"
autocorrect="off"
autocapitalize="off"
tabindex="1"
v-model="query"
@input="handleInput"
@click="focus = true"
@keydown.escape="handleEscape"
@keyup.enter="handleSubmit"
@keydown.up="navigateUp"
@keydown.down="navigateDown"
/>
<svg class="search-icon" fill="currentColor" @click="handleSubmit">
<use xlink:href="#iconSearch"></use>
</svg>
</div>
<!--
<transition name="fade">
<div class="dropdown" v-if="!disabled && focus && query.length > 0">
<div class="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) {
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
const textLength = input.value.length;
setTimeout(() => {
input.focus()
input.setSelectionRange(textLength, textLength + 1)
}, 1)
input.focus();
input.setSelectionRange(textLength, textLength + 1);
}, 1);
},
handleInput(e){
this.selectedResult = 0
this.$emit('input', this.query);
openResult(item, index) {
this.selectedResult = index;
this.$popup.open(item.id, item.type);
},
handleInput(e) {
this.selectedResult = 0;
this.$emit("input", this.query);
if (! this.focus) {
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"
};
}
})
console.log(this.elasticSearchResults)
})
});
results = this.removeDuplicates(results);
this.elasticSearchResults = results;
});
},
removeDuplicates(searchResults) {
let filteredResults = [];
searchResults.map(result => {
const numberOfDuplicates = filteredResults.filter(
filterItem => filterItem.id == result.id
);
if (numberOfDuplicates.length >= 1) {
return null;
}
filteredResults.push(result);
});
if (this.adult == false) {
filteredResults = filteredResults.filter(
result => result.adult == false
);
}
return filteredResults;
},
handleSubmit() {
let searchResults = this.elasticSearchResults
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;
@@ -196,7 +302,11 @@ export default {
width: calc(100%);
}
&--results {
.not-found {
font-weight: 400;
}
&-results {
padding-left: 60px;
width: 100%;
@@ -211,7 +321,7 @@ export default {
width: calc(100% - 25px);
max-width: fit-content;
list-style: none;
list-style: none;
color: rgba(0, 0, 0, 0.5);
text-transform: capitalize;
cursor: pointer;
@@ -222,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;
}
@@ -235,16 +347,16 @@ 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{
@include tablet-min {
position: relative;
width: 100%;
right: 0px;
@@ -252,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;
@@ -278,10 +393,10 @@ export default {
left: 15px;
top: 15px;
@include tablet-min{
@include tablet-min {
top: 27px;
left: 25px;
}
}
}
</style>
</style>

View File

@@ -5,14 +5,14 @@
<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="signin"/>
<seasoned-button @click="signin">sign in</seasoned-button>
<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>
@@ -39,11 +39,25 @@ export default {
setValue(l, t) {
this[l] = t
},
signin(){
submit() {
this.messages = [];
let username = this.username;
let password = this.password;
login(username, password)
if (username == null || username.length == 0) {
this.messages.push({ type: 'error', title: 'Missing username' })
return
}
if (password == null || password.length == 0) {
this.messages.push({ type: 'error', title: 'Missing password' })
return
}
this.signin(username, password)
},
signin(username, password) {
login(username, password, true)
.then(data => {
if (data.success){
const jwtData = parseJwt(data.token)
@@ -57,7 +71,7 @@ export default {
})
.catch(error => {
if (error.status === 401) {
this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
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 })

View File

@@ -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>

View File

@@ -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,14 +41,7 @@ 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>
@@ -70,7 +63,6 @@ export default {
.message {
width: 100%;
max-width: 35rem;
min-height: 75px;
display: flex;
margin-top: 1rem;
@@ -78,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;
@@ -91,10 +83,11 @@ 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 {
@@ -112,9 +105,8 @@ export default {
}
.pinstripe {
height: 100%;
width: 0.5rem;
// background-color: $color-error-highlight;
background-color: $color-error-highlight;
}
.dismiss {

View File

@@ -1,42 +1,38 @@
<template>
<div class="darkToggle">
<span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span>
</div>
</template>
<script>
export default {
data() {
return {
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
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;
@@ -49,9 +45,13 @@ export default {
right: 0;
z-index: 10;
@include mobile-only {
margin-bottom: 5rem;
}
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
</style>

View File

@@ -2,7 +2,7 @@
<div>
<a @click="$emit('click')">
<li>
<figure :class="activeClassIfActive">
<figure v-if="iconRef" :class="activeClassIfActive">
<svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg>
</figure>
@@ -23,7 +23,7 @@ export default {
props: {
iconRef: {
type: String,
required: true
required: false
},
iconRefActive: {
type: String,
@@ -85,11 +85,11 @@ li {
border-bottom: 1px solid $text-color-5;
&:hover {
color: $text-color-70;
color: $text-color;
cursor: pointer;
.icon {
fill: $text-color-70;
fill: $text-color;
cursor: pointer;
transform: scale(1.1, 1.1);
}

View File

@@ -11,8 +11,8 @@ const setDocumentTitle = (state) => {
export default {
namespaced: true,
state: {
emoji: '🍕',
titlePrefix: 'request',
emoji: '',
titlePrefix: 'seasoned',
title: undefined
},
getters: {

View File

@@ -69,6 +69,14 @@ export default {
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: {
@@ -101,4 +109,4 @@ export default {
commit('SET_SETTINGS', settings)
}
}
}
}

View File

@@ -14,11 +14,13 @@ let routes = [
{
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)
},
{
@@ -46,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)
},
// {
@@ -65,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: '/'
@@ -76,7 +91,7 @@ let routes = [
];
const router = new VueRouter({
mode: 'hash',
mode: 'history',
base: '/',
routes,
linkActiveClass: 'is-active'
@@ -90,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();
});

View File

@@ -1,11 +1,10 @@
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently */
}
.end-section {
@@ -47,4 +46,4 @@
&-absolute {
position: absolute;
}
}
}

View File

@@ -18,12 +18,12 @@
--background-nav-logo: #081c24;
--color-green: #01d277;
--color-green-90: rgba(1, 210, 119, .9);
--color-green-70: rgba(1, 210, 119, .73);
--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;
--white-70: rgba(255,255,255,0.7);
--white-70: rgba(255, 255, 255, 0.7);
--color-warning: rgba(241, 188, 53, 0.7);
--color-warning-highlight: #f1bc35;
@@ -31,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;
}
@@ -55,7 +55,7 @@
@include mobile-only {
:root {
--header-size: 50px;
--header-size: calc(50px + 1.5rem);
}
}
@@ -67,9 +67,9 @@ $green-90: var(--color-green-90);
$green-70: var(--color-green-70);
$teal: #091c24;
$black: #081c24;
$black-80: rgba(0,0,0,0.8);
$black-80: rgba(0, 0, 0, 0.8);
$white: #fff;
$white-80: rgba(255,255,255,0.8);
$white-80: rgba(255, 255, 255, 0.8);
$text-color: var(--text-color) !default;
$text-color-70: var(--text-color-70) !default;

708
yarn.lock

File diff suppressed because it is too large Load Diff