<template> <div v-if="timeline" class="timeline" > <ul class="timeline-items nolist"> <li v-for="chapter in timeline" :key="`chapter-${chapter.id}`" :style="{ left: `${(chapter.time / duration) * 100}%` }" :title="formatDuration(chapter.time)" class="timeline-item" ><a :href="`/tag/${chapter.tags[0].slug}`" class="link" >{{ chapter.tags[0]?.name || ' ' }}</a></li> </ul> </div> <ul v-else class="chapters nolist" > <li v-for="chapter in chapters" :key="`chapter-${chapter.id}`" class="chapter" > <img :src="getPath(chapter.poster, 'thumbnail')" :style="{ 'background-image': `url('${getPath(chapter.poster, 'lazy')}'` }" loading="lazy" class="chapter-poster" > <span class="chapter-details"> <span v-if="typeof chapter.time === 'number'" v-tooltip="'Time in video'" class="chapter-time" ><Icon icon="film3" /> {{ formatDuration(chapter.time) }}</span> <span v-if="chapter.duration" v-tooltip="'Duration'" class="chapter-duration" ><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span> </span> <div class="chapter-info"> <h3 v-if="chapter.title" class="chapter-row chapter-title" :title="chapter.title" >{{ chapter.title }}</h3> <p v-if="chapter.description" class="chapter-row chapter-description" >{{ chapter.description }}</p> <ul class="chapter-tags chapter-row nolist"> <li v-for="tag in chapter.tags" :key="`chapter-tag-${tag.slug}`" class="chapter-tag" > <a :href="`/tag/${tag.slug}`" class="link" >{{ tag.name }}</a> </li> </ul> </div> </li> </ul> </template> <script setup> import { computed } from 'vue'; import getPath from '#/src/get-path.js'; import { formatDuration } from '#/utils/format.js'; const props = defineProps({ chapters: { type: Array, default: () => [], }, }); const lastChapter = props.chapters.at(-1); const duration = lastChapter.time + lastChapter.duration; const timeline = computed(() => { if (props.chapters.every((chapter) => chapter.time)) { return props.chapters.filter((chapter) => chapter.tags?.length > 0); } return null; }); </script> <style scoped> .chapters { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: .5rem; } .chapter { display: flex; flex-direction: column; background: var(--background); box-shadow: 0 0 3px var(--shadow-weak-30); border-radius: .25rem; margin: 0 0 .5rem 0; font-size: 0; overflow: hidden; } .chapter-poster { width: 100%; height: 10rem; object-fit: cover; object-position: center; } .chapter-details { height: 1.75rem; display: flex; justify-content: space-between; align-items: center; padding: 0 .5rem; border-radius: 0 0 .25rem .25rem; margin: 0 0 .5rem 0; color: var(--text-light); background: var(--grey-dark-40); font-size: .8rem; font-weight: bold; .icon { fill: var(--text-light); margin-right: .5rem; } } .chapter-duration, .chapter-time { display: flex; align-items: center; } .chapter-duration .icon { /* narrower icon */ margin: -.1rem .3rem 0 0; } .chapter-info { padding: 0 .5rem; font-size: 1rem; } .chapter-row { margin: 0 0 .5rem 0; } .chapter-title { padding: 0; font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .chapter-description { line-height: 1.5; } .chapter-tags { overflow: hidden; } .chapter-tag { margin: 0 .5rem .25rem 0; font-size: .75rem; color: var(--glass-strong-10); .link { color: inherit; text-decoration: none; } &:hover { color: var(--primary); } } .timeline-items { position: relative; height: 5rem; border-bottom: solid 1px var(--shadow-weak); } .timeline-item { position: absolute; bottom: -.25rem; padding: .1rem .5rem; border-radius: .6rem; color: var(--primary); background: var(--background); transform: rotate(-60deg); transform-origin: 0 50%; box-shadow: 0 0 3px var(--shadow-weak); font-size: .8rem; font-weight: bold; .link { color: inherit; } &:before { content: ''; display: inline-block; width: 1rem; height: 2px; position: absolute; left: calc(-1rem + 1px); margin: .3rem .5rem 0 0; background: var(--primary); } } @media(--small-30) { .chapters { grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); } } </style>