Added support for filters

This commit is contained in:
2025-10-31 18:15:36 +03:30
parent 4b1cd06ac7
commit c5e32fc26f
5 changed files with 190 additions and 69 deletions

86
src/components/MediaFilters.vue Executable file
View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import type { SearchFilters } from '@/types/SearchFilters'
import { reactive, watch } from 'vue'
const props = defineProps<{
modelValue?: SearchFilters // optional initial value
}>()
const emit = defineEmits<{
(e: 'update:filters', value: SearchFilters): void
}>()
const local = reactive<SearchFilters>({
includeAdult: props.modelValue?.includeAdult ?? false,
onlyMovies: props.modelValue?.onlyMovies ?? true,
onlySeries: props.modelValue?.onlySeries ?? false,
})
function toggleIncludeAdult(e: Event) {
local.includeAdult = (e.target as HTMLInputElement).checked
}
function toggleOnlyMovies(e: Event) {
const checked = (e.target as HTMLInputElement).checked
if (!checked && !local.onlySeries) {
local.onlySeries = true
}
local.onlyMovies = checked
}
function toggleOnlySeries(e: Event) {
const checked = (e.target as HTMLInputElement).checked
if (!checked && !local.onlyMovies) {
local.onlyMovies = true
}
local.onlySeries = checked
}
watch(
() => ({ ...local }),
(next) => {
// ensureAtLeastOneGenre()
const out: SearchFilters = {
includeAdult: next.includeAdult,
onlyMovies: next.onlyMovies,
onlySeries: next.onlySeries,
}
emit('update:filters', out)
},
{ deep: true, immediate: true },
)
</script>
<template>
<form class="flex gap-2 justify-center mb-8">
<label>
<input
class="btn"
:class="local.includeAdult ? 'btn-error' : ''"
type="checkbox"
:checked="local.includeAdult"
@change="toggleIncludeAdult"
aria-label="Adult"
/>
</label>
<label>
<input
class="btn"
type="checkbox"
:checked="local.onlyMovies"
@change="toggleOnlyMovies"
aria-label="Movies"
/>
</label>
<label>
<input
class="btn"
type="checkbox"
:checked="local.onlySeries"
@change="toggleOnlySeries"
aria-label="TV Series"
/>
</label>
</form>
</template>

View File

