Reinitialized commit. Update and actors overview with some filters.

This commit is contained in:
2023-12-30 06:29:53 +01:00
commit 3f099b5e95
1208 changed files with 134732 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<template>
<ul class="actors nolist">
<li
v-for="actor in actors"
:key="`actor-${actor.id}`"
class="actor"
>
<ActorTile :actor="actor" />
</li>
</ul>
</template>
<script setup>
import ActorTile from './tile.vue';
defineProps({
actors: {
type: Array,
default: () => [],
},
});
</script>
<style scoped>
.actors {
display: grid;
grid-template-columns: repeat(auto-fill, 10rem);
gap: .5rem;
}
</style>

39
components/actors/gender.vue Executable file
View File

@@ -0,0 +1,39 @@
<template>
<span
v-if="gender"
class="gender"
:class="{ [gender]: true }"
>
<Icon :icon="gender" />
</span>
</template>
<script setup>
defineProps({
gender: {
type: String,
default: null,
},
});
</script>
<style scoped>
.gender {
&.female .icon {
fill: var(--female);
filter: drop-shadow(0 0 1px var(--shadow-weak-20));
}
&.male .icon {
fill: var(--male);
filter: drop-shadow(0 0 1px var(--shadow-weak-20));
}
&.transsexual .icon {
fill: var(--text-light);
filter: drop-shadow(1px 0 0 var(--female)) drop-shadow(-1px 0 0 var(--female)) drop-shadow(0 1px 0 var(--female)) drop-shadow(0 -1px 0 var(--female))
drop-shadow(1px 0 0 var(--male)) drop-shadow(-1px 0 0 var(--male)) drop-shadow(0 1px 0 var(--male)) drop-shadow(0 -1px 0 var(--male))
drop-shadow(0 0 1px var(--shadow-weak-20))
}
}
</style>

147
components/actors/tile.vue Normal file
View File

@@ -0,0 +1,147 @@
<template>
<div class="actor">
<span class="name">{{ actor.name }}</span>
<Link
:href="`/actor/${actor.id}/${actor.slug}`"
class="avatar-link no-link"
>
<img
v-if="actor.avatar"
:src="actor.avatar.isS3 ? `https://cdndev.traxxx.me/${actor.avatar.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
:style="{ 'background-image': actor.avatar.isS3 ? `url(https://cdndev.traxxx.me/${actor.avatar.lazy})` : `url(/media/${actor.avatar.lazy})` }"
class="avatar"
>
</Link>
<div class="details">
<span class="birth">
<Gender :gender="actor.gender" />
<span
v-if="actor.ageFromBirth"
:title="`Born ${formatDate(actor.dateOfBirth, 'MMMM d, yyyy')}`"
class="age"
>{{ actor.ageFromBirth }}</span>
<span
v-if="actor.ageThen && actor.ageThen < actor.ageFromBirth"
class="age age-then"
>{{ actor.ageThen }}</span>
</span>
<span
v-if="actor.birthCountry"
:title="`Born in ${actor.birthCountry.name}`"
class="country"
>
{{ actor.birthCountry.alpha2 }}
<img
:src="`/flags/${actor.birthCountry.alpha2.toLowerCase()}.svg`"
class="flag"
>
</span>
</div>
</div>
</template>
<script setup>
import { formatDate } from '#/src/format.js';
import Gender from './gender.vue';
defineProps({
actor: {
type: Object,
default: null,
},
});
</script>
<style scoped>
.actor {
display: flex;
flex-direction: column;
width: 10rem;
height: 15rem;
position: relative;
border-radius: .25rem;
margin: .25rem;
box-shadow: 0 0 3px var(--shadow-weak-30);
&:hover {
box-shadow: 0 0 3px var(--shadow-weak-20);
.name {
color: var(--primary);
}
}
}
.name {
padding: .25rem .5rem;
font-weight: bold;
font-size: .9rem;
user-select: all;
}
.avatar-link {
display: block;
flex-grow: 1;
overflow: hidden;
}
.avatar {
height: 100%;
width: 100%;
object-fit: cover;
object-position: center 0;
background-size: cover;
background-position: center 0;
}
.details {
width: 100%;
height: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 0;
padding: 0 .5rem;
color: var(--text-light);
background: var(--shadow);
font-size: .9rem;
font-weight: bold;
}
.gender {
display: inline-block;
padding: .25rem 0;
margin-right: .25rem;
transform: translateY(1px);
}
.birth {
display: flex;
align-items: center;
}
.age {
margin-right: .25rem;
}
.age-then {
color: var(--shadow-weak-10);
}
.country {
display: flex;
align-items: center;
}
.flag {
height: .75rem;
margin-left: .25rem;
}
</style>

