mirror of
https://github.com/mmahdium/TBW.git
synced 2026-08-14 16:22:48 +03:30
Added support for filters
This commit is contained in:
Executable
+86
@@ -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>
|
</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"
|
||||||
>
|
>
|
||||||
|
|||||||
+80
-65
@@ -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> => {
|
||||||
|
|||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
export type SearchFilters = {
|
||||||
|
includeAdult: boolean
|
||||||
|
onlyMovies: boolean
|
||||||
|
onlySeries: boolean
|
||||||
|
}
|
||||||
+18
-3
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user