Merge branch 'feature/user-graphs' into refactor
This commit is contained in:
316
src/components/ActivityPage.vue
Normal file
316
src/components/ActivityPage.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="wrapper" v-if="hasPlexUser">
|
||||
<h1>Your watch activity</h1>
|
||||
|
||||
<div class="filter">
|
||||
<h2>Filter</h2>
|
||||
|
||||
<div class="filter-item">
|
||||
<label class="desktop-only">Days:</label>
|
||||
<input class="dayinput"
|
||||
v-model="days"
|
||||
placeholder="number of days"
|
||||
type="number"
|
||||
pattern="[0-9]*"
|
||||
:style="{maxWidth: `${3 + (0.5 * days.length)}rem`}"/>
|
||||
<!-- <datalist id="days">
|
||||
<option v-for="index in 1500" :value="index" :key="index"></option>
|
||||
</datalist> -->
|
||||
</div>
|
||||
|
||||
<toggle-button class="filter-item" :options="chartTypes" :selected.sync="selectedChartDataType" />
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h3 class="chart-header">Activity per day:</h3>
|
||||
<div class="chart">
|
||||
<canvas ref="activityCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<h3 class="chart-header">Activity per day of week:</h3>
|
||||
<div class="chart">
|
||||
<canvas ref="playsByDayOfWeekCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>Must be authenticated</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import ToggleButton from '@/components/ui/ToggleButton';
|
||||
import { fetchChart } from '@/api'
|
||||
|
||||
var Chart = require('chart.js');
|
||||
Chart.defaults.global.elements.point.radius = 0
|
||||
Chart.defaults.global.elements.point.hitRadius = 10
|
||||
Chart.defaults.global.elements.point.pointHoverRadius = 10
|
||||
Chart.defaults.global.elements.point.hoverBorderWidth = 4
|
||||
|
||||
export default {
|
||||
components: { ToggleButton },
|
||||
data() {
|
||||
return {
|
||||
days: 30,
|
||||
selectedChartDataType: 'plays',
|
||||
charts: [{
|
||||
name: 'Watch activity',
|
||||
ref: 'activityCanvas',
|
||||
data: null,
|
||||
urlPath: '/plays_by_day',
|
||||
graphType: 'line'
|
||||
}, {
|
||||
name: 'Plays by day of week',
|
||||
ref: 'playsByDayOfWeekCanvas',
|
||||
data: null,
|
||||
urlPath: '/plays_by_dayofweek',
|
||||
graphType: 'bar'
|
||||
}],
|
||||
chartData: [{
|
||||
type: 'plays',
|
||||
tooltipLabel: 'Play count',
|
||||
},{
|
||||
type: 'duration',
|
||||
tooltipLabel: 'Watched duration',
|
||||
valueConvertFunction: this.convertSecondsToHumanReadable
|
||||
}],
|
||||
gridColor: getComputedStyle(document.documentElement).getPropertyValue('--text-color-5')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPlexUser() {
|
||||
return store.getters['userModule/plex_userid'] != null ? true : false
|
||||
},
|
||||
chartTypes() {
|
||||
return this.chartData.map(chart => chart.type)
|
||||
},
|
||||
selectedChartType() {
|
||||
return this.chartData.filter(data => data.type == this.selectedChartDataType)[0]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hasPlexUser(newValue, oldValue) {
|
||||
if (newValue != oldValue && newValue == true) {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
days(newValue) {
|
||||
if (newValue !== '') {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
selectedChartDataType(selectedChartDataType) {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (typeof(this.days) == 'number') {
|
||||
this.days = this.days.toString()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchChartData(charts) {
|
||||
if (this.hasPlexUser == false) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let chart of charts) {
|
||||
|
||||
|
||||
|
||||
fetchChart(chart.urlPath, this.days, this.selectedChartType.type)
|
||||
.then(data => {
|
||||
this.series = data.data.series.filter(group => group.name === 'TV')[0].data; // plays pr date in groups (movie/tv/music)
|
||||
this.categories = data.data.categories; // dates
|
||||
|
||||
const x_labels = data.data.categories.map(date => {
|
||||
if (date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
|
||||
const [year, month, day] = date.split('-')
|
||||
return `${day}.${month}`
|
||||
}
|
||||
|
||||
return date
|
||||
})
|
||||
let y_activityMovies = data.data.series.filter(group => group.name === 'Movies')[0].data
|
||||
let y_activityTV = data.data.series.filter(group => group.name === 'TV')[0].data
|
||||
|
||||
const datasets = [{
|
||||
label: `Movies watch last ${ this.days } days`,
|
||||
data: y_activityMovies,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: `Shows watch last ${ this.days } days`,
|
||||
data: y_activityTV,
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||
borderColor: 'rgba(255, 159, 64, 1)',
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
|
||||
if (chart.data == null) {
|
||||
this.generateChart(chart, x_labels, datasets)
|
||||
} else {
|
||||
chart.data.clear();
|
||||
chart.data.data.labels = x_labels;
|
||||
chart.data.data.datasets = datasets;
|
||||
chart.data.update();
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
generateChart(chart, labels, datasets) {
|
||||
const chartInstance = new Chart(this.$refs[chart.ref], {
|
||||
type: chart.graphType,
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
// hitRadius: 8,
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`,
|
||||
label: (tooltipItem, data) => {
|
||||
let label = data.datasets[tooltipItem.datasetIndex].label
|
||||
let value = tooltipItem.value;
|
||||
let text = 'Duration watched'
|
||||
|
||||
const context = label.split(' ')[0]
|
||||
if (context) {
|
||||
text = `${context} ${this.selectedChartType.tooltipLabel.toLowerCase()}`
|
||||
}
|
||||
|
||||
if (this.selectedChartType.valueConvertFunction) {
|
||||
value = this.selectedChartType.valueConvertFunction(tooltipItem.value)
|
||||
}
|
||||
|
||||
return ` ${text}: ${value}`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
color: this.gridColor
|
||||
},
|
||||
stacked: chart.graphType === 'bar',
|
||||
ticks: {
|
||||
// suggestedMax: 10000,
|
||||
callback: (value, index, values) => {
|
||||
if (this.selectedChartType.valueConvertFunction) {
|
||||
return this.selectedChartType.valueConvertFunction(value, values)
|
||||
}
|
||||
return value
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
stacked: chart.graphType === 'bar',
|
||||
gridLines: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.data = chartInstance;
|
||||
},
|
||||
convertSecondsToHumanReadable(value, values=null) {
|
||||
const highestValue = values ? values[0] : value;
|
||||
|
||||
// minutes
|
||||
if (highestValue < 3600) {
|
||||
const minutes = Math.floor(value / 60);
|
||||
|
||||
value = `${minutes} m`
|
||||
}
|
||||
// hours and minutes
|
||||
else if (highestValue > 3600 && highestValue < 86400) {
|
||||
const hours = Math.floor(value / 3600);
|
||||
const minutes = Math.floor(value % 3600 / 60);
|
||||
|
||||
value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m`
|
||||
}
|
||||
// days and hours
|
||||
else if (highestValue > 86400 && highestValue < 31557600) {
|
||||
const days = Math.floor(value / 86400);
|
||||
const hours = Math.floor(value % 86400 / 3600);
|
||||
|
||||
value = days != 0 ? `${days} d ${hours} h` : `${hours} h`
|
||||
}
|
||||
// years and days
|
||||
else if (highestValue > 31557600) {
|
||||
const years = Math.floor(value / 31557600);
|
||||
const days = Math.floor(value % 31557600 / 86400);
|
||||
|
||||
value = years != 0 ? `${years} y ${days} d` : `${days} d`
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
.wrapper {
|
||||
padding: 2rem;
|
||||
|
||||
@include mobile-only {
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
&-item:not(:first-of-type) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.dayinput {
|
||||
font-size: 1.2rem;
|
||||
max-width: 3rem;
|
||||
background-color: $background-ui;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
height: 35vh;
|
||||
width: 90vw;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -2,8 +2,11 @@
|
||||
<header :class="{ 'sticky': sticky }">
|
||||
<h2>{{ title }}</h2>
|
||||
|
||||
<span v-if="info" class="result-count">{{ info }}</span>
|
||||
<router-link v-else-if="link" :to="link" class='view-more'>
|
||||
<div v-if="info instanceof Array" class="flex flex-direction-column">
|
||||
<span v-for="item in info" class="info">{{ item }}</span>
|
||||
</div>
|
||||
<span v-else class="info">{{ info }}</span>
|
||||
<router-link v-if="link" :to="link" class='view-more' :aria-label="`View all ${title}`">
|
||||
View All
|
||||
</router-link>
|
||||
</header>
|
||||
@@ -19,10 +22,10 @@ export default {
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: true
|
||||
},
|
||||
info: {
|
||||
type: String,
|
||||
type: [String, Array],
|
||||
required: false
|
||||
},
|
||||
link: {
|
||||
@@ -37,12 +40,16 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import './src/scss/variables';
|
||||
@import './src/scss/media-queries';
|
||||
@import './src/scss/main';
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1.8rem 12px;
|
||||
align-items: center;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
|
||||
&.sticky {
|
||||
background-color: $background-color;
|
||||
@@ -51,22 +58,19 @@ header {
|
||||
position: -webkit-sticky;
|
||||
top: $header-size;
|
||||
z-index: 4;
|
||||
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 300;
|
||||
text-transform: capitalize;
|
||||
line-height: 18px;
|
||||
line-height: 1.4rem;
|
||||
margin: 0;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.view-more {
|
||||
font-size: 13px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: .5px;
|
||||
color: $text-color-70;
|
||||
@@ -82,12 +86,13 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.result-count {
|
||||
.info {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
letter-spacing: .5px;
|
||||
color: $text-color;
|
||||
text-decoration: none;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
|
||||
@@ -151,7 +151,7 @@ export default {
|
||||
this.title = movie.title
|
||||
this.poster = movie.poster
|
||||
this.backdrop = movie.backdrop
|
||||
this.matched = movie.existsInPlex
|
||||
this.matched = movie.exists_in_plex || false
|
||||
this.checkIfRequested(movie)
|
||||
.then(status => this.requested = status)
|
||||
|
||||
@@ -161,9 +161,7 @@ export default {
|
||||
return await getRequestStatus(movie.id, movie.type)
|
||||
},
|
||||
nestedDataToString(data) {
|
||||
let nestedArray = []
|
||||
data.forEach(item => nestedArray.push(item));
|
||||
return nestedArray.join(', ');
|
||||
return data.join(', ')
|
||||
},
|
||||
sendRequest(){
|
||||
request(this.id, this.type, storage.token)
|
||||
@@ -200,7 +198,7 @@ export default {
|
||||
this.prevDocumentTitle = store.getters['documentTitle/title']
|
||||
|
||||
if (this.type === 'movie') {
|
||||
getMovie(this.id)
|
||||
getMovie(this.id, true)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: '404' });
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
<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 }}</p>
|
||||
@@ -67,7 +72,7 @@ export default {
|
||||
}
|
||||
|
||||
@include desktop-lg-min{
|
||||
padding: 20px;
|
||||
padding: 15px;
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
@@ -115,3 +120,46 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0.8rem;
|
||||
|
||||
> progress {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
progress {
|
||||
border-radius: 4px;
|
||||
height: 1.4rem;
|
||||
}
|
||||
progress::-webkit-progress-bar {
|
||||
background-color: rgba($black, 0.55);
|
||||
border-radius: 4px;
|
||||
}
|
||||
progress::-webkit-progress-value {
|
||||
background-color: $green-70;
|
||||
border-radius: 4px;
|
||||
|
||||
}
|
||||
progress::-moz-progress-bar {
|
||||
/* style rules */
|
||||
background-color: green;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<section class="profile">
|
||||
<div class="profile__content" v-if="userLoggedIn">
|
||||
<header class="profile__header">
|
||||
<h2 class="profile__title">{{ emoji }} Welcome {{ userName }}</h2>
|
||||
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
|
||||
|
||||
<div class="button--group">
|
||||
<seasoned-button @click="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<settings v-if="showSettings"></settings>
|
||||
|
||||
<list-header title="User requests" :info="resultCount"/>
|
||||
<list-header title="User requests" :info="resultCount" />
|
||||
<results-list v-if="results" :results="results" />
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,6 @@ export default {
|
||||
data(){
|
||||
return{
|
||||
userLoggedIn: '',
|
||||
userName: '',
|
||||
emoji: '',
|
||||
results: undefined,
|
||||
totalResults: undefined,
|
||||
@@ -58,25 +57,10 @@ export default {
|
||||
const loadedResults = this.results.length
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
|
||||
return `${loadedResults} of ${totalResults} results`
|
||||
}
|
||||
},
|
||||
username: () => store.getters['userModule/username']
|
||||
},
|
||||
methods: {
|
||||
createSession(token){
|
||||
axios.get(`https://api.themoviedb.org/3/authentication/session/new?api_key=${storage.apiKey}&request_token=${token}`)
|
||||
.then(function(resp){
|
||||
let data = resp.data;
|
||||
if(data.success){
|
||||
let id = data.session_id;
|
||||
localStorage.setItem('session_id', id);
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.userLoggedIn = true;
|
||||
this.getUserInfo();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
getUserInfo(){
|
||||
this.userName = localStorage.getItem('username');
|
||||
},
|
||||
toggleSettings() {
|
||||
this.showSettings = this.showSettings ? false : true;
|
||||
},
|
||||
@@ -91,7 +75,6 @@ export default {
|
||||
this.userLoggedIn = false;
|
||||
} else {
|
||||
this.userLoggedIn = true;
|
||||
this.getUserInfo();
|
||||
|
||||
getUserRequests()
|
||||
.then(results => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { register } from '@/api'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
@@ -40,23 +40,20 @@ export default {
|
||||
let verifyCredentials = this.checkCredentials(username, password, passwordRepeat);
|
||||
|
||||
if (verifyCredentials.verified) {
|
||||
axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
.then(resp => {
|
||||
let data = resp.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' })
|
||||
}
|
||||
|
||||
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' })
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.response.data.error })
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -3,17 +3,23 @@
|
||||
<div class="profile__content" v-if="userLoggedIn">
|
||||
<section class='settings'>
|
||||
<h3 class='settings__header'>Plex account</h3>
|
||||
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
|
||||
|
||||
<form class="form">
|
||||
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
|
||||
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
|
||||
:value.sync="plexPassword" @submit="authenticatePlex" />
|
||||
<div v-if="!hasPlexUser">
|
||||
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
|
||||
|
||||
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
|
||||
<form class="form">
|
||||
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
|
||||
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
|
||||
:value.sync="plexPassword" @submit="authenticatePlex" />
|
||||
|
||||
<seasoned-messages :messages.sync="messages" />
|
||||
</form>
|
||||
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="settings__info">Awesome, your account is already authenticated with plex! Enjoy viewing your seasoned search history, plex watch history and real-time torrent download progress.</span>
|
||||
<seasoned-button @click="unauthenticatePlex">un-link plex account</seasoned-button>
|
||||
</div>
|
||||
<seasoned-messages :messages.sync="messages" />
|
||||
|
||||
<hr class='setting__divider'>
|
||||
|
||||
@@ -44,12 +50,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import storage from '@/storage'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
|
||||
import { plexAuthenticate } from '@/api'
|
||||
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api'
|
||||
|
||||
export default {
|
||||
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
|
||||
@@ -60,7 +67,21 @@ export default {
|
||||
plexUsername: null,
|
||||
plexPassword: null,
|
||||
newPassword: null,
|
||||
newPasswordRepeat: null
|
||||
newPasswordRepeat: null,
|
||||
emoji: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPlexUser: function() {
|
||||
return this.settings && this.settings['plex_userid']
|
||||
},
|
||||
settings: {
|
||||
get: () => {
|
||||
return store.getters['userModule/settings']
|
||||
},
|
||||
set: function(newSettings) {
|
||||
store.dispatch('userModule/setSettings', newSettings)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -70,26 +91,37 @@ export default {
|
||||
changePassword() {
|
||||
return
|
||||
},
|
||||
authenticatePlex() {
|
||||
async authenticatePlex() {
|
||||
let username = this.plexUsername
|
||||
let password = this.plexPassword
|
||||
|
||||
plexAuthenticate(username, password)
|
||||
.then(resp => {
|
||||
const data = resp.data
|
||||
this.messages.push({ type: 'success', title: 'Authenticated with plex', message: 'Successfully linked plex account with seasoned request' })
|
||||
const response = await linkPlexAccount(username, password)
|
||||
|
||||
console.log('response from plex:', data.username)
|
||||
this.messages.push({
|
||||
type: response.success ? 'success' : 'error',
|
||||
title: response.success ? 'Authenticated with plex' : 'Something went wrong',
|
||||
message: response.message
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
this.messages.push({ type: 'error', title: 'Something went wrong', message: error.message })
|
||||
if (response.success)
|
||||
getSettings().then(settings => this.settings = settings)
|
||||
},
|
||||
async unauthenticatePlex() {
|
||||
const response = await unlinkPlexAccount()
|
||||
|
||||
this.messages.push({
|
||||
type: response.success ? 'success' : 'error',
|
||||
title: response.success ? 'Unlinked plex account ' : 'Something went wrong',
|
||||
message: response.message
|
||||
})
|
||||
|
||||
if (response.success)
|
||||
getSettings().then(settings => this.settings = settings)
|
||||
}
|
||||
},
|
||||
created(){
|
||||
if (localStorage.getItem('token')){
|
||||
const token = localStorage.getItem('token') || false;
|
||||
if (token){
|
||||
this.userLoggedIn = true
|
||||
}
|
||||
}
|
||||
@@ -151,7 +183,7 @@ a {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(8, 28, 36, 0.05);
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 70px;
|
||||
margin-left: 20px;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
<section>
|
||||
<h1>Sign in</h1>
|
||||
|
||||
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
|
||||
<seasoned-input placeholder="username"
|
||||
icon="Email"
|
||||
type="email"
|
||||
:value.sync="username" />
|
||||
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="signin"/>
|
||||
|
||||
<seasoned-button @click="signin">sign in</seasoned-button>
|
||||
@@ -16,7 +19,7 @@
|
||||
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { login } from '@/api'
|
||||
import storage from '../storage'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
@@ -39,29 +42,25 @@ export default {
|
||||
let username = this.username;
|
||||
let password = this.password;
|
||||
|
||||
axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, {
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
.then(resp => {
|
||||
let data = resp.data;
|
||||
if (data.success){
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('username', username);
|
||||
localStorage.setItem('admin', data.admin);
|
||||
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.message.endsWith('401')) {
|
||||
this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
|
||||
}
|
||||
else {
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||
}
|
||||
});
|
||||
login(username, password)
|
||||
.then(data => {
|
||||
if (data.success){
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('username', username);
|
||||
localStorage.setItem('admin', data.admin || false);
|
||||
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 401) {
|
||||
this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
|
||||
}
|
||||
else {
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
created(){
|
||||
|
||||
100
src/components/ui/ToggleButton.vue
Normal file
100
src/components/ui/ToggleButton.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="toggle-container">
|
||||
<button v-for="option in options" class="toggle-button" @click="toggle(option)"
|
||||
:class="toggleValue === option ? 'selected' : null"
|
||||
>{{ option }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
Array,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toggleValue: this.selected || this.options[0]
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.toggle(this.toggleValue)
|
||||
},
|
||||
methods: {
|
||||
toggle(toggleValue) {
|
||||
this.toggleValue = toggleValue;
|
||||
if (this.selected !== undefined) {
|
||||
this.$emit('update:selected', toggleValue)
|
||||
} else {
|
||||
this.$emit('change', toggleValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
$background: $background-ui;
|
||||
$background-selected: $background-color-secondary;
|
||||
|
||||
.toggle-container {
|
||||
width: 100%;
|
||||
max-width: 15rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// padding: 0.2rem;
|
||||
background-color: $background;
|
||||
border: 2px solid $background;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid $background;
|
||||
border-right: 4px solid $background;
|
||||
|
||||
.toggle-button {
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0;
|
||||
border: 0;
|
||||
color: $text-color;
|
||||
// background-color: $text-color-5;
|
||||
background-color: $background;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.selected {
|
||||
color: $text-color;
|
||||
// background-color: $background-color-secondary;
|
||||
background-color: $background-selected;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// &:first-of-type, &:last-of-type {
|
||||
// border-left: 4px solid $background;
|
||||
// border-right: 4px solid $background;
|
||||
// }
|
||||
|
||||
|
||||
// &:first-of-type {
|
||||
// border-top-left-radius: 4px;
|
||||
// border-bottom-left-radius: 4px;
|
||||
// }
|
||||
|
||||
// &:last-of-type {
|
||||
// border-top-right-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user