Updated seasonedinput to also handle two-way binded value prop. This changes is reflected most all places that seaoned-input is used

. Fuck, also added the new ResultsList which replaces MoviesList
This commit is contained in:
2019-10-22 23:09:29 +02:00
parent 4528b240e1
commit 6d6f1ffd06
8 changed files with 139 additions and 441 deletions

View File

@@ -1,386 +0,0 @@
<template>
<div>
<div class='movies-list' v-if="!error">
<header class='list-header'>
<h2 class='header__title'>{{ listTitle }}</h2>
<router-link class='header__view-more'
:to="'/list/' + list.route"
v-if='shortList'>
View All</router-link>
<div v-else style="line-height: 0;">
<span class='header__result-count' v-if="totalResults">{{ resultCount }} results</span>
<loading-placeholder v-else :count="1" lineClass='short nomargin'></loading-placeholder>
</div>
</header>
<!-- <ul class="filter">
<li class="filter-item" v-for="(item, index) in results" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item.title }}</li>
</ul> -->
<ul class='results'>
<movies-list-item v-for='movie in results' :movie="movie" :shortList="shortList"></movies-list-item>
</ul>
<loader v-if="loader" />
<div class='end-section' v-if="!shortList">
<seasoned-button v-if="currentPage < totalPages" @click="loadMore">load more</seasoned-button>
</div>
</div>
<div v-else style="display: flex; height: 50vh; width: 100%; justify-content: center; align-items: center;">
<h1 v-if="error">{{ error }}</h1>
<h1 v-else>Unable to load list: {{ listTitle }}</h1>
</div>
</div>
</template>
<script>
import storage from '@/storage.js'
import MoviesListItem from '@/components/MoviesListItem.vue'
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import LoadingPlaceholder from '@/components/ui/LoadingPlaceholder.vue'
import Loader from '@/components/ui/Loader.vue'
import { searchTmdb, getTmdbListByPath } from '@/api.js'
export default {
props: {
shortList: {
type: Boolean,
default: false
},
propList: Object
},
components: { MoviesListItem, SeasonedButton, LoadingPlaceholder, Loader },
data() {
return {
listTitle: 'No listname found',
results: [],
currentPage: 1,
totalResults: 0,
totalPages: -1,
fetchingResults: false,
error: undefined,
loader: false,
filters: {
status: {
elms: ['all', 'requested', 'downloading', 'downloaded'],
selected: 0,
}
}
}
},
computed: {
resultCount() {
return this.totalResults.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
}
},
beforeMount() {
if (this.propList) {
this.list = this.propList
}
this.setPageFromUrlQuery()
this.parseURI()
},
mounted() {
setTimeout(() => {
if (this.results.length === 0 && this.error === undefined) {
this.loader = true
}
}, 200)
},
methods: {
setPageFromUrlQuery() {
if (this.$route.query.page)
this.currentPage = this.$route.query.page
console.log('url page param found', this.currentPage)
},
getListByName(name) {
return storage.homepageLists.filter(list => list.route === name)[0]
},
parseURI() {
const currentRouteName = this.$route.name
// route name is list - we are in a list view
if (currentRouteName === 'list') {
const nameParam = this.$route.params.name
if (this.getListByName(nameParam)) {
this.list = this.getListByName(nameParam)
this.listTitle = this.list.title
this.fetchListitems()
} else {
this.error = `Unable to load list: `
}
} // route name is search - we are searcing
else if (currentRouteName === 'search') {
if (this.$route.query.query) {
this.query = decodeURIComponent(this.$route.query.query)
this.listTitle = 'Search results: ' + this.query
this.fetchSearchItems()
} else {
this.error = 'Search query is not defined, please try again'
}
} // no matched route found - using prop to fetch list items
else {
this.listTitle = this.list.title
this.fetchListitems()
}
document.title = this.listTitle
},
// TODO these should receive a path not get it from list instance
fetchListitems() {
getTmdbListByPath(this.list.path, this.currentPage)
.then(this.parseResponse)
.catch(error => {
console.error(error)
this.error = 'Network error'
})
},
fetchSearchItems() {
searchTmdb(this.query, this.currentPage)
.then(this.parseResponse)
},
// TODO what parts are modular and what parts do we want the component to deal with
// if we pass in some object and then as we initialize we set to local variables.
// This way we call the http-api from outside and pass the response in to the component[0]
// Could also parse the response we are requesting then return a clean object we can
// pass down[1].
// [0] if this is done we should also take the page, total pages, total results and
// the list of results. Maybe also the title of the list or use local title as fallback?
// [1] an issue with this that duplicate code will be needed for doing the same with
// url params and paths.
// (What if we eliminated folder based routes and implemented the routes in hashes
// with single page applications today the navigation is simple enought that it
// would maybe not be needed to have a path-route but a hash-local.storage
// implementation; would allow sharing and remembering paths is just silly for most
// Single-Page-Applications that are tightly scoped applications)
parseResponse(response) {
const data = response.data
if (data.page > data.total_pages) {
console.error('You have reached the end')
this.error = 'You have reached the end'
return
}
if (this.results.length) {
this.results.push(...data.results)
} else {
this.results = this.shortList ? data.results.slice(0,12) : data.results
}
this.page = data.page
this.totalPages = data.total_pages
this.totalResults = data.total_results || data.results.length
this.loader = false
console.info(`Response from list: ${this.listTitle}`, { results: this.results, page: this.page, totalPages: this.totalPages, totalResults: this.totalResults })
},
loadMore(){
this.currentPage++;
console.log('path and name:', this.$route.path, this.$route.name)
let url = ''
if (this.$route.path.includes('list'))
url = `/#${this.$route.path}?page=${this.currentPage}`
else if (this.$route.path.includes('search'))
url = `/#/search?query=${this.query}&page=${this.currentPage}`
console.log('new url', url)
window.history.replaceState({}, 'foo', url)
this.parseURI()
},
// sort() {
// console.log(this.showFilters)
// },
// toggleFilter(item, index){
// this.showFilter = this.showFilter ? false : true;
// // this.results = this.results.filter(result => result.status != 'downloaded')
// },
// applyFilter(item, index) {
// this.filter = item;
// this.filters.status.selected = index;
// console.log('applied query filter: ', item, index)
// this.fetchCategory()
// }
},
watch: {
$route: function () {
console.log('updated route')
this.results = false
this.currentPage = 1
this.setPageFromUrlQuery()
this.parseURI()
}
}
}
</script>
<style lang="scss" scoped>
@import './src/scss/media-queries';
@import './src/scss/variables';
.movies-list {
& ul:last-of-type {
padding-bottom: 1.5rem;
}
&:first-of-type header {
padding-top: 1.75rem;
}
header {
width: 100%;
display: flex;
justify-content: space-between;
padding: 12px;
&.sticky {
background-color: $background-color-secondary;
position: sticky;
position: -webkit-sticky;
top: $header-size;
z-index: 4;
}
h2 {
font-size: 18px;
font-weight: 300;
text-transform: capitalize;
line-height: 18px;
margin: 0;
color: $text-color;
}
.view-more {
font-size: 13px;
font-weight: 300;
letter-spacing: .5px;
color: $text-color-70;
text-decoration: none;
transition: color .5s ease;
cursor: pointer;
&:after{
content: " →";
}
&:hover{
color: $text-color;
}
}
.result-count {
font-size: 13px;
font-weight: 300;
letter-spacing: .5px;
color: $text-color;
text-decoration: none;
}
}
.results {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0;
list-style: none;
&.shortList > li {
display: none;
&:nth-child(-n+4) {
display: block;
}
}
.fullwidth-button {
width: 100%;
margin: 1rem 0;
display: flex;
justify-content: center;
}
}
@include tablet-min {
header {
padding-left: 1.25rem;
}
.results.shortList > li:nth-child(-n+6) {
display: block;
}
}
@include tablet-landscape-min {
header {
padding-left: 1.5rem;
}
.results.shortList > li:nth-child(-n+8) {
display: block;
}
}
@include desktop-min {
.results.shortList > li:nth-child(-n+12) {
display: block;
}
}
@include desktop-lg-min {
header {
padding-left: 1.75rem;
}
.results.shortList > li:nth-child(-n+16) {
display: block;
}
}
}
// .shutter {
// $height: 36px;
// height: $height;
// width: 100%;
// background-color: $background-color-secondary;
// position: absolute;
// margin-bottom: -$height;
// position: -webkit-sticky; /* Safari */
// position: sticky;
// top: $header-size;
// z-index: 4;
// @include tablet-min{
// background-color: blue;
// height: 23px 15px;
// }
// @include tablet-landscape-min{
// background-color: orange;
// height: 30px;
// }
// @include desktop-min{
// background-color: navajowhite;
// height: 34px;
// }
// }
.form__group-input {
padding: 10px 5px 10px 15px;
margin-left: 0;
height: 38px;
width: 150px;
font-size: 15px;
@include desktop-min {
width: 200px;
font-size: 17px;
}
}
</style>