208
components/filters/range.vue Executable file
View File

@@ -0,0 +1,208 @@
<template>
<div class="filter-section">
<label class="filter-label noselect">
<span class="label">
<Checkbox
:checked="!disabled"
class="checkbox"
@change="(checked) => $emit('enable', checked)"
/>{{ label }}
</span>
<span
v-if="!disabled"
class="label-values"
>{{ value[0] }} - {{ value[1] }}<template v-if="unit">&nbsp;{{ unit }}</template></span>
</label>
<span class="filter-split">
<Range
:min="min"
:max="max"
:value="value"
:values="values"
:disabled="disabled"
:allow-enable="allowEnable"
@enable="emit('enable', true)"
@input="(range) => emit('input', range)"
@change="(range) => emit('change', range)"
>
<template #start><slot name="start" /></template>
<template #end><slot name="end" /></template>
</Range>
</span>
</div>
</template>
<script setup>
import Checkbox from '../form/checkbox.vue';
import Range from '../form/range.vue';
defineProps({
label: {
type: String,
default: null,
},
value: {
type: Array,
default: () => [0, 10],
},
values: {
type: Array,
default: null,
},
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 10,
},
unit: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
allowEnable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['change', 'input', 'enable']);
</script>
<style scoped>
.filter-section {
width: 15rem;
max-width: 100%;
border-bottom: solid 1px var(--shadow-hint);
}
.filter-label {
display: flex;
justify-content: space-between;
padding: .75rem .5rem .5rem .5rem;
color: var(--shadow);
font-weight: bold;
font-size: .9rem;
.label {
display: inline-flex;
align-items: center;
text-transform: capitalize;
}
.checkbox {
margin: 0 .75rem 0 0;
}
.icon {
margin: 0 .5rem 0 0;
}
}
.label-values {
font-weight: normal;
}
.filter-split {
display: flex;
align-items: center;
}
.toggle-container,
.range-container {
display: flex;
flex-grow: 1;
align-items: center;
padding: .5rem 0;
&.on {
.toggle-label.on {
color: var(--enabled);
.icon {
fill: var(--enabled);
}
}
.toggle {
background-color: var(--enabled-background);
&::-webkit-slider-thumb {
background: var(--enabled);
}
&::-moz-range-thumb {
background: var(--enabled);
}
}
}
&.off {
.toggle-label.off {
color: var(--disabled);
.icon {
fill: var(--disabled);
}
}
.toggle {
background-color: var(--disabled-background);
&::-webkit-slider-thumb {
background: var(--disabled);
}
&::-moz-range-thumb {
background: var(--disabled);
}
}
}
}
.toggle-label {
display: inline-flex;
justify-content: center;
min-width: 1.5rem;
flex-shrink: 0;
padding: 0 .5rem;
color: var(--shadow);
font-weight: bold;
font-size: .9rem;
&.on {
text-align: right;
}
.icon {
fill: var(--shadow);
}
&:hover {
cursor: pointer;
&.on {
color: var(--enabled);
.icon {
fill: var(--enabled);
}
}
&.off {
color: var(--disabled);
.icon {
fill: var(--disabled);
}
}
}
}
</style>

102
components/form/checkbox.vue Executable file
View File

