138 lines
3 KiB
Vue
138 lines
3 KiB
Vue
<template>
|
||
<table class="table is-fullwidth is-striped">
|
||
<thead>
|
||
<tr>
|
||
<th>title</th>
|
||
<th>year</th>
|
||
<th>type</th>
|
||
<th>rating</th>
|
||
<th>runtime</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in items" :key="item.id" :data-unwind-id="item.id">
|
||
<td v-if="item.original_title">
|
||
<a :href="`https://www.imdb.com/title/${item.imdb_id}/`">
|
||
<span>{{ item.original_title }}</span>
|
||
</a>
|
||
<span v-if="item.title != item.original_title"> ({{ item.title }})</span>
|
||
</td>
|
||
<td v-else>
|
||
<a :href="`https://www.imdb.com/title/${item.imdb_id}/`">
|
||
<span>{{ item.title }}</span>
|
||
</a>
|
||
</td>
|
||
<td>
|
||
<span class="year">{{ item.release_year }}</span>
|
||
</td>
|
||
<td>
|
||
<span class="mediatype">{{ item.media_type }}</span>
|
||
</td>
|
||
<td>
|
||
<span
|
||
class="score imdb-score tag is-large"
|
||
:title="`IMDb rating (1-10) / ${item.imdb_votes} votes`"
|
||
>{{ imdb_rating(item.imdb_score) }}</span
|
||
>
|
||
<span
|
||
class="score tag is-info is-large"
|
||
:title="`User rating (1-10) / ${
|
||
item.user_scores.length
|
||
} votes / σ = ${imdb_stdev(item.user_scores)}`"
|
||
>{{ avg_imdb_rating(item.user_scores) }}</span
|
||
>
|
||
</td>
|
||
<td>
|
||
<span>{{ duration(item.runtime) }}</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div ref="sentinel"></div>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { defineComponent } from "vue"
|
||
import { mean, pstdev } from "../utils.ts"
|
||
|
||
function avg_imdb_rating(scores) {
|
||
return imdb_rating(scores.length === 0 ? null : mean(scores))
|
||
}
|
||
|
||
function imdb_stdev(scores) {
|
||
return pstdev(scores.map(imdb_rating_from_score))
|
||
}
|
||
|
||
function imdb_rating_from_score(score) {
|
||
return Math.round((score * 9) / 10 + 10) / 10
|
||
}
|
||
|
||
function imdb_rating(score) {
|
||
if (score == null) {
|
||
return "-"
|
||
}
|
||
|
||
const deci = 10 * imdb_rating_from_score(score)
|
||
return `${(deci / 10) | 0}.${deci % 10}`
|
||
}
|
||
|
||
function duration(minutes_total) {
|
||
if (minutes_total == null) {
|
||
return "-"
|
||
}
|
||
|
||
const m = minutes_total % 60
|
||
const h = (minutes_total / 60) | 0
|
||
return `${h} h ${m} m`
|
||
}
|
||
|
||
export default defineComponent({
|
||
props: {
|
||
items: {
|
||
type: Array,
|
||
required: true,
|
||
},
|
||
},
|
||
emits: ["reach-bottom"],
|
||
mounted() {
|
||
const options = {
|
||
rootMargin: "500px",
|
||
}
|
||
const observer = new IntersectionObserver(([e]) => {
|
||
if (!e.isIntersecting) {
|
||
return
|
||
}
|
||
this.$emit("reach-bottom")
|
||
}, options)
|
||
observer.observe(this.$refs.sentinel)
|
||
},
|
||
methods: {
|
||
avg_imdb_rating,
|
||
imdb_rating,
|
||
imdb_stdev,
|
||
duration,
|
||
},
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.year {
|
||
color: grey;
|
||
}
|
||
.mediatype {
|
||
color: grey;
|
||
}
|
||
.score {
|
||
width: 2em;
|
||
height: 2em;
|
||
}
|
||
.score + .score {
|
||
margin-left: 1em;
|
||
}
|
||
.imdb-score {
|
||
background-color: rgb(245, 197, 24);
|
||
}
|
||
.user-score {
|
||
}
|
||
</style>
|