2024-02-27 00:20:15 +00:00
< template >
< div class = "page" >
< Filters
v - if = "showFilters"
: class = "{ loading }"
>
< div class = "filter" >
< input
v - model = "filters.search"
type = "search"
placeholder = "Search movies"
class = "search input"
@ search = "search"
>
< / div >
< TagsFilter
: filters = "filters"
: tags = "aggTags"
@ update = "updateFilter"
/ >
< ChannelsFilter
: filters = "filters"
: channels = "aggChannels"
@ update = "updateFilter"
/ >
< ActorsFilter
: filters = "filters"
: actors = "aggActors"
@ update = "updateFilter"
/ >
< / Filters >
2024-02-27 00:51:14 +00:00
< div class = "movies-container" >
< div class = "movies-header" >
< div class = "meta" > { { total } } results < / div >
< select
v - model = "scope"
class = "input"
@ change = "search({ autoScope: false })"
2024-02-27 00:20:15 +00:00
>
2024-02-27 00:51:14 +00:00
< option value = "likes" > Likes < / option >
< option value = "latest" > Latest < / option >
< option value = "upcoming" > Upcoming < / option >
< option value = "new" > New < / option >
< option
value = "results"
: disabled = "!filters.search"
> Relevance < / option >
< / select >
< / div >
2024-02-27 00:20:15 +00:00
2024-02-27 00:51:14 +00:00
< ul class = "movies nolist" >
< li
v - for = "movie in movies"
: key = "`movie-${movie.id}`"
class = "movie"
>
< a
: href = "`/movie/${movie.id}/${movie.slug}`"
: title = "movie.title"
class = "cover-container"
2024-02-27 00:20:15 +00:00
>
2024-02-27 00:51:14 +00:00
< img
v - if = "movie.covers[0]"
: src = "movie.covers[0].isS3 ? `https://cdndev.traxxx.me/${movie.covers[0].thumbnail}` : `/media/${movie.covers[0].thumbnail}`"
: style = "{ 'background-image': movie.covers[0].isS3 ? `url(https://cdndev.traxxx.me/${movie.covers[0].lazy})` : `url(/media/${movie.covers[0].lazy})` }"
class = "cover"
loading = "lazy"
>
2024-02-27 00:20:15 +00:00
2024-02-27 00:51:14 +00:00
< img
v - else
src = "/public/img/icons/movie.svg"
class = "nocover"
2024-02-27 00:20:15 +00:00
>
2024-02-27 00:51:14 +00:00
< / a >
2024-02-27 00:20:15 +00:00
2024-02-27 00:51:14 +00:00
< div class = "tile-meta" >
< div class = "channel" >
< Link
: href = "movie.channel.isIndependent || !movie.network ? `/${movie.channel.type}/${movie.channel.slug}` : `/${movie.network.type}/${movie.network.slug}`"
class = "favicon-link"
>
< img
: src = "movie.channel.isIndependent || !movie.network ? `/logos/${movie.channel.slug}/favicon.png` : `/logos/${movie.network.slug}/favicon.png`"
class = "favicon"
>
< / Link >
< Link
: href = "`/${movie.channel.type}/${movie.channel.slug}`"
class = "nolink channel-link"
> { { movie . channel . name } } < / Link >
< / div >
< time
: datetime = "movie.effectiveDate.toISOString()"
class = "date"
: class = "{ nodate: !movie.date }"
> { { format ( movie . effectiveDate , movie . effectiveDate . getFullYear ( ) === currentYear ? 'MMM d' : 'MMM d, y' ) } } < / time >
2024-02-27 00:20:15 +00:00
< / div >
2024-02-27 00:51:14 +00:00
< a
: href = "`/movie/${movie.id}/${movie.slug}`"
: title = "movie.title"
class = "title nolink"
> { { movie . title } } < / a >
< ul
: title = "movie.actors.map((actor) => actor.name).join(', ')"
class = "actors nolist"
2024-02-27 00:20:15 +00:00
>
2024-02-27 00:51:14 +00:00
< li
v - for = "actor in movie.actors"
: key = "`actor-${movie.id}-${actor.slug}`"
class = "actor-item"
>
< a
: href = "`/actor/${actor.id}/${actor.slug}`"
class = "actor nolink"
> { { actor . name } } < / a >
< / li >
< / ul >
< ul
: title = "movie.tags.map((tag) => tag.name).join(', ')"
class = "tags nolist"
2024-02-27 00:20:15 +00:00
>
2024-02-27 00:51:14 +00:00
< li
v - for = "tag in movie.tags"
: key = "`tag-${movie.id}-${tag.slug}`"
>
< a
: href = "`/tag/${tag.slug}`"
class = "tag nolink"
> { { tag . name } } < / a >
< / li >
< / ul >
< / li >
< / ul >
< / div >
2024-02-27 00:20:15 +00:00
< / div >
< / template >
< script setup >
import { ref , inject } from 'vue' ;
import { format } from 'date-fns' ;
import { parse } from 'path-to-regexp' ;
import navigate from '#/src/navigate.js' ;
import { get } from '#/src/api.js' ;
import { getActorIdentifier , parseActorIdentifier } from '#/src/query.js' ;
import events from '#/src/events.js' ;
import Filters from '#/components/filters/filters.vue' ;
import ActorsFilter from '#/components/filters/actors.vue' ;
import TagsFilter from '#/components/filters/tags.vue' ;
import ChannelsFilter from '#/components/filters/channels.vue' ;
const pageContext = inject ( 'pageContext' ) ;
const { pageProps , routeParams , urlParsed } = pageContext ;
const {
actor : pageActor ,
tag : pageTag ,
entity : pageEntity ,
} = pageProps ;
const movies = ref ( pageProps . movies ) ;
const aggActors = ref ( pageProps . aggActors || [ ] ) ;
const aggTags = ref ( pageProps . aggTags || [ ] ) ;
const aggChannels = ref ( pageProps . aggChannels || [ ] ) ;
const currentPage = ref ( Number ( routeParams . page ) ) ;
const scope = ref ( routeParams . scope ) ;
const total = ref ( Number ( pageProps . total ) ) ;
const loading = ref ( false ) ;
const showFilters = ref ( true ) ;
const currentYear = new Date ( ) . getFullYear ( ) ;
const actorIds = urlParsed . search . actors ? . split ( ',' ) . map ( ( identifier ) => parseActorIdentifier ( identifier ) ? . id ) . filter ( Boolean ) || [ ] ;
const queryActors = actorIds . map ( ( urlActorId ) => aggActors . value . find ( ( aggActor ) => aggActor . id === urlActorId ) ) . filter ( Boolean ) ;
const networks = Object . fromEntries ( aggChannels . value . map ( ( channel ) => ( channel . type === 'network' ? channel : channel . parent ) ) . filter ( Boolean ) . map ( ( parent ) => [ ` _ ${ parent . slug } ` , parent ] ) ) ;
const channels = Object . fromEntries ( aggChannels . value . filter ( ( channel ) => channel . type === 'channel' ) . map ( ( channel ) => [ channel . slug , channel ] ) ) ;
const queryEntity = networks [ urlParsed . search . e ] || channels [ urlParsed . search . e ] ;
const filters = ref ( {
search : urlParsed . search . q ,
tags : urlParsed . search . tags ? . split ( ',' ) . filter ( Boolean ) || [ ] ,
entity : queryEntity ,
actors : queryActors ,
} ) ;
function getPath ( targetScope , preserveQuery ) {
const path = parse ( routeParams . path ) . map ( ( segment ) => {
if ( segment . name === 'scope' ) {
return ` ${ segment . prefix } ${ targetScope } ` ;
}
if ( segment . name === 'page' ) {
return ` ${ segment . prefix } ${ 1 } ` ;
}
return ` ${ segment . prefix || '' } ${ routeParams [ segment . name ] || segment } ` ;
} ) . join ( '' ) ;
if ( preserveQuery && urlParsed . searchOriginal ) {
return ` ${ path } ${ urlParsed . searchOriginal } ` ;
}
return path ;
}
async function search ( options = { } ) {
if ( options . resetPage !== false ) {
currentPage . value = 1 ;
}
if ( options . autoScope !== false ) {
if ( filters . value . search ) {
scope . value = 'results' ;
}
if ( ! filters . value . search && scope . value === 'results' ) {
scope . value = 'latest' ;
}
}
const query = {
q : filters . value . search || undefined ,
} ;
const entity = filters . value . entity || pageEntity ;
const entitySlug = entity ? . type === 'network' ? ` _ ${ entity . slug } ` : entity ? . slug ;
loading . value = true ;
navigate ( getPath ( scope . value , false ) , {
... query ,
actors : filters . value . actors . map ( ( filterActor ) => getActorIdentifier ( filterActor ) ) . join ( ',' ) || undefined , // don't include page actor ID in query, already a parameter
tags : filters . value . tags . join ( ',' ) || undefined ,
e : filters . value . entity ? . type === 'network' ? ` _ ${ filters . value . entity . slug } ` : ( filters . value . entity ? . slug || undefined ) ,
} , { redirect : false } ) ;
const res = await get ( '/movies' , {
... query ,
actors : [ pageActor , ... filters . value . actors ] . filter ( Boolean ) . map ( ( filterActor ) => getActorIdentifier ( filterActor ) ) . join ( ',' ) , // if we're on an actor page, that actor ID needs to be included
tags : [ pageTag ? . slug , ... filters . value . tags ] . filter ( Boolean ) . join ( ',' ) ,
e : entitySlug ,
scope : scope . value ,
page : currentPage . value , // client uses param rather than query pagination
} ) ;
movies . value = res . movies ;
aggActors . value = res . aggActors ;
aggTags . value = res . aggTags ;
aggChannels . value = res . aggChannels ;
total . value = res . total ;
loading . value = false ;
events . emit ( 'scrollUp' ) ;
}
function updateFilter ( prop , value , reload = true ) {
filters . value [ prop ] = value ;
if ( reload ) {
search ( ) ;
}
}
< / script >
< style scoped >
. page {
display : flex ;
background : var ( -- background - base - 10 ) ;
}
2024-02-27 00:51:14 +00:00
. movies - container {
display : flex ;
flex - direction : column ;
flex - grow : 1 ;
}
. movies - header {
display : flex ;
align - items : center ;
padding : .5 rem 1 rem .25 rem 3 rem ;
}
. meta {
display : flex ;
flex - grow : 1 ;
justify - content : space - between ;
align - items : center ;
}
2024-02-27 00:20:15 +00:00
. movies {
display : grid ;
grid - template - columns : repeat ( auto - fill , minmax ( 13 rem , 1 fr ) ) ;
gap : 1 rem ;
2024-02-27 00:51:14 +00:00
padding : .5 rem 1 rem 1 rem 1 rem ;
2024-02-27 00:20:15 +00:00
}
. movie {
max - height : 30 rem ;
display : flex ;
flex - direction : column ;
box - shadow : 0 0 3 px var ( -- shadow - weak - 30 ) ;
border - radius : .25 rem ;
overflow : hidden ;
background : var ( -- background - base ) ;
& : hover {
box - shadow : 0 0 3 px var ( -- shadow - weak - 20 ) ;
}
}
. cover - container {
display : flex ;
align - items : center ;
justify - content : center ;
flex - grow : 1 ;
background : var ( -- shadow - weak - 30 ) ;
aspect - ratio : 5 / 7 ;
}
. cover {
width : 100 % ;
height : 100 % ;
object - fit : cover ;
background - size : cover ;
background - position : center ;
}
. nocover {
width : 25 % ;
opacity : .1 ;
}
2024-02-27 00:51:14 +00:00
. tile - meta {
2024-02-27 00:20:15 +00:00
display : flex ;
justify - content : space - between ;
align - items : center ;
padding : .4 rem .5 rem ;
border - radius : 0 0 .25 rem .25 rem ;
margin - bottom : .5 rem ;
background : var ( -- shadow - strong - 30 ) ;
color : var ( -- text - light ) ;
font - size : .8 rem ;
}
. channel {
display : inline - flex ;
align - items : center ;
margin - right : .5 rem ;
white - space : nowrap ;
overflow : hidden ;
}
. channel - link {
overflow : hidden ;
text - overflow : ellipsis ;
font - weight : bold ;
}
. favicon - link {
display : inline - flex ;
}
. favicon {
width : 1 rem ;
height : 1 rem ;
margin - right : .5 rem ;
object - fit : contain ;
}
. date {
flex - shrink : 0 ;
}
. nodate {
color : var ( -- highlight - strong - 10 ) ;
}
. title {
white - space : nowrap ;
overflow : hidden ;
text - overflow : ellipsis ;
font - weight : bold ;
padding : 0 .5 rem ;
margin - bottom : .25 rem ;
}
. actors {
height : 2.4 rem ;
display : flex ;
flex - wrap : wrap ;
overflow : hidden ;
font - size : .9 rem ;
padding : 0 .5 rem ;
margin - bottom : .35 rem ;
line - height : 1.35 ;
}
. actor - item : not ( : last - child ) : after {
content : ',\00a0' ;
}
. actor {
& : hover {
color : var ( -- primary ) ;
}
}
. tags {
height : 1 rem ;
display : flex ;
flex - wrap : wrap ;
gap : .5 rem ;
overflow : hidden ;
padding : 0 .5 rem ;
margin - bottom : .25 rem ;
color : var ( -- shadow - strong - 10 ) ;
font - size : .75 rem ;
}
. tag {
flex - shrink : 0 ;
& : hover {
color : var ( -- primary ) ;
}
}
< / style >