@@ -0,0 +1,102 @@
<template>
<label class="check-container noselect">
<span
v-if="label"
class="check-label"
>{{ label }}</span>
<input
v-show="false"
:id="`checkbox-${uid}`"
:checked="checked"
type="checkbox"
class="check-checkbox"
@change="$emit('change', $event.target.checked)"
>
<label
:for="`checkbox-${uid}`"
class="check"
/>
</label>
</template>
<script setup>
const uid = String(Math.random()).slice(2);
defineProps({
checked: {
type: Boolean,
default: false,
},
label: {
type: String,
default: null,
},
});
defineEmits(['change']);
</script>
<style scoped>
.check-container {
display: flex;
justify-content: space-between;
cursor: pointer;
}
.check {
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
background-color: var(--shadow-weak-30);
border-radius: .25rem;
cursor: pointer;
transition: background .15s ease;
&::after {
content: '';
width: .5rem;
height: .3rem;
border: solid 2px var(--text-light);
border-top: none;
border-right: none;
margin: -.2rem 0 0 0;
transform: rotateZ(-45deg) scaleX(0);
transition: transform .15s ease;
}
}
.check-cross .check::before {
content: '';
width: 100%;
height: 100%;
position: absolute;
background: url('/img/icons/cross3.svg') no-repeat center/80%;
opacity: .15;
transition: transform .1s ease;
}
.check-checkbox:checked + .check {
background: var(--primary);
&::after {
transform: rotateZ(-45deg) scaleX(1);
}
&::before {
transform: scaleX(0);
}
}
.check-label {
overflow: hidden;
text-transform: capitalize;
text-overflow: ellipsis;
margin: 0 .5rem 0 0;
}
</style>

225
components/form/range.vue Executable file
View File

