mirror of
https://github.com/Xevion/the-office.git
synced 2025-12-16 16:13:34 -06:00
Move all ./client files into root as this repository becomes client-based
This commit is contained in:
87
src/components/Character.vue
Normal file
87
src/components/Character.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb v-if="ready" :items="breadcrumbs"></b-breadcrumb>
|
||||
<b-card v-else class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton style="width: 40%;"></Skeleton>
|
||||
</b-card>
|
||||
<b-card>
|
||||
<h4 v-if="ready">{{ character.name }}</h4>
|
||||
<Skeleton v-else style="max-width: 30%"></Skeleton>
|
||||
<b-card-body v-if="ready">
|
||||
{{ character.summary }}
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumb-skeleton {
|
||||
background-color: $grey-3;
|
||||
height: 48px;
|
||||
|
||||
& > .card-body {
|
||||
padding: 0 0 0 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Skeleton from './Skeleton.vue';
|
||||
import {types} from "@/mutation_types";
|
||||
|
||||
export default {
|
||||
name: 'Character',
|
||||
components: {
|
||||
Skeleton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
character: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return this.character !== undefined && this.character !== null;
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: {name: 'Home'},
|
||||
},
|
||||
{
|
||||
text: 'Characters',
|
||||
to: {name: 'Characters'},
|
||||
},
|
||||
{
|
||||
text:
|
||||
this.character !== null && this.character !== undefined
|
||||
? this.character.name || this.$route.params.character
|
||||
: this.$route.params.character,
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchCharacter();
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.$nextTick(() => {
|
||||
this.fetchCharacter();
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchCharacter() {
|
||||
this.$store.dispatch(types.FETCH_CHARACTER, this.$route.params.character)
|
||||
.then(() => {
|
||||
this.character = this.$store.getters.getCharacter(this.$route.params.character);
|
||||
})
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
23
src/components/CharacterBadges.vue
Normal file
23
src/components/CharacterBadges.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="pt-2" v-if="characters" :fluid="true">
|
||||
<b-button squared class="mx-2 my-1 character-button" size="sm" v-for="(character, character_id) in characters"
|
||||
:key="character.name" :id="`character-${character_id}`"
|
||||
:title="`${character.appearances} Quote${character.appearances > 1 ? 's' : ''}`"
|
||||
:to="{ name: 'Character', params: { character: character_id } }"
|
||||
>
|
||||
{{ character.name }}
|
||||
<b-badge class="ml-1">{{ character.appearances }}</b-badge>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
characters: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
87
src/components/Characters.vue
Normal file
87
src/components/Characters.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb v-if="ready" :items="breadcrumbs"></b-breadcrumb>
|
||||
<b-card v-else class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton style="width: 40%;"></Skeleton>
|
||||
</b-card>
|
||||
<b-card v-if="ready">
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="(character, character_id) in characters" :key="character_id">
|
||||
<b-row align-v="start" align-content="start">
|
||||
<b-col cols="5" md="4" lg="4" xl="3">
|
||||
<b-img-lazy fluid-grow class="px-2" :src="faceURL(character_id)"
|
||||
:blank-src="faceURL(character_id, thumbnail = true)"
|
||||
blank-width="200" blank-height="200"
|
||||
></b-img-lazy>
|
||||
<!-- <b-img fluid-grow class="px-2"></b-img>-->
|
||||
</b-col>
|
||||
<b-col>
|
||||
<h4>
|
||||
{{ character.name || character_id }}
|
||||
<router-link class="no-link"
|
||||
:to="{ name: 'Character', params: {character: character_id} }">
|
||||
<b-icon class="h6" icon="caret-right-fill"></b-icon>
|
||||
</router-link>
|
||||
</h4>
|
||||
<p class="pl-3">{{ character.summary }}</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
.b-icon {
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
color: #007fe0;
|
||||
|
||||
&:hover {
|
||||
color: darken(#007fe0, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {types} from "@/mutation_types";
|
||||
import Skeleton from "@/components/Skeleton.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Skeleton
|
||||
},
|
||||
methods: {
|
||||
faceURL(character, thumbnail = false) {
|
||||
return `${process.env.VUE_APP_API_URL}/static/img/${character}/` + (thumbnail ? "face_thumb.webp" : "face.webp");
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch(types.PRELOAD_CHARACTER)
|
||||
.then(() => {
|
||||
// recompute computed properties since Vuex won't do it
|
||||
this.$forceUpdate();
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return this.$store.state.preloaded;
|
||||
},
|
||||
characters() {
|
||||
return this.$store.state.characters;
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{text: 'Home', to: {name: 'Home'}},
|
||||
{text: 'Characters', active: true}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
142
src/components/Episode.vue
Normal file
142
src/components/Episode.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb v-if="ready" :items="breadcrumbs"></b-breadcrumb>
|
||||
<b-card v-else class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton style="width: 40%;"></Skeleton>
|
||||
</b-card>
|
||||
<b-card class="mb-4">
|
||||
<template v-if="ready">
|
||||
<h3 class="card-title">"{{ episode.title }}"</h3>
|
||||
<span>{{ episode.description }}</span>
|
||||
<CharacterBadges v-if="episode && episode.characters" :characters="episode.characters"></CharacterBadges>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Skeleton style="width: 30%;"></Skeleton>
|
||||
<Skeleton style="width: 70%; height: 60%;"></Skeleton>
|
||||
<Skeleton style="width: 45%; height: 60%;"></Skeleton>
|
||||
<Skeleton style="width: 69%; height: 40%;"></Skeleton>
|
||||
</template>
|
||||
</b-card>
|
||||
<div v-if="ready">
|
||||
<b-card v-for="(scene, sceneIndex) in episode.scenes" :key="`scene-${sceneIndex}`"
|
||||
class="mb-1" body-class="p-0">
|
||||
<b-card-text class="my-2">
|
||||
<QuoteList :quotes="scene.quotes" :sceneIndex="sceneIndex"></QuoteList>
|
||||
<span v-if="scene.deleted" class="mt-n2 mb-4 text-muted deleted-scene pl-2"
|
||||
:footer="`Deleted Scene ${scene.deleted}`">
|
||||
Deleted Scene {{ scene.deleted }}
|
||||
</span>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumb-skeleton {
|
||||
background-color: $grey-3;
|
||||
height: 48px;
|
||||
|
||||
& > .card-body {
|
||||
padding: 0 0 0 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.card-title {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.deleted-scene {
|
||||
font-size: 0.75em;
|
||||
line-height: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import QuoteList from "./QuoteList.vue";
|
||||
import CharacterBadges from "./CharacterBadges.vue";
|
||||
import Skeleton from './Skeleton.vue';
|
||||
import {types} from "@/mutation_types";
|
||||
|
||||
export default {
|
||||
name: "Episode",
|
||||
components: {
|
||||
QuoteList,
|
||||
CharacterBadges,
|
||||
Skeleton,
|
||||
},
|
||||
created() {
|
||||
// When page loads directly on this Episode initially, fetch data
|
||||
this.fetch();
|
||||
},
|
||||
watch: {
|
||||
// When route changes, fetch data for current Episode route
|
||||
$route() {
|
||||
this.$nextTick(() => {
|
||||
this.fetch();
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
// Fetch the episode, then scroll - already fetched episode should scroll immediately
|
||||
this.$store.dispatch(types.FETCH_EPISODE, {season: this.params.season, episode: this.params.episode})
|
||||
.then(() => {
|
||||
// Force update, as for some reason it doesn't update naturally. I hate it too.
|
||||
this.$forceUpdate()
|
||||
|
||||
// Scroll down to quote
|
||||
if (this.$route.hash) {
|
||||
this.$nextTick(() => {
|
||||
const section = document.getElementById(this.$route.hash.substring(1));
|
||||
this.$scrollTo(section, 500, {easing: "ease-in"});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
episode() {
|
||||
return this.$store.getters.getEpisode(this.params.season, this.params.episode)
|
||||
},
|
||||
// Shorthand - literally useless, why does everything to have such long prefixes in dot notation
|
||||
params() {
|
||||
return this.$route.params
|
||||
},
|
||||
ready() {
|
||||
return this.$store.getters.isFetched(this.params.season, this.params.episode)
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: {
|
||||
name: 'Home'
|
||||
}
|
||||
},
|
||||
{
|
||||
text: `Season ${this.$route.params.season}`,
|
||||
to: {
|
||||
name: 'Season',
|
||||
season: this.$route.params.season
|
||||
}
|
||||
},
|
||||
{
|
||||
text: `Episode ${this.$route.params.episode}`,
|
||||
to: {
|
||||
name: 'Episode',
|
||||
season: this.$route.params.season,
|
||||
episode: this.$route.params.episode
|
||||
},
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
56
src/components/Home.vue
Normal file
56
src/components/Home.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<b-card>
|
||||
<template v-if="stats">
|
||||
<h4>
|
||||
The Office Quotes
|
||||
</h4>
|
||||
<b-card-text>
|
||||
A Vue.js application serving you {{ stats.totals.quote }} quotes from your
|
||||
favorite show - The Office.
|
||||
<br/>
|
||||
Click on a Season and Episode on the left-hand sidebar to view quotes.
|
||||
Search for quotes with the instant searchbox.
|
||||
</b-card-text>
|
||||
</template>
|
||||
<b-card-text v-else>
|
||||
<Skeleton style="width: 45%"></Skeleton>
|
||||
<Skeleton style="width: 75%"></Skeleton>
|
||||
<Skeleton style="width: 60%"></Skeleton>
|
||||
<Skeleton style="width: 60%"></Skeleton>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Skeleton from './Skeleton.vue';
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {
|
||||
Skeleton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stats: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getStats() {
|
||||
const path = `${process.env.VUE_APP_API_URL}/api/stats/`;
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
this.stats = res.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getStats();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
50
src/components/QuoteList.vue
Normal file
50
src/components/QuoteList.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<table class="quote-list px-3 w-100">
|
||||
<tr
|
||||
v-for="(quote, index) in quotes"
|
||||
:key="`quote-${index}`"
|
||||
:id="`${sceneIndex}-${index}`"
|
||||
:class="
|
||||
$route.hash !== null &&
|
||||
$route.hash.substring(1) === `${sceneIndex}-${index}`
|
||||
? 'highlight'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<td class="quote-speaker pl-3" v-if="quote.speaker">
|
||||
<span class="my-3">
|
||||
{{ quote.speaker }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3">{{ quote.text }}</td>
|
||||
<td class="px-1 pl-2">
|
||||
<a :href="quote_link(index)" @click="copy(index)" class="no-link">
|
||||
<b-icon icon="link45deg"></b-icon>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
sceneIndex: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
quotes: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
quote_link(quoteIndex) {
|
||||
return `/${this.$route.params.season}/${this.$route.params.episode}#${this.sceneIndex}-${quoteIndex}`;
|
||||
},
|
||||
copy(quoteIndex) {
|
||||
this.$copyText(process.env.VUE_APP_BASE_URL + this.quote_link(quoteIndex))
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
159
src/components/SearchResult.vue
Normal file
159
src/components/SearchResult.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<b-card class="mb-1" body-class="p-0 expandable-result" footer-class="my-1"
|
||||
@mouseover="hoverOn" @mouseleave="hoverOff" v-on:click="toggleExpansion"
|
||||
:class="[expanded ? 'expanded' : '']">
|
||||
<b-card-text class="mu-2 py-1 mb-1">
|
||||
<table v-if="expanded" class="quote-list px-3 py-1 w-100">
|
||||
<tr
|
||||
v-for="(quote, index) in above"
|
||||
class="secondary"
|
||||
:key="`quote-a-${index}`"
|
||||
>
|
||||
<td class="quote-speaker my-3 pl-3">
|
||||
<div>{{ quote.speaker }}</div>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3">
|
||||
<div>{{ quote.text }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="quote-speaker my-3 pl-3"
|
||||
v-html="item._highlightResult.speaker.value"
|
||||
></td>
|
||||
<td
|
||||
class="quote-text w-100 pr-3"
|
||||
v-html="item._highlightResult.text.value"
|
||||
></td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(quote, index) in below"
|
||||
class="secondary"
|
||||
:key="`quote-b-${index}`"
|
||||
>
|
||||
<td class="quote-speaker my-3 pl-3">
|
||||
<div>{{ quote.speaker }}</div>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3">
|
||||
<div>{{ quote.text }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-else class="quote-list px-3 py-1 w-100">
|
||||
<tr>
|
||||
<td
|
||||
class="quote-speaker my-3 pl-3"
|
||||
v-html="item._highlightResult.speaker.value"
|
||||
></td>
|
||||
<td
|
||||
class="quote-text w-100 pr-3"
|
||||
v-html="item._highlightResult.text.value"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
<router-link v-if="expanded" class="no-link search-result-link w-100 text-muted mb-2 ml-2"
|
||||
:to="{name: 'Episode', params: { season: item.season, episode: item.episode_rel },
|
||||
hash: `#${item.section_rel - 1}-${item.quote_rel - 1}`, }">
|
||||
Season {{ item.season }} Episode {{ item.episode_rel }} Scene
|
||||
{{ item.section_rel }}
|
||||
</router-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.expandable-result {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-result-link {
|
||||
white-space: nowrap;
|
||||
font-size: 0.75em !important;
|
||||
}
|
||||
|
||||
.quote-list > tr {
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-4;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.quote-speaker {
|
||||
min-width: 75px;
|
||||
padding-right: 1em;
|
||||
font-weight: 600;
|
||||
vertical-align: text-top;
|
||||
text-align: right;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
props: ["item"],
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
fetching: false,
|
||||
above: null,
|
||||
below: null,
|
||||
timeoutID: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fetched() {
|
||||
return this.above !== null || this.below !== null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleExpansion() {
|
||||
this.expanded = !this.expanded;
|
||||
// if first time expanding, fetch quotes
|
||||
if (!this.fetchQuotes()) {
|
||||
this.hasExpanded = true;
|
||||
this.fetchQuotes();
|
||||
}
|
||||
},
|
||||
hoverFetch() {
|
||||
if (!this.fetched && !this.fetching) {
|
||||
this.fetching = true;
|
||||
this.fetchQuotes();
|
||||
this.fetching = false;
|
||||
}
|
||||
},
|
||||
hoverOn() {
|
||||
// Schedule a fetching event
|
||||
this.timeoutID = setTimeout(this.hoverFetch, 300);
|
||||
},
|
||||
hoverOff() {
|
||||
// Hover is off. Unschedule event if it has not already fetched.
|
||||
clearTimeout(this.timeoutID);
|
||||
},
|
||||
fetchQuotes() {
|
||||
const path = `${process.env.VUE_APP_API_URL}/api/quote_surround?season=\
|
||||
${this.item.season}&episode=${this.item.episode_rel}&scene=${this.item.section_rel}"e=${this.item.quote_rel}`;
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
this.above = res.data.above;
|
||||
this.below = res.data.below;
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
20
src/components/SearchResults.vue
Normal file
20
src/components/SearchResults.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<ais-hits>
|
||||
<div slot-scope="{ items }">
|
||||
<SearchResult v-for="item in items" :item="item" :key="item.objectID"></SearchResult>
|
||||
</div>
|
||||
</ais-hits>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchResult from "./SearchResult.vue";
|
||||
|
||||
export default {
|
||||
name: "SearchResults",
|
||||
components: {
|
||||
SearchResult,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
71
src/components/Season.vue
Normal file
71
src/components/Season.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb :items="breadcrumbs"></b-breadcrumb>
|
||||
<b-card v-if="ready">
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="episode in season.episodes" :key="episode.episode_id">
|
||||
<b-row align-v="start" align-content="start">
|
||||
<b-col cols="5" md="4" lg="4" xl="3">
|
||||
<b-img-lazy fluid-grow class="px-2" src="https://via.placeholder.com/250"></b-img-lazy>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<h4>
|
||||
{{ episode.title }}
|
||||
<router-link class="no-link"
|
||||
:to="getEpisodeRoute(season.season_id, episode.episode_id)">
|
||||
<b-icon class="h6" icon="caret-right-fill"></b-icon>
|
||||
</router-link>
|
||||
</h4>
|
||||
<p class="pl-3">{{ episode.description }}</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
.b-icon {
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
color: #007fe0;
|
||||
&:hover {
|
||||
color: darken(#007fe0, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
getEpisodeRoute(s, e) {
|
||||
return {name: 'Episode', params: {season: s, episode: e}}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return this.$store.state.preloaded;
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: {name: 'Home'}
|
||||
},
|
||||
{
|
||||
text: `Season ${this.$route.params.season}`,
|
||||
active: true
|
||||
}
|
||||
]
|
||||
},
|
||||
season() {
|
||||
return this.$store.state.quoteData[this.$route.params.season - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
65
src/components/SeasonList.vue
Normal file
65
src/components/SeasonList.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="accordion" role="tablist">
|
||||
<b-card class="season-item" v-for="season in seasons" :key="season.season_id">
|
||||
<b-card-header header-tag="header" role="tab" v-b-toggle="'accordion-' + season.season_id">
|
||||
<a class="no-link align-items-center justify-content-between d-flex" v-if="isPreloaded">
|
||||
<h5 class="mb-0 pu-0 mu-0 season-title">
|
||||
Season {{ season.season_id }}
|
||||
</h5>
|
||||
<b-icon class="" icon="chevron-down"></b-icon>
|
||||
</a>
|
||||
<Skeleton v-else></Skeleton>
|
||||
</b-card-header>
|
||||
<b-collapse :id="'accordion-' + season.season_id" accordion="accordion-season-list">
|
||||
<b-card-body class="h-100 px-0">
|
||||
<b-list-group>
|
||||
<template v-for="(episode, index) in seasons[season.season_id - 1].episodes">
|
||||
<template v-if="isPreloaded">
|
||||
<b-list-group-item class="no-link episode-item" :key="`rl-${episode.episode_id}`"
|
||||
:to="{name: 'Episode', params: { season: season.season_id, episode: episode.episode_id }, }"
|
||||
:id="`s-${season.season_id}-ep-${episode.episode_id}`">
|
||||
Episode {{ episode.episode_id }} - "{{ episode.title }}"
|
||||
</b-list-group-item>
|
||||
<b-popover :key="`bpop-${episode.episode_id}`" triggers="hover"
|
||||
placement="right" delay="25" :target="`s-${season.season_id}-ep-${episode.episode_id}`">
|
||||
<template v-slot:title>
|
||||
{{ episode.title }}
|
||||
</template>
|
||||
{{ episode.description }}
|
||||
</b-popover>
|
||||
</template>
|
||||
<b-list-group-item v-else class="no-link episode-item" :key="index">
|
||||
<Skeleton></Skeleton>
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
</b-list-group>
|
||||
</b-card-body>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Skeleton from './Skeleton.vue';
|
||||
import {types} from "@/mutation_types";
|
||||
|
||||
export default {
|
||||
name: "SeasonList",
|
||||
components: {
|
||||
Skeleton
|
||||
},
|
||||
computed: {
|
||||
seasons() {
|
||||
return this.$store.state.quoteData;
|
||||
},
|
||||
// if SeasonList episode data (titles/descriptions) is loaded and ready
|
||||
isPreloaded() {
|
||||
return this.$store.state.preloaded;
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
created() {
|
||||
this.$store.dispatch(types.PRELOAD)
|
||||
},
|
||||
};
|
||||
</script>
|
||||
84
src/components/Skeleton.vue
Normal file
84
src/components/Skeleton.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="outer-skeleton">
|
||||
<div class="skeleton" :class="[animated ? undefined : 'no-animate']" :style="[style, inner_style]"></div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
background-size: 200px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(90deg, var(--secondary-color, $grey-4), var(--primary-color, $grey-6), var(--secondary-color, $grey-4));
|
||||
background-color: var(--secondary-color, $grey-4);
|
||||
animation: 1.25s ease-in-out 0s infinite normal none running SkeletonLoading;
|
||||
border-radius: var(--border-radius, 3px);
|
||||
|
||||
&.no-animate {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.outer-skeleton {
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
@-webkit-keyframes SkeletonLoading {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes SkeletonLoa,ding {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
inner_style: {
|
||||
type: Object
|
||||
},
|
||||
inner_class: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
border_radius: {
|
||||
type: String,
|
||||
},
|
||||
primary_color: {
|
||||
type: String,
|
||||
},
|
||||
secondary_color: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
'--primary-color': this.primary_color,
|
||||
'--secondary-color': this.secondary_color,
|
||||
'--border-radius': this.border_radius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user