<template> <div class="tile" :class="{ unstashed: !favorited && pageStash && user && pageStash.id === user.primaryStash?.id }" > <div class="poster-container"> <Link :href="`/scene/${scene.id}/${scene.slug}`" target="_blank" class="poster" > <img v-if="scene.poster" :src="scene.poster.isS3 ? `https://cdndev.traxxx.me/${scene.poster.thumbnail}` : `/media/${scene.poster.thumbnail}`" :style="{ 'background-image': scene.poster.isS3 ? `url(https://cdndev.traxxx.me/${scene.poster.lazy})` : `url(/media/${scene.poster.lazy})` }" loading="lazy" class="thumbnail" > <Icon v-else icon="clapboard" /> </Link> <Icon v-show="favorited" icon="heart7" class="heart favorited" @click.native.stop="unstash" /> <Icon v-show="!favorited && user" icon="heart8" class="heart" @click.native.stop="stash" /> </div> <div class="meta"> <div class="channel"> <Link :href="scene.channel.isIndependent || !scene.network ? `/${scene.channel.type}/${scene.channel.slug}` : `/${scene.network.type}/${scene.network.slug}`" class="favicon-link" > <img :src="scene.channel.isIndependent || !scene.network ? `/logos/${scene.channel.slug}/favicon.png` : `/logos/${scene.network.slug}/favicon.png`" class="favicon" > </Link> <Link :href="`/${scene.channel.type}/${scene.channel.slug}`" class="nolink channel-link" >{{ scene.channel.name }}</Link> </div> <time :datetime="scene.effectiveDate.toISOString()" class="date" >{{ format(scene.effectiveDate, 'MMM d, y') }}</time> </div> <Link :href="`/scene/${scene.id}/${scene.slug}`" :title="scene.title" target="_blank" class="row title nolink" >{{ scene.title }}</Link> <ul class="row actors nolist" :title="scene.actors.map((actor) => actor.name).join(', ')" > <li v-for="actor in scene.actors" :key="`actor-${scene.id}-${actor.id}`" class="actor" > <Link :href="`/actor/${actor.id}/${actor.slug}`" class="nolink" >{{ actor.name }}</Link> </li> </ul> <ul class="row tags nolist" :title="scene.tags.map((tag) => tag.name).join(', ')" > <li v-for="tag in scene.tags" :key="`tag-${scene.id}-${tag.id}`" class="tag" > <Link :href="`/tag/${tag.slug}`" class="nolink" >{{ tag.name }}</Link> </li> </ul> </div> </template> <script setup> import { ref, inject } from 'vue'; import { format } from 'date-fns'; import { post, del } from '#/src/api.js'; import events from '#/src/events.js'; import ellipsis from '#/utils/ellipsis.js'; import Icon from '../icon/icon.vue'; const props = defineProps({ scene: { type: Object, default: null, }, }); const pageContext = inject('pageContext'); const user = pageContext.user; const pageStash = pageContext.pageProps.stash; const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.primary)); const fbCutoff = 20; async function stash() { try { favorited.value = true; await post(`/stashes/${user.primaryStash.id}/scenes`, { sceneId: props.scene.id }); events.emit('feedback', { type: 'success', message: `"${ellipsis(props.scene.title, fbCutoff)}" stashed to ${user.primaryStash.name}`, }); } catch (error) { favorited.value = false; events.emit('feedback', { type: 'error', message: `Failed to stash "${ellipsis(props.scene.title, fbCutoff)}" to ${user.primaryStash.name}`, }); } } async function unstash() { try { favorited.value = false; await del(`/stashes/${user.primaryStash.id}/scenes/${props.scene.id}`); events.emit('feedback', { type: 'remove', message: `"${ellipsis(props.scene.title, fbCutoff)}" unstashed from ${user.primaryStash.name}`, }); } catch (error) { favorited.value = true; console.error(error); events.emit('feedback', { type: 'error', message: `Failed to unstash "${ellipsis(props.scene.title, fbCutoff)}" from ${user.primaryStash.name}`, }); } } </script> <style scoped> .tile { width: 100%; overflow: hidden; background: var(--background-base); border-radius: .25rem; box-shadow: 0 0 3px var(--shadow-weak-30); &:hover { box-shadow: 0 0 3px var(--shadow-weak-20); .heart { fill: var(--text-light); } } &.unstashed { opacity: .5; } } .poster-container { position: relative; overflow: hidden; font-size: 0; } .poster { display: flex; justify-content: center; align-items: center; aspect-ratio: 16/9; border-radius: .25rem .25rem 0 0; overflow: hidden; background: var(--background-dark-20); .icon { width: 3rem; height: 3rem; fill: var(--shadow-weak-40); } } .thumbnail { width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; background-size: cover; background-position: center; } .icon.heart { width: 2rem; height: 1.5rem; position: absolute; top: 0; right: 0; padding: .5rem .5rem 1rem 1rem; fill: var(--highlight-strong-10); filter: drop-shadow(0 0 3px var(--shadow)); &:hover { cursor: pointer; fill: var(--primary); } &.favorited { fill: var(--primary); } } .meta { display: flex; justify-content: space-between; align-items: center; padding: .4rem .5rem; border-radius: 0 0 .25rem .25rem; margin-bottom: .5rem; font-size: .8rem; color: var(--text-light); background: var(--shadow-strong-30); box-shadow: 0 0 3px var(--shadow); } .channel { display: inline-flex; align-items: center; font-weight: bold; } .favicon-link { display: inline-flex; } .favicon { width: 1rem; height: 1rem; margin-right: .5rem; object-fit: contain; } .row { margin: 0 .5rem .25rem .5rem; font-size: .9rem; } .title { display: block; margin-bottom: .4rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: bold; } .actor:hover, .tag:hover { color: var(--primary); } .actors { height: 1rem; overflow: hidden; white-space: pre-wrap; } .actor { &:not(:last-child)::after { content: ',\0020'; } } .tags { height: 1.25rem; overflow: hidden; } .tag { margin: 0 .5rem .25rem 0; padding: .1rem 0; color: var(--shadow-strong-10); font-size: .75rem; } </style>