@@ -0,0 +1,225 @@
<template>
<div class="range-container">
<div
class="label label-start"
:class="{ disabled }"
@click="setRange(range.a === minValue ? 'a' : 'b', min)"
>
<slot name="start" />
</div>
<div
class="range"
:class="{ disabled }"
:style="{ background: `linear-gradient(90deg, var(--slider-track) ${minPercentage}%, var(--slider-range) ${minPercentage}%, var(--slider-range) ${maxPercentage}%, var(--slider-track) ${maxPercentage}%)` }"
@click="setNearest"
>
<input
v-model.number="range.a"
:min="min"
:max="max"
:data-value="range.a"
:disabled="disabled"
type="range"
class="slider"
@input="set('input')"
@change="set('change')"
@click.stop
>
<input
v-model.number="range.b"
:min="min"
:max="max"
:data-value="range.b"
:disabled="disabled"
type="range"
class="slider"
@input="set('input')"
@change="set('change')"
@click.stop
>
</div>
<div
class="label label-end"
:class="{ disabled }"
@click="setRange(range.b === maxValue ? 'b' : 'a', max)"
>
<slot name="end" />
</div>
</div>
</template>
<script setup>
import {
ref,
computed,
nextTick,
} from 'vue';
const props = defineProps({
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 10,
},
value: {
type: Array,
default: () => [3, 7],
},
values: {
type: Array,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
allowEnable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['change', 'input', 'enable']);
const range = ref({
a: props.values ? props.values.indexOf(props.value[0]) : props.value[0],
b: props.values ? props.values.indexOf(props.value[1]) : props.value[1],
});
const minValue = computed(() => Math.min(range.value.a, range.value.b));
const maxValue = computed(() => Math.max(range.value.a, range.value.b));
const minPercentage = computed(() => ((minValue.value - props.min) / (props.max - props.min)) * 100);
const maxPercentage = computed(() => ((maxValue.value - props.min) / (props.max - props.min)) * 100);
function set(type = 'change') {
if (props.values) {
emit(type, [props.values[minValue.value], props.values[maxValue.value]]);
return;
}
emit(type, [minValue.value, maxValue.value]);
}
async function setNearest(event) {
if (props.allowEnable) {
emit('enable');
}
await nextTick;
if (!props.disabled) {
const closestValue = Math.round((event.offsetX / event.target.getBoundingClientRect().width) * (props.max - props.min)) + props.min;
const closestSlider = Math.abs(range.value.a - closestValue) < Math.abs(range.value.b - closestValue) ? 'a' : 'b';
range.value[closestSlider] = closestValue;
emit('change', [minValue.value, maxValue.value]);
}
}
async function setRange(prop, value) {
if (props.allowEnable) {
emit('enable');
}
await nextTick;
if (!props.disabled) {
range.value[prop] = value;
emit('change', [minValue.value, maxValue.value]);
}
}
</script>
<style>
.dark .range-container .range {
--slider-range: var(--lighten-weak-10);
}
</style>
<style scoped>
.range-container {
display: flex;
justify-content: space-between;
}
.range {
--slider-track: var(--shadow-weak-30);
--slider-range: var(--primary-faded);
--slider-thumb: var(--primary);
position: relative;
height: 1.25rem;
flex-grow: 1;
border-radius: .625rem;
&.disabled {
--slider-range: var(--shadow-weak-10);
--slider-thumb: var(--grey-dark-10);
}
}
.slider {
width: 100%;
top: 0;
margin: 0;
appearance: none;
position: absolute;
background: none;
outline: none;
pointer-events: none;
}
.slider::-webkit-slider-thumb {
appearance: none;
display: block;
width: 1.25rem;
height: 1.25rem;
border: none;
border-radius: 50%;
background: var(--slider-thumb);
pointer-events: visible;
cursor: pointer;
box-shadow: 0 0 3px var(--shadow-weak-10);
}
.slider::-moz-range-thumb {
appearance: none;
display: block;
width: 1.25rem;
height: 1.25rem;
border: none;
border-radius: 50%;
background: var(--slider-thumb);
pointer-events: visible;
cursor: pointer;
box-shadow: 0 0 3px var(--shadow-weak-10);
transform: translateY(2px);
}
.label {
padding: 0 .5rem;
&:hover:not(.disabled) {
cursor: pointer;
::v-deep(.icon) {
fill: var(--primary);
}
}
}
::v-deep(.icon) {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
fill: var(--shadow);
}
</style>

115
components/form/toggle.vue Executable file
View File

@@ -0,0 +1,115 @@
<template>
<label
class="toggle-container noselect"
:class="{ light: $store.state.ui.theme === 'dark' }"
>
<input
:id="`toggle-${id}`"
:checked="checked"
:true-value="trueValue"
:false-value="falseValue"
:disabled="disabled"
type="checkbox"
class="toggle-input"
@change="$emit('change', $event.target.checked)"
>
<label
:for="`toggle-${id}`"
class="toggle"
/>
</label>
</template>
<script>
export default {
props: {
checked: {
type: Boolean,
default: false,
},
trueValue: {
type: null,
default: true,
},
falseValue: {
type: null,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['change'],
data() {
return {
id: Math.floor(new Date().getTime() * Math.random()),
};
},
};
</script>
<style lang="scss" scoped>
@import 'breakpoints';
.toggle-container {
display: inline-block;
cursor: pointer;
&.light {
.toggle {
background: var(--lighten-weak);
}
.toggle-input:checked + .toggle {
background: var(--lighten);
}
}
}
.toggle {
width: 2rem;
height: .9rem;
display: flex;
align-items: center;
position: relative;
background: var(--shadow-hint);
border-radius: 1rem;
cursor: pointer;
&::after {
content: '';
background-color: var(--background-light);
width: 1rem;
height: 1rem;
display: inline-block;
position: absolute;
left: 0;
border-radius: 50%;
box-shadow: 0 0 2px var(--darken-strong);
transition: background-color .2s ease, left .2s ease;
}
}
.toggle-input {
display: none;
&:checked + .toggle {
background: var(--primary-faded);
&::after {
background: var(--primary);
left: calc(100% - 1rem);
}
}
&[disabled] + .toggle {
background: var(--shadow-weak);
&::after {
background: var(--shadow);
}
}
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<header class="header">
<Link href="/">
<h1 class="title">
<div
class="logo"
v-html="logo"
/>
</h1>
</Link>
<nav class="nav">
<ul class="nav-list nolist">
<li class="nav-item">
<Link
class="link"
href="/updates"
>Updates</Link>
</li>
<li class="nav-item">
<Link
class="link"
href="/actors"
>Actors</Link>
</li>
<li class="nav-item">
<Link
class="link"
href="/channels"
>Channels</Link>
</li>
<li class="nav-item">
<Link
class="link"
href="/tags"
>Tags</Link>
</li>
<li class="nav-item">
<Link
class="link"
href="/movies"
>Movies</Link>
</li>
</ul>
</nav>
</header>
</template>
<script setup>
import logo from '../../assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved
</script>
<style scoped>
.header {
display: flex;
align-items: center;
box-shadow: 0 0 3px var(--shadow-weak-10);
}
.title {
margin: 0;
display: inline-block;
}
.logo {
display: flex;
width: 8rem;
height: 3rem;
padding: .75rem;
margin-right: 1rem;
fill: var(--primary);
}
.nav {
display: inline-block;
}
.nav-item .link {
font-size: .9rem;
color: var(--shadow-strong-10);
padding: 1rem;
height: 100%;
&:hover {
text-decoration: none;
}
}
.link {
font-weight: bold;
&.active {
color: var(--primary);
}
}
</style>

58
components/icon/icon.vue Executable file
View File

@@ -0,0 +1,58 @@
<template>
<div
:class="{ active }"
:title="title"
class="icon"
v-html="svg"
/>
</template>
<script setup>
import { ref } from 'vue';
const svg = ref(null);
const props = defineProps({
icon: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
active: {
type: Boolean,
default: false,
},
});
import(`../../assets/img/icons/${props.icon}.svg?raw`).then((raw) => {
svg.value = raw.default;
});
</script>
<style>
.icon {
fill: var(--text);
display: inline-block;
flex-shrink: 0;
width: 1rem;
height: 1rem;
font-size: 0; /* prevent flashing null before icon is loaded */
svg {
width: 100%;
height: 100%;
}
&.active {
fill: var(--shadow);
&:hover {
fill: var(--text);
cursor: pointer;
}
}
}
</style>

21
components/link/link.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<a
:class="{ active: pageContext.urlPathname === $attrs.href || active }"
rel="noopener noreferrer"
>
<slot />
</a>
</template>
<script setup>
import { inject } from 'vue';
defineProps({
active: {
type: Boolean,
default: false,
},
});
const pageContext = inject('pageContext');
</script>

View File

@@ -0,0 +1,175 @@
<template>
<nav class="pagination">
<ul class="pages nolist">
<li>
<Link
:href="`/updates/${routeParams?.scope}/1`"
:class="{ disabled: !hasPrevPage }"
class="page first nolink"
><Icon icon="first2" /></Link>
</li>
<li>
<Link
:href="hasPrevPage ? `/updates/${routeParams?.scope}/${currentPage - 1}` : null"
:class="{ disabled: !hasPrevPage }"
class="page prev nolink"
><Icon icon="arrow-left" /></Link>
</li>
</ul>
<div class="index">
<ul class="pages before wrap nolist">
<li
v-for="page in prevPages"
:key="`page-${page}`"
>
<Link
:href="`/updates/${routeParams?.scope}/${page}`"
class="page nolink"
:class="{ active: page === currentPage }"
>{{ page }}</Link>
</li>
</ul>
<ul class="pages nolist">
<li>
<div class="page active">{{ currentPage }}</div>
</li>
</ul>
<ul class="pages after wrap nolist">
<li
v-for="page in nextPages"
:key="`page-${page}`"
>
<Link
:href="`/updates/${routeParams?.scope}/${page}`"
class="page nolink"
:class="{ active: page === currentPage }"
>{{ page }}</Link>
</li>
</ul>
</div>
<ul class="pages nolist">
<li>
<Link
:href="hasNextPage ? `/updates/${routeParams?.scope}/${currentPage + 1}` : null"
:class="{ disabled: !hasNextPage }"
class="page next nolink"
><Icon icon="arrow-right" /></Link>
</li>
<li>
<Link
:href="`/updates/${routeParams?.scope}/${pageTotal}`"
:class="{ disabled: !hasNextPage }"
class="page last nolink"
><Icon icon="last2" /></Link>
</li>
</ul>
</nav>
</template>
<script setup>
import { inject } from 'vue';
const { routeParams, pageProps } = inject('pageContext');
const currentPage = Number(routeParams?.page);
const limit = Number(pageProps.limit) || 30;
const total = Number(pageProps.total);
const pageTotal = Math.ceil(total / limit);
const hasNextPage = currentPage + 1 <= pageTotal;
const hasPrevPage = currentPage - 1 >= 1;
console.log(routeParams);
const prevPages = Array.from({ length: 4 }, (value, index) => {
const page = currentPage - index - 1;
if (page < 1) {
return null;
}
return page;
}).filter(Boolean);
const nextPages = Array.from({ length: 4 }, (value, index) => {
const page = currentPage + index + 1;
if (page > pageTotal) {
return null;
}
return page;
}).filter(Boolean);
console.log(total, limit, currentPage);
console.log(prevPages);
console.log(nextPages);
</script>
<style scoped>
.pagination {
height: 5rem;
display: flex;
justify-content: center;
padding: 1rem;
font-size: 0;
overflow: hidden;
}
.pages {
display: flex;
}
.wrap {
flex-wrap: wrap;
}
.before {
flex-direction: row-reverse;
}
.index {
display: flex;
justify-content: center;
}
.page {
width: 3rem;
height: 3rem;
display: inline-flex;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
background: var(--background);
box-shadow: 0 0 3px var(--shadow-weak-30);
color: var(--shadow);
font-weight: bold;
font-size: 1rem;
.icon {
width: .9rem;
height: .9rem;
fill: var(--shadow);
}
&.active {
color: var(--primary);
}
&.disabled .icon {
fill: var(--shadow-weak-20);
}
}
.prev {
margin-right: .5rem;
}
.next {
margin-left: .5rem;
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="page">
<nav class="scopes">
<Link
href="/updates/latest"
class="scope nolink"
:active="scope === 'latest'"
>Latest</Link>
<Link
href="/updates/upcoming"
class="scope nolink"
:active="scope === 'upcoming'"
>Upcoming</Link>
<Link
href="/updates/new"
class="scope nolink"
:active="scope === 'new'"
>New</Link>
</nav>
<ul class="scenes nolist">
<li
v-for="scene in scenes"
:key="scene.id"
>
<Scene :scene="scene" />
</li>
</ul>
<Pagination />
</div>
</template>
<script setup>
import { defineProps, inject } from 'vue';
import Scene from './tile.vue';
import Pagination from '../pagination/pagination.vue';
const { routeParams } = inject('pageContext');
const { scope } = routeParams;
defineProps({
scenes: {
type: Array,
default: () => [],
},
});
</script>
<style scoped>
.page {
background: var(--background-base-10);
}
.scenes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
gap: .75rem .5rem;
padding: 1rem;
}
.scopes {
margin-left: 1rem;
}
.scope {
box-sizing: border-box;
padding: 1rem;
color: var(--shadow);
font-size: .9rem;
font-weight: bold;
&.active {
color: var(--primary);
font-weight: bold;
}
}
</style>

188
components/scenes/tile.vue Normal file
View File

@@ -0,0 +1,188 @@
<template>
<div class="tile">
<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"
>
</Link>
<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 { format } from 'date-fns';
defineProps({
scene: {
type: Object,
default: null,
},
});
</script>
<style scoped>
.tile {
width: 100%;
overflow: hidden;
background: var(--background-base);
border-radius: .25rem;
box-shadow: 0 0 3px var(--shadow-weak-30);
}
.poster {
display: block;
height: 14rem;
border-radius: .25rem .25rem 0 0;
overflow: hidden;
}
.thumbnail {
height: 100%;
width: 100%;
object-fit: cover;
background-size: cover;
background-position: center;
}
.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;
}
.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>