mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-03-11 11:55:38 +00:00
* On every route change, update local variables from query params * ResultSection is keyed to query to force re-render * Resolved lint warnings * replace webpack w/ vite * update all imports with alias @ and scss * vite environment variables, also typed * upgraded eslint, defined new rules & added ignore comments * resolved linting issues * moved index.html to project root * updated dockerfile w/ build stage before runtime image definition * sign drone config
279 lines
6.6 KiB
Vue
279 lines
6.6 KiB
Vue
<template>
|
|
<section class="person">
|
|
<header ref="header">
|
|
<div class="info">
|
|
<h1 v-if="person">
|
|
{{ person.name }}
|
|
</h1>
|
|
<div v-else>
|
|
<loading-placeholder :count="1" />
|
|
<loading-placeholder :count="1" line-class="short" :top="3.5" />
|
|
</div>
|
|
|
|
<span v-if="person && person['known_for_department']" class="known-for">
|
|
{{
|
|
person.known_for_department === "Acting"
|
|
? "Actor"
|
|
: person.known_for_department
|
|
}}
|
|
</span>
|
|
</div>
|
|
|
|
<figure class="person__poster">
|
|
<img
|
|
ref="poster-image"
|
|
class="person-item__img is-loaded"
|
|
alt="Image of person"
|
|
:src="poster"
|
|
/>
|
|
</figure>
|
|
</header>
|
|
|
|
<div v-if="loading">
|
|
<loading-placeholder :count="6" />
|
|
<loading-placeholder line-class="short" :top="3" />
|
|
<loading-placeholder :count="6" line-class="fullwidth" />
|
|
<loading-placeholder line-class="short" :top="4.5" />
|
|
<loading-placeholder />
|
|
</div>
|
|
|
|
<div v-if="person">
|
|
<Detail v-if="age" title="Age" :detail="age" />
|
|
|
|
<Detail
|
|
v-if="person"
|
|
title="Born"
|
|
:detail="person.place_of_birth ? person.place_of_birth : '(Not found)'"
|
|
/>
|
|
|
|
<Detail v-if="person.biography" title="Biography">
|
|
<Description :description="person.biography" />
|
|
</Detail>
|
|
|
|
<Detail
|
|
v-if="creditedShows.length"
|
|
title="movies"
|
|
:detail="`Credited in ${creditedMovies.length} movies`"
|
|
>
|
|
<CastList :cast="creditedMovies" />
|
|
</Detail>
|
|
|
|
<Detail
|
|
v-if="creditedShows.length"
|
|
title="shows"
|
|
:detail="`Credited in ${creditedShows.length} shows`"
|
|
>
|
|
<CastList :cast="creditedShows" />
|
|
</Detail>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from "vue";
|
|
import CastList from "@/components/CastList.vue";
|
|
import Detail from "@/components/popup/Detail.vue";
|
|
import Description from "@/components/popup/Description.vue";
|
|
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
|
import type { Ref, ComputedRef } from "vue";
|
|
import { getPerson, getPersonCredits } from "../../api";
|
|
import type {
|
|
IPerson,
|
|
IPersonCredits,
|
|
IMovie,
|
|
IShow
|
|
} from "../../interfaces/IList";
|
|
import { MediaTypes } from "../../interfaces/IList";
|
|
|
|
interface Props {
|
|
id: number;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const ASSET_URL = "https://image.tmdb.org/t/p/";
|
|
const ASSET_SIZES = ["w500", "w780", "original"];
|
|
|
|
const person: Ref<IPerson> = ref();
|
|
const credits: Ref<IPersonCredits> = ref();
|
|
const loading: Ref<boolean> = ref(false);
|
|
const creditedMovies: Ref<Array<IMovie | IShow>> = ref([]);
|
|
const creditedShows: Ref<Array<IMovie | IShow>> = ref([]);
|
|
|
|
const poster: ComputedRef<string> = computed(() => {
|
|
if (!person.value) return "/assets/placeholder.png";
|
|
if (!person.value?.poster) return "/assets/no-image.svg";
|
|
|
|
return `${ASSET_URL}${ASSET_SIZES[0]}${person.value.poster}`;
|
|
});
|
|
|
|
const age: ComputedRef<string> = computed(() => {
|
|
if (!person.value?.birthday) return "";
|
|
|
|
const today = new Date().getFullYear();
|
|
const birthYear = new Date(person.value.birthday).getFullYear();
|
|
return `${today - birthYear} years old`;
|
|
});
|
|
|
|
function setCredits(_credits: IPersonCredits) {
|
|
credits.value = _credits;
|
|
}
|
|
|
|
function setPerson(_person: IPerson) {
|
|
person.value = _person;
|
|
}
|
|
|
|
function sortPopularity(a: IMovie | IShow, b: IMovie | IShow): number {
|
|
return a.popularity < b.popularity ? 1 : -1;
|
|
}
|
|
|
|
function alreadyExists(
|
|
item: IMovie | IShow,
|
|
pos: number,
|
|
self: Array<IMovie | IShow>
|
|
) {
|
|
const names = self.map(_item => _item.title);
|
|
return names.indexOf(item.title) === pos;
|
|
}
|
|
|
|
function personCreditedFrom(cast: Array<IMovie | IShow>): void {
|
|
creditedMovies.value = cast
|
|
.filter(credit => credit.type === MediaTypes.Movie)
|
|
.filter(alreadyExists)
|
|
.sort(sortPopularity);
|
|
|
|
creditedShows.value = cast
|
|
.filter(credit => credit.type === MediaTypes.Show)
|
|
.filter(alreadyExists)
|
|
.sort(sortPopularity);
|
|
}
|
|
|
|
function fetchPerson() {
|
|
if (!props.id) {
|
|
console.error("Unable to fetch person, missing id!"); // eslint-disable-line no-console
|
|
return;
|
|
}
|
|
|
|
getPerson(props.id)
|
|
.then(setPerson)
|
|
.then(() => getPersonCredits(person.value?.id))
|
|
.then(setCredits)
|
|
.then(() => personCreditedFrom(credits.value?.cast));
|
|
}
|
|
|
|
// On create functions
|
|
fetchPerson();
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import "scss/loading-placeholder";
|
|
@import "scss/variables";
|
|
@import "scss/media-queries";
|
|
@import "scss/main";
|
|
|
|
section.person {
|
|
overflow: hidden;
|
|
position: relative;
|
|
padding: 40px;
|
|
background-color: var(--background-color);
|
|
|
|
@include mobile {
|
|
padding: 50px 20px 10px;
|
|
}
|
|
|
|
&:before {
|
|
content: "";
|
|
display: block;
|
|
position: absolute;
|
|
top: -130px;
|
|
left: -100px;
|
|
z-index: 1;
|
|
width: 1000px;
|
|
height: 500px;
|
|
transform: rotate(21deg);
|
|
background-color: #062541;
|
|
|
|
@include mobile {
|
|
// top: -52vw;
|
|
top: -215px;
|
|
}
|
|
}
|
|
}
|
|
|
|
header {
|
|
$duration: 0.2s;
|
|
transition: height $duration ease;
|
|
position: relative;
|
|
background-color: transparent;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
height: 350px;
|
|
z-index: 2;
|
|
|
|
@include mobile {
|
|
height: 180px;
|
|
}
|
|
|
|
.info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 30px;
|
|
padding-left: 0;
|
|
text-align: left;
|
|
|
|
@include mobile {
|
|
padding: 0;
|
|
}
|
|
}
|
|
|
|
h1 {
|
|
color: $green;
|
|
width: 100%;
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
font-size: 30px;
|
|
margin-top: 0;
|
|
|
|
@include mobile {
|
|
font-size: 24px;
|
|
margin: 10px 0;
|
|
// padding: 30px 30px 30px 40px;
|
|
}
|
|
}
|
|
|
|
.known-for {
|
|
color: rgba(255, 255, 255, 0.8);
|
|
font-size: 1.2rem;
|
|
}
|
|
}
|
|
|
|
.person__poster {
|
|
display: block;
|
|
margin: auto;
|
|
width: fit-content;
|
|
border-radius: 10px;
|
|
background-color: grey;
|
|
animation: pulse 1s infinite ease-in-out;
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
background-color: rgba(165, 165, 165, 0.1);
|
|
}
|
|
50% {
|
|
background-color: rgba(165, 165, 165, 0.3);
|
|
}
|
|
100% {
|
|
background-color: rgba(165, 165, 165, 0.1);
|
|
}
|
|
}
|
|
|
|
> img {
|
|
border-radius: 10px;
|
|
width: 100%;
|
|
|
|
@include mobile {
|
|
max-width: 225px;
|
|
}
|
|
}
|
|
}
|
|
</style>
|