Files
seasoned/src/components/popup/Person.vue
Kevin 8e586811ec Feat: vite & upgraded dependencies (#100)
* 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
2026-02-23 20:53:19 +01:00

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>