<template> <div class="movies-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> <div class="movies-container"> <div class="movies-header"> <div class="meta">{{ total }} results</div> <select v-model="scope" class="input" @change="search({ autoScope: false })" > <!-- not selected in SSR without prop --> <option v-if="pageStash" :selected="scope === 'stashed'" value="stashed" >Added</option> <option v-if="filters.search" :selected="scope === 'results'" value="results" >Relevance</option> <option value="likes">Popular</option> <option value="latest">Latest</option> <option value="upcoming">Upcoming</option> <option value="new">New</option> </select> </div> <ul class="movies nolist"> <li v-for="movie in movies" :key="`movie-${movie.id}`" > <MovieTile :movie="movie" /> </li> </ul> <Pagination :total="total" :page="currentPage" /> </div> </div> </template> <script setup> import { ref, inject } from 'vue'; 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 MovieTile from '#/components/movies/tile.vue'; 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'; import Pagination from '#/components/pagination/pagination.vue'; const pageContext = inject('pageContext'); const { pageProps, routeParams, urlParsed } = pageContext; const { actor: pageActor, tag: pageTag, entity: pageEntity, stash: pageStash, } = 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 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, stashId: pageStash?.id, 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> .movies-page { display: flex; background: var(--background-base-10); } .movies-container { display: flex; flex-direction: column; flex-grow: 1; } .movies-header { display: flex; align-items: center; padding: .5rem 1rem .25rem 3rem; } .meta { display: flex; flex-grow: 1; justify-content: space-between; align-items: center; } .movies { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 1rem; padding: .5rem 1rem 1rem 1rem; } @media(--compact) { .movies { grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); } } @media(--small-20) { .movies { padding: .5rem .5rem 1rem .5rem; gap: .5rem .25rem; } } @media(--small-50) { .movies { grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); } } </style>