@@ -13,7 +13,7 @@ function onSubmit() {
</script> </script>
<template> <template>
<form @submit.prevent="onSubmit" class="flex justify-center mb-8"> <form @submit.prevent="onSubmit" class="flex justify-center mb-3">
<div <div
class="flex items-center w-full max-w-md bg-white/70 backdrop-blur-md border border-gray-200/60 shadow-sm hover:shadow-md transition rounded-lg overflow-hidden" class="flex items-center w-full max-w-md bg-white/70 backdrop-blur-md border border-gray-200/60 shadow-sm hover:shadow-md transition rounded-lg overflow-hidden"
> >

View File

@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MediaResponseType } from '@/types/Media' import type { MediaResponseType } from '@/types/Media'
import { mapMedia } from '@/types/MediaMap' import { mapMedia } from '@/types/MediaMap'
import type { MovieDetailsType } from '@/types/Movie' import type { MovieDetailsType } from '@/types/Movie'
import { mapMovieDetails } from '@/types/MovieMap' import { mapMovieDetails } from '@/types/MovieMap'
import type { SearchFilters } from '@/types/SearchFilters'
import type { TvSeriesDetailsType } from '@/types/TvSeries' import type { TvSeriesDetailsType } from '@/types/TvSeries'
import { mapTvSeriesDetails } from '@/types/TvSeriesMap' import { mapTvSeriesDetails } from '@/types/TvSeriesMap'
import axios from 'axios' import axios from 'axios'
@@ -18,86 +20,99 @@ const instance = axios.create({
}, },
}) })
export const searchMovies = async (query: string): Promise<MediaResponseType> => { export const loadMedia = async (
const [movieSearch, tvSearch] = await Promise.all([ query: string,
instance.get(`/search/movie`, { page = 1,
params: { filters: SearchFilters,
query: query, ): Promise<MediaResponseType> => {
include_adult: true, const params = {
}, query,
}), include_adult: filters.includeAdult || false,
instance.get(`/search/tv`, { page,
params: { }
query: query, const requests: Promise<any>[] = []
include_adult: true, if (filters.onlyMovies) {
}, requests.push(instance.get(`/search/movie`, { params }))
}), }
]) if (filters.onlySeries) {
requests.push(instance.get(`/search/tv`, { params }))
}
const movieData = movieSearch.data const responses = await Promise.all(requests)
const tvData = tvSearch.data
// If only one type was requested, handle that directly
if (filters.onlyMovies) {
const movieSearch = responses[0]
if (movieSearch.status !== 200) {
return {
Results: [],
Page: 0,
totalResults: 0,
totalPages: 0,
ErrorMessage: movieSearch.data.Error,
}
}
const movies = movieSearch.data.results.map((r: any) => mapMedia({ ...r, media_type: 'movie' }))
return {
Results: movies,
Page: movieSearch.data.page,
totalResults: movieSearch.data.total_results,
totalPages: movieSearch.data.total_pages,
ErrorMessage: '',
}
}
if (filters.onlySeries) {
const tvSearch = responses[0]
if (tvSearch.status !== 200) {
return {
Results: [],
Page: 0,
totalResults: 0,
totalPages: 0,
ErrorMessage: tvSearch.data.Error,
}
}
const tv = tvSearch.data.results.map((r: any) => mapMedia({ ...r, media_type: 'tv' }))
return {
Results: tv,
Page: tvSearch.data.page,
totalResults: tvSearch.data.total_results,
totalPages: tvSearch.data.total_pages,
ErrorMessage: '',
}
}
// Otherwise, both
const [movieSearch, tvSearch] = responses
if (movieSearch.status !== 200 || tvSearch.status !== 200) { if (movieSearch.status !== 200 || tvSearch.status !== 200) {
return { return {
Results: [], Results: [],
Page: 0, Page: 0,
totalResults: 0, totalResults: 0,
totalPages: 0, totalPages: 0,
ErrorMessage: movieSearch.data.Error || tvSearch.data.Error, ErrorMessage: movieSearch?.data?.Error || tvSearch?.data?.Error,
} }
} }
const filteredMovies = movieData.results.map((result: { media_type: string }) => { const filteredMovies = movieSearch.data.results.map((r: any) =>
return { ...result, media_type: 'movie' } mapMedia({ ...r, media_type: 'movie' }),
}) )
const filteredTV = tvSearch.data.results.map((r: any) => mapMedia({ ...r, media_type: 'tv' }))
const filteredTV = tvData.results.map((result: { media_type: string }) => { const res: any[] = []
return { ...result, media_type: 'tv' } for (let i = 0; i < Math.max(filteredMovies.length, filteredTV.length); i++) {
}) if (i < filteredMovies.length) res.push(filteredMovies[i])
if (i < filteredTV.length) res.push(filteredTV[i])
}
const data: MediaResponseType = { return {
Results: [...filteredMovies.map(mapMedia), ...filteredTV.map(mapMedia)], Results: res,
Page: Math.max(movieData.page, tvData.page), Page: Math.max(movieSearch.data.page, tvSearch.data.page),
totalResults: movieData.total_results + tvData.total_results, totalResults: movieSearch.data.total_results + tvSearch.data.total_results,
totalPages: Math.max(movieData.total_pages, tvData.total_pages), totalPages: Math.max(movieSearch.data.total_pages, tvSearch.data.total_pages),
ErrorMessage: '', ErrorMessage: '',
} }
return data
}
export const loadMoreMovies = async (query: string, page: number): Promise<MediaResponseType> => {
const response = await instance.get(`/search/multi`, {
params: {
query: query,
include_adult: true,
page: page,
},
})
if (response.status !== 200) {
return {
Results: [],
Page: 0,
totalResults: 0,
totalPages: 0,
ErrorMessage: response.data.Error,
}
}
const filtered = response.data.results.filter((result: { media_type: string }) => {
return result.media_type === 'movie' || result.media_type === 'tv'
})
const data: MediaResponseType = {
Results: filtered.map(mapMedia),
Page: response.data.page,
totalResults: response.data.total_results,
totalPages: response.data.total_pages,
ErrorMessage: '',
}
return data
} }
export const getMovieDetails = async (id: string): Promise<MovieDetailsType | null> => { export const getMovieDetails = async (id: string): Promise<MovieDetailsType | null> => {

5
src/types/SearchFilters.ts Executable file
View File

@@ -0,0 +1,5 @@
export type SearchFilters = {
includeAdult: boolean
onlyMovies: boolean
onlySeries: boolean
}

View File

@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { loadMoreMovies, searchMovies } from '@/lib/api' import { loadMedia } from '@/lib/api'
import { onMounted, ref, watch } from 'vue' import { onMounted, ref, watch } from 'vue'
import SearchBar from '@/components/SearchBar.vue' import SearchBar from '@/components/SearchBar.vue'
import MediaList from '@/components/MediaList.vue' import MediaList from '@/components/MediaList.vue'
import type { MediaType } from '@/types/Media' import type { MediaType } from '@/types/Media'
import ErrorAlert from '@/components/alerts/ErrorAlert.vue' import ErrorAlert from '@/components/alerts/ErrorAlert.vue'
import { useSearchPageStore, useMediaStore } from '@/stores/media' import { useSearchPageStore, useMediaStore } from '@/stores/media'
import MediaFilters from '@/components/MediaFilters.vue'
import type { SearchFilters } from '@/types/SearchFilters'
const medias = ref<MediaType[]>() const medias = ref<MediaType[]>()
const seachError = ref<string>('') const seachError = ref<string>('')
@@ -16,6 +18,11 @@ const isLoadingMore = ref(false)
const state = useSearchPageStore() const state = useSearchPageStore()
const store = useMediaStore() const store = useMediaStore()
const filters = ref<SearchFilters>({
includeAdult: false,
onlyMovies: true,
onlySeries: false,
})
const handleAddMedia = (media: MediaType) => { const handleAddMedia = (media: MediaType) => {
store.addMedia(media) store.addMedia(media)
@@ -29,7 +36,7 @@ async function searchMovie() {
try { try {
isSearching.value = true isSearching.value = true
searchPage.value = 1 searchPage.value = 1
const result = await searchMovies(searchQuery.value) const result = await loadMedia(searchQuery.value, searchPage.value, filters.value)
console.log(result) console.log(result)
if (result.totalResults === 0) { if (result.totalResults === 0) {
medias.value = [] medias.value = []
@@ -53,7 +60,7 @@ async function loadMore() {
try { try {
isLoadingMore.value = true isLoadingMore.value = true
searchPage.value++ searchPage.value++
const result = await loadMoreMovies(searchQuery.value, searchPage.value) const result = await loadMedia(searchQuery.value, searchPage.value, filters.value)
medias.value?.push(...result.Results) medias.value?.push(...result.Results)
state.setState(searchPage.value, searchQuery.value, medias.value ? medias.value : []) state.setState(searchPage.value, searchQuery.value, medias.value ? medias.value : [])
} catch (error) { } catch (error) {
@@ -91,6 +98,12 @@ watch(searchQuery, () => {
timeoutId = null timeoutId = null
}, 500) }, 500)
}) })
watch(filters, () => {
if (searchQuery.value.length > 2) {
searchMovie()
}
})
</script> </script>
<template> <template>
@@ -105,6 +118,8 @@ watch(searchQuery, () => {
<!-- Search bar --> <!-- Search bar -->
<SearchBar v-model="searchQuery" @submit="searchMovie" /> <SearchBar v-model="searchQuery" @submit="searchMovie" />
<MediaFilters @update:filters="(f) => (filters = f)" />
<!-- Loading spinner --> <!-- Loading spinner -->
<div v-if="isSearching" class="flex justify-center my-16"> <div v-if="isSearching" class="flex justify-center my-16">
<span class="loading loading-ring loading-lg text-primary"></span> <span class="loading loading-ring loading-lg text-primary"></span>