mirror of
				https://github.com/mmahdium/TBW.git
				synced 2025-11-03 20:20:49 +01:00 
			
		
		
		
	Added support for filters
This commit is contained in:
		
							
								
								
									
										86
									
								
								src/components/MediaFilters.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										86
									
								
								src/components/MediaFilters.vue
									
									
									
									
									
										Executable 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>
 | 
			
		||||
@@ -13,7 +13,7 @@ function onSubmit() {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <form @submit.prevent="onSubmit" class="flex justify-center mb-8">
 | 
			
		||||
  <form @submit.prevent="onSubmit" class="flex justify-center mb-3">
 | 
			
		||||
    <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"
 | 
			
		||||
    >
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										145
									
								
								src/lib/api.ts
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								src/lib/api.ts
									
									
									
									
									
								
							@@ -1,7 +1,9 @@
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
import type { MediaResponseType } from '@/types/Media'
 | 
			
		||||
import { mapMedia } from '@/types/MediaMap'
 | 
			
		||||
import type { MovieDetailsType } from '@/types/Movie'
 | 
			
		||||
import { mapMovieDetails } from '@/types/MovieMap'
 | 
			
		||||
import type { SearchFilters } from '@/types/SearchFilters'
 | 
			
		||||
import type { TvSeriesDetailsType } from '@/types/TvSeries'
 | 
			
		||||
import { mapTvSeriesDetails } from '@/types/TvSeriesMap'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
@@ -18,86 +20,99 @@ const instance = axios.create({
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const searchMovies = async (query: string): Promise<MediaResponseType> => {
 | 
			
		||||
  const [movieSearch, tvSearch] = await Promise.all([
 | 
			
		||||
    instance.get(`/search/movie`, {
 | 
			
		||||
      params: {
 | 
			
		||||
        query: query,
 | 
			
		||||
        include_adult: true,
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    instance.get(`/search/tv`, {
 | 
			
		||||
      params: {
 | 
			
		||||
        query: query,
 | 
			
		||||
        include_adult: true,
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
  ])
 | 
			
		||||
export const loadMedia = async (
 | 
			
		||||
  query: string,
 | 
			
		||||
  page = 1,
 | 
			
		||||
  filters: SearchFilters,
 | 
			
		||||
): Promise<MediaResponseType> => {
 | 
			
		||||
  const params = {
 | 
			
		||||
    query,
 | 
			
		||||
    include_adult: filters.includeAdult || false,
 | 
			
		||||
    page,
 | 
			
		||||
  }
 | 
			
		||||
  const requests: Promise<any>[] = []
 | 
			
		||||
  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 tvData = tvSearch.data
 | 
			
		||||
  const responses = await Promise.all(requests)
 | 
			
		||||
 | 
			
		||||
  // 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) {
 | 
			
		||||
    return {
 | 
			
		||||
      Results: [],
 | 
			
		||||
      Page: 0,
 | 
			
		||||
      totalResults: 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 }) => {
 | 
			
		||||
    return { ...result, media_type: 'movie' }
 | 
			
		||||
  })
 | 
			
		||||
  const filteredMovies = movieSearch.data.results.map((r: any) =>
 | 
			
		||||
    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 }) => {
 | 
			
		||||
    return { ...result, media_type: 'tv' }
 | 
			
		||||
  })
 | 
			
		||||
  const res: any[] = []
 | 
			
		||||
  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 = {
 | 
			
		||||
    Results: [...filteredMovies.map(mapMedia), ...filteredTV.map(mapMedia)],
 | 
			
		||||
    Page: Math.max(movieData.page, tvData.page),
 | 
			
		||||
    totalResults: movieData.total_results + tvData.total_results,
 | 
			
		||||
    totalPages: Math.max(movieData.total_pages, tvData.total_pages),
 | 
			
		||||
  return {
 | 
			
		||||
    Results: res,
 | 
			
		||||
    Page: Math.max(movieSearch.data.page, tvSearch.data.page),
 | 
			
		||||
    totalResults: movieSearch.data.total_results + tvSearch.data.total_results,
 | 
			
		||||
    totalPages: Math.max(movieSearch.data.total_pages, tvSearch.data.total_pages),
 | 
			
		||||
    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> => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								src/types/SearchFilters.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								src/types/SearchFilters.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
export type SearchFilters = {
 | 
			
		||||
  includeAdult: boolean
 | 
			
		||||
  onlyMovies: boolean
 | 
			
		||||
  onlySeries: boolean
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { loadMoreMovies, searchMovies } from '@/lib/api'
 | 
			
		||||
import { loadMedia } from '@/lib/api'
 | 
			
		||||
import { onMounted, ref, watch } from 'vue'
 | 
			
		||||
import SearchBar from '@/components/SearchBar.vue'
 | 
			
		||||
import MediaList from '@/components/MediaList.vue'
 | 
			
		||||
import type { MediaType } from '@/types/Media'
 | 
			
		||||
import ErrorAlert from '@/components/alerts/ErrorAlert.vue'
 | 
			
		||||
import { useSearchPageStore, useMediaStore } from '@/stores/media'
 | 
			
		||||
import MediaFilters from '@/components/MediaFilters.vue'
 | 
			
		||||
import type { SearchFilters } from '@/types/SearchFilters'
 | 
			
		||||
 | 
			
		||||
const medias = ref<MediaType[]>()
 | 
			
		||||
const seachError = ref<string>('')
 | 
			
		||||
@@ -16,6 +18,11 @@ const isLoadingMore = ref(false)
 | 
			
		||||
 | 
			
		||||
const state = useSearchPageStore()
 | 
			
		||||
const store = useMediaStore()
 | 
			
		||||
const filters = ref<SearchFilters>({
 | 
			
		||||
  includeAdult: false,
 | 
			
		||||
  onlyMovies: true,
 | 
			
		||||
  onlySeries: false,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const handleAddMedia = (media: MediaType) => {
 | 
			
		||||
  store.addMedia(media)
 | 
			
		||||
@@ -29,7 +36,7 @@ async function searchMovie() {
 | 
			
		||||
  try {
 | 
			
		||||
    isSearching.value = true
 | 
			
		||||
    searchPage.value = 1
 | 
			
		||||
    const result = await searchMovies(searchQuery.value)
 | 
			
		||||
    const result = await loadMedia(searchQuery.value, searchPage.value, filters.value)
 | 
			
		||||
    console.log(result)
 | 
			
		||||
    if (result.totalResults === 0) {
 | 
			
		||||
      medias.value = []
 | 
			
		||||
@@ -53,7 +60,7 @@ async function loadMore() {
 | 
			
		||||
  try {
 | 
			
		||||
    isLoadingMore.value = true
 | 
			
		||||
    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)
 | 
			
		||||
    state.setState(searchPage.value, searchQuery.value, medias.value ? medias.value : [])
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
@@ -91,6 +98,12 @@ watch(searchQuery, () => {
 | 
			
		||||
    timeoutId = null
 | 
			
		||||
  }, 500)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
watch(filters, () => {
 | 
			
		||||
  if (searchQuery.value.length > 2) {
 | 
			
		||||
    searchMovie()
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -105,6 +118,8 @@ watch(searchQuery, () => {
 | 
			
		||||
    <!-- Search bar -->
 | 
			
		||||
    <SearchBar v-model="searchQuery" @submit="searchMovie" />
 | 
			
		||||
 | 
			
		||||
    <MediaFilters @update:filters="(f) => (filters = f)" />
 | 
			
		||||
 | 
			
		||||
    <!-- Loading spinner -->
 | 
			
		||||
    <div v-if="isSearching" class="flex justify-center my-16">
 | 
			
		||||
      <span class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user