View File

@@ -2,12 +2,13 @@
<section>
<h1>Register new user</h1>
<seasoned-input text="username" icon="Email" type="username"
@inputValue="setValue('username', $event)" />
<seasoned-input text="password" icon="Keyhole" type="password"
@inputValue="setValue('password', $event)" @enter="requestNewUser"/>
<seasoned-input text="repeat password" icon="Keyhole" type="password"
@inputValue="setValue('password', $event)" @enter="requestNewUser"/>
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
<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>
@@ -18,29 +19,26 @@
<script>
import axios from 'axios'
import storage from '@/storage.js'
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import SeasonedInput from '@/components/ui/SeasonedInput.vue'
import SeasonedMessages from '@/components/ui/SeasonedMessages.vue'
export default {
components: { SeasonedButton, SeasonedInput, SeasonedMessages },
data(){
return{
data() {
return {
messages: [],
username: undefined,
password: undefined,
passwordRepeat: undefined
username: null,
password: null,
passwordRepeat: null
}
},
methods: {
requestNewUser(){
let username = this.username
let password = this.password
let password_re = this.passwordRepeat
let { username, password, passwordRepeat } = this
let verifyCredentials = this.checkCredentials(username, password, passwordRepeat);
let verifyCredentials = this.checkCredentials(username, password, password_re);
if (verifyCredentials.verified) {
axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
username: username,
@@ -65,19 +63,25 @@ export default {
this.messages.push({ type: 'warning', title: 'Parse error', message: verifyCredentials.reason })
}
},
checkCredentials(username, password, password_re) {
if (password !== password_re) {
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 if (username === undefined) {
return {
verified: false,
reason: 'Please insert username'
}
}
else {
return {
verified: true,
@@ -85,9 +89,6 @@ export default {
}
}
},
setValue(l, t) {
this[l] = t
},
logOut(){
localStorage.clear();
eventHub.$emit('setUserStatus');

View File

@@ -0,0 +1,68 @@
<template>
<ul class="results" :class="{'shortList': shortList}">
<movies-list-item v-for='movie in results' :movie="movie" />
</ul>
</template>
<script>
import MoviesListItem from '@/components/MoviesListItem'
export default {
components: { MoviesListItem },
props: {
results: {
type: Array,
required: true
},
shortList: {
type: Boolean,
required: false,
default: false
}
}
}
</script>
<style lang="scss" scoped>
@import './src/scss/media-queries';
.results {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0;
list-style: none;
&.shortList > li {
display: none;
&:nth-child(-n+4) {
display: block;
}
}
}
@include tablet-min {
.results.shortList > li:nth-child(-n+6) {
display: block;
}
}
@include tablet-landscape-min {
.results.shortList > li:nth-child(-n+8) {
display: block;
}
}
@include desktop-min {
.results.shortList > li:nth-child(-n+10) {
display: block;
}
}
@include desktop-lg-min {
.results.shortList > li:nth-child(-n+16) {
display: block;
}
}
</style>

View File

@@ -6,22 +6,24 @@
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
<form class="form">
<seasoned-input text="plex username" icon="Email"
@inputValue="setValue('plexUsername', $event)"/>
<seasoned-input text="plex password" icon="Keyhole" type="password"
@inputValue="setValue('plexPassword', $event)"/>
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
:value.sync="plexPassword" @submit="authenticatePlex" />
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
<seasoned-messages :messages.sync="messages" />
</form>
<hr class='setting__divider'>
<h3 class='settings__header'>Change password</h3>
<form class="form">
<seasoned-input text="new password" icon="Keyhole" type="password"
@inputValue="setValue('newPass', $event)"/>
<seasoned-input text="repeat new password" icon="Keyhole" type="password"
@inputValue="setValue('newPassConfirm', $event)"/>
<seasoned-input placeholder="new password" icon="Keyhole" type="password"
:value.sync="newPassword" />
<seasoned-input placeholder="repeat new password" icon="Keyhole" type="password"
:value.sync="newPasswordRepeat" />
<seasoned-button @click="changePassword">change password</seasoned-button>
</form>
@@ -45,11 +47,12 @@
import storage from '@/storage.js'
import SeasonedInput from '@/components/ui/SeasonedInput.vue'
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import SeasonedMessages from '@/components/ui/SeasonedMessages.vue'
import { plexAuthenticate } from '@/api.js'
export default {
components: { SeasonedInput, SeasonedButton },
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
data(){
return{
userLoggedIn: '',
@@ -73,11 +76,12 @@ export default {
plexAuthenticate(username, password)
.then((resp) => {
let data = resp.data;
console.log('response from plex:', data.user)
let data = resp.data;
this.messages.push({ type: 'success', title: 'Authenticated with plex', message: 'Successfully linked plex account with seasoned request' })
// console.log('response from plex:', data.user)
})
.catch((error) => {
console.log('error: ', error)
this.messages.push({ type: 'error', title: 'Something went wrong', message: error.message })
})
}
},

View File

@@ -2,10 +2,8 @@
<section>
<h1>Sign in</h1>
<seasoned-input text="username" icon="Email" type="username"
@inputValue="setValue('username', $event)" />
<seasoned-input text="password" icon="Keyhole" type="password"
@inputValue="setValue('password', $event)" @enter="signin"/>
<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>

View File

@@ -75,7 +75,7 @@
<div class="editQuery" v-if="editSearchQuery">
<seasonedInput text="Torrent query" icon="_torrents" @inputValue="(val) => editedSearchQuery = val" @enter="fetchTorrents(editedSearchQuery)" />
<seasonedInput placeholder="Torrent query" icon="_torrents" :value.sync="editedSearchQuery" @enter="fetchTorrents(editedSearchQuery)" />
<div style="height: 45px; width: 5px;"></div>

View File

@@ -1,29 +1,37 @@
<template>
<div class="group" :class="{ completed: value.length > 0 }">
<div class="group" :class="{ completed: value }">
<svg class="group__input-icon"><use v-bind="{'xlink:href':'#icon' + icon}"></use></svg>
<input class="group__input" :type="tempType || type" ref="plex_username"
v-model="value" :placeholder="text" @keyup.enter="submit" @input="handleInput" />
<input class="group__input" :type="tempType || type" @input="handleInput" v-model="inputValue"
:placeholder="placeholder" @keyup.enter="submit" />
<i v-if="value.length > 0 && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i>
<i v-if="value && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i>
</div>
</template>
<script>
export default {
props: {
text: { type: String },
placeholder: { type: String },
icon: { type: String },
type: { type: String }
type: { type: String, default: 'text' },
value: { type: String, default: undefined }
},
data() {
return { value: '', tempType: undefined }
return {
inputValue: undefined,
tempType: undefined
}
},
methods: {
submit(event) {
this.$emit('enter')
},
handleInput(value) {
this.$emit('inputValue', this.value)
handleInput(event) {
if (this.value !== undefined) {
this.$emit('update:value', this.inputValue)
} else {
this.$emit('change', this.inputValue, event)
}
},
toggleShowPassword() {
if (this.tempType === 'text') {

View File

@@ -1,6 +1,6 @@
<template>
<transition-group name="fade">
<div class="message" v-for="message in messages" :class="message.type || 'warning'" :key="message">
<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>
@@ -31,6 +31,11 @@ export default {
localMessages: [...this.messages]
}
},
computed: {
reversedMessages() {
return [...this.messages].reverse()
}
},
methods: {
clicked(e) {
const removedMessage = [...this.messages].filter(mes => mes !== e)