Added improved types

This commit is contained in:
2025-10-22 21:12:20 +03:30
parent 138e7c3248
commit 72271d1de2
12 changed files with 509 additions and 71 deletions
+13 -8
View File
@@ -11,9 +11,14 @@ const store = useMoviesStore()
const imageLoadFailed = ref(false) const imageLoadFailed = ref(false)
const loaded = ref(false) const loaded = ref(false)
const alreadyAdded = computed(() => const alreadyAdded = computed(() => store.movieList.some((movie) => movie.Id === props.movie.Id))
store.movieList.some((movie) => movie.imdbID === props.movie.imdbID),
) const imageSource = computed(() => {
if (!props.movie.PosterPath) {
return ''
}
return `https://image.tmdb.org/t/p/w300${props.movie.PosterPath}`
})
</script> </script>
<template> <template>
@@ -22,7 +27,7 @@ const alreadyAdded = computed(() =>
v-motion-fade-visible-once v-motion-fade-visible-once
> >
<!-- Poster --> <!-- Poster -->
<router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }"> <router-link :to="{ name: 'details', params: { type: props.movie.MediaType, id: props.movie.Id } }">
<figure class="overflow-hidden flex items-center justify-center aspect-[2/3] bg-gray-50"> <figure class="overflow-hidden flex items-center justify-center aspect-[2/3] bg-gray-50">
<span v-if="!loaded" class="loading loading-ring loading-lg text-primary"></span> <span v-if="!loaded" class="loading loading-ring loading-lg text-primary"></span>
@@ -45,7 +50,7 @@ const alreadyAdded = computed(() =>
<img <img
v-show="loaded && !imageLoadFailed" v-show="loaded && !imageLoadFailed"
:src="props.movie.Poster" :src="imageSource"
:alt="props.movie.Title" :alt="props.movie.Title"
class="object-cover w-full h-full transform transition-transform duration-500 hover:scale-105" class="object-cover w-full h-full transform transition-transform duration-500 hover:scale-105"
@load="loaded = true" @load="loaded = true"
@@ -61,13 +66,13 @@ const alreadyAdded = computed(() =>
<!-- Body --> <!-- Body -->
<div class="card-body p-4 flex flex-col"> <div class="card-body p-4 flex flex-col">
<router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }"> <router-link :to="{ name: 'details', params: { type: props.movie.MediaType, id: props.movie.Id } }">
<h2 <h2
class="card-title text-base font-semibold bg-gradient-to-r from-gray-700 to-gray-500 bg-clip-text text-transparent" class="card-title text-base font-semibold bg-gradient-to-r from-gray-700 to-gray-500 bg-clip-text text-transparent"
> >
{{ props.movie.Title }} {{ props.movie.Title }}
</h2> </h2>
<p class="text-sm text-gray-400">{{ props.movie.Year }}</p> <p class="text-sm text-gray-400">{{ props.movie.ReleaseDate.slice(0, 4) }}</p>
</router-link> </router-link>
<div class="card-actions justify-end mt-auto"> <div class="card-actions justify-end mt-auto">
@@ -83,7 +88,7 @@ const alreadyAdded = computed(() =>
v-motion-fade-visible-once v-motion-fade-visible-once
v-else v-else
class="btn btn-sm px-4 bg-gradient-to-r from-red-50 to-red-100 border border-red-200 text-red-600 hover:from-red-100 hover:to-red-200 hover:text-red-700 transition" class="btn btn-sm px-4 bg-gradient-to-r from-red-50 to-red-100 border border-red-200 text-red-600 hover:from-red-100 hover:to-red-200 hover:text-red-700 transition"
@click="store.removeMovie(props.movie.imdbID)" @click="store.removeMovie(props.movie.Id)"
> >
Remove Remove
</button> </button>
+2 -2
View File
@@ -29,10 +29,10 @@ const emit = defineEmits<{ (e: 'loaded', id: string): void; (e: 'loadMore'): voi
v-else v-else
class="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5" class="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"
> >
<li v-for="movie in props.movies" :key="movie.imdbID" v-auto-animate> <li v-for="movie in props.movies" :key="movie.Id" v-auto-animate>
<MovieCard :movie="movie" /> <MovieCard :movie="movie" />
</li> </li>
<li> <li v-if="!props.isSearch">
<AddMoreCard /> <AddMoreCard />
</li> </li>
</ul> </ul>
+66 -27
View File
@@ -1,53 +1,96 @@
import type { MovieResponseType, MovieType } from '@/types/Movie' import type { MediaResponseType } from '@/types/Media'
import { mapMedia } from '@/types/MediaMap'
import axios from 'axios' import axios from 'axios'
const API_KEY = '595695c3' // I know this should not be here and I dont care const TMDB_READ_API_KEY =
'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIzZjY3MDNmM2UyYTBiMmI0MGZlNGZiYjNlMTU0NjI0NCIsIm5iZiI6MTc2MDU3NDcwNi45MjQsInN1YiI6IjY4ZjAzY2YyNGZmNGM0NjI1NmM3N2EyNyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.HHReb_7Oue_o1wkgH2mmbCgdMJ8buFfRdCtwQqj_1Us' // I know this should not be here and I dont care
const instance = axios.create({ const instance = axios.create({
baseURL: 'https://www.omdbapi.com/', baseURL: 'https://api.themoviedb.org/3',
timeout: 6969, timeout: 6969,
params: { headers: {
apikey: API_KEY, 'Content-Type': 'application/json',
Authorization: `Bearer ${TMDB_READ_API_KEY}`,
}, },
}) })
export const searchMovies = async (query: string): Promise<MovieResponseType> => { export const searchMovies = async (query: string): Promise<MediaResponseType> => {
const response = await instance.get(``, { const response = await instance.get(`/search/multi`, {
params: { params: {
s: query, query: query,
include_adult: true,
}, },
}) })
// if (response.data.Response === 'False') { if (response.status !== 200) {
// throw new Error(response.data.Error) return {
// } Results: [],
Page: 0,
totalResults: 0,
totalPages: 0,
ErrorMessage: response.data.Error,
}
}
const data: MovieResponseType = { const filtered = response.data.results.filter((result: { media_type: string }) => {
Search: response.data.Search as MovieType[], return result.media_type === 'movie' || result.media_type === 'tv'
totalResults: response.data.totalResults, })
Response: response.data.Response === 'True',
ErrorMessage: response.data.Error || '', const data: MediaResponseType = {
Results: filtered.map(mapMedia),
Page: response.data.page,
totalResults: response.data.total_results,
totalPages: response.data.total_pages,
ErrorMessage: '',
} }
return data return data
} }
export const loadMoreMovies = async (query: string, page: number) => { export const loadMoreMovies = async (query: string, page: number): Promise<MediaResponseType> => {
const response = await instance.get(``, { const response = await instance.get(`/search/multi`, {
params: { params: {
s: query, query: query,
page: page, include_adult: true,
page: page
}, },
}) })
if (response.data.Response === 'False') { if (response.status !== 200) {
throw new Error(response.data.Error) 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) => {
const response = await instance.get(`movie/${id}`, {
})
return response.data return response.data
} }
export const getMovie = async (id: string) => { export const getSeriesDetails = async (id: string) => {
const response = await instance.get(``, { const response = await instance.get(``, {
params: { params: {
i: id, i: id,
@@ -55,9 +98,5 @@ export const getMovie = async (id: string) => {
}, },
}) })
if (response.data.Response === 'False') {
throw new Error(response.data.Error)
}
return response.data return response.data
} }
+1 -1
View File
@@ -17,7 +17,7 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/ListView.vue'), component: () => import('@/views/ListView.vue'),
}, },
{ {
path: '/details/:id', path: '/details/:type/:id',
name: 'details', name: 'details',
component: () => import('@/views/DetailsView.vue'), component: () => import('@/views/DetailsView.vue'),
}, },
+3 -3
View File
@@ -15,14 +15,14 @@ export const useMoviesStore = defineStore('movies', () => {
const movieList = ref<MovieType[]>(loadMovies()) const movieList = ref<MovieType[]>(loadMovies())
function addMovie(movie: MovieType) { function addMovie(movie: MovieType) {
if (!movieList.value.find((m) => m.imdbID === movie.imdbID)) { if (!movieList.value.find((m) => m.Id === movie.Id)) {
movieList.value.push(movie) movieList.value.push(movie)
saveMovies(movieList.value) saveMovies(movieList.value)
} }
} }
function removeMovie(movieId: string) { function removeMovie(movieId: number) {
movieList.value = movieList.value.filter((m) => m.imdbID !== movieId) movieList.value = movieList.value.filter((m) => m.Id !== movieId)
saveMovies(movieList.value) saveMovies(movieList.value)
} }
+24
View File
@@ -0,0 +1,24 @@
export type MediaType = {
Adult: boolean
BackdropPath: string
Id: number
Title: string
OriginalTitle: string
OriginalLanguage: string
MediaType: 'movie' | 'tv' | 'person' | 'collection'
Overview: string
PosterPath: string
Popularity: number
ReleaseDate: string // For movies
FirstAirDate: string // For TV series
VoteAverage: number
VoteCount: number
}
export type MediaResponseType = {
Results: MediaType[]
Page: number
totalResults: number
totalPages: number
ErrorMessage: string
}
+37
View File
@@ -0,0 +1,37 @@
import type { MediaType } from "./Media"
type TMDBMedia = {
adult: boolean
backdrop_path: string | null
id: number
title?: string
name?: string
original_title?: string
original_name?: string
original_language: string
overview: string
poster_path: string | null
popularity: number
release_date?: string
first_air_date?: string
vote_average: number
vote_count: number
media_type: 'movie' | 'tv' | 'person' | 'collection'
}
export const mapMedia = (m: TMDBMedia): MediaType => ({
Adult: m.adult,
BackdropPath: m.backdrop_path ?? '',
Id: m.id,
Title: m.title ?? m.name ?? '',
OriginalTitle: m.original_title ?? m.original_name ?? '',
OriginalLanguage: m.original_language,
MediaType: m.media_type,
Overview: m.overview,
PosterPath: m.poster_path ?? '',
Popularity: m.popularity,
ReleaseDate: m.release_date ?? '',
FirstAirDate: m.first_air_date ?? '',
VoteAverage: m.vote_average,
VoteCount: m.vote_count,
})
+35 -25
View File
@@ -1,28 +1,38 @@
export type MovieType = {
Title: string
Year: string
imdbID: string
Type: string
Poster: string
}
export type MovieResponseType = {
Search: MovieType[]
totalResults: string
Response: boolean
ErrorMessage: string
}
export type MovieDetailsType = { export type MovieDetailsType = {
Adult: boolean
BackdropPath: string
Budget: number
Genres: Array<{ id: number; name: string }>
Homepage: string
Id: number
ImdbId: string
OriginalLanguage: string
OriginalTitle: string
Overview: string
Popularity: number
PosterPath: string
ProductionCompanies: Array<{
id: number
logoPath: string | null
name: string
originCountry: string
}>
ProductionCountries: Array<{
iso31661: string
name: string
}>
ReleaseDate: string
Revenue: number
Runtime: number
SpokenLanguages: Array<{
englishName: string
iso6391: string
name: string
}>
Status: string
Tagline: string
Title: string Title: string
Year: string Video: boolean
imdbID: string VoteAverage: number
Type: string VoteCount: number
Poster: string
Plot: string
Language: string
Country: string
imdbRating: string
Error: boolean
ErrorMessage: string
} }
+82
View File
@@ -0,0 +1,82 @@
import type { MovieDetailsType } from "./Movie"
type TMDBMovieDetails = {
adult: boolean
backdrop_path: string | null
budget: number
genres: Array<{ id: number; name: string }>
homepage: string
id: number
imdb_id: string
original_language: string
original_title: string
overview: string
popularity: number
poster_path: string | null
production_companies: Array<{
id: number
logo_path: string | null
name: string
origin_country: string
}>
production_countries: Array<{
iso_3166_1: string
name: string
}>
release_date: string
revenue: number
runtime: number
spoken_languages: Array<{
english_name: string
iso_639_1: string
name: string
}>
status: string
tagline: string
title: string
video: boolean
vote_average: number
vote_count: number
}
export const mapMovieDetails = (m: TMDBMovieDetails): MovieDetailsType => ({
Adult: m.adult,
BackdropPath: m.backdrop_path ?? '',
Budget: m.budget,
Genres: m.genres.map(genre => ({
id: genre.id,
name: genre.name
})),
Homepage: m.homepage,
Id: m.id,
ImdbId: m.imdb_id,
OriginalLanguage: m.original_language,
OriginalTitle: m.original_title,
Overview: m.overview,
Popularity: m.popularity,
PosterPath: m.poster_path ?? '',
ProductionCompanies: m.production_companies.map(company => ({
id: company.id,
logoPath: company.logo_path,
name: company.name,
originCountry: company.origin_country
})),
ProductionCountries: m.production_countries.map(country => ({
iso31661: country.iso_3166_1,
name: country.name
})),
ReleaseDate: m.release_date,
Revenue: m.revenue,
Runtime: m.runtime ?? 0,
SpokenLanguages: m.spoken_languages.map(lang => ({
englishName: lang.english_name,
iso6391: lang.iso_639_1,
name: lang.name
})),
Status: m.status,
Tagline: m.tagline ?? '',
Title: m.title,
Video: m.video,
VoteAverage: m.vote_average,
VoteCount: m.vote_count,
})
+78
View File
@@ -0,0 +1,78 @@
export type TvSeriesDetailsType = {
Adult: boolean
BackdropPath: string
CreatedBy: Array<{
id: number
creditId: string
name: string
gender: number
profilePath: string | null
}>
EpisodeRunTime: number[]
FirstAirDate: string
Genres: Array<{ id: number; name: string }>
Homepage: string
Id: number
InProduction: boolean
Languages: string[]
LastAirDate: string
LastEpisodeToAir: {
id: number
name: string
overview: string
voteAverage: number
voteCount: number
airDate: string
episodeNumber: number
productionCode: string
runtime: number
seasonNumber: number
showId: number
stillPath: string | null
} | null
Name: string
Networks: Array<{
id: number
logoPath: string | null
name: string
originCountry: string
}>
NumberOfEpisodes: number
NumberOfSeasons: number
OriginCountry: string[]
OriginalLanguage: string
OriginalName: string
Overview: string
Popularity: number
PosterPath: string
ProductionCompanies: Array<{
id: number
logoPath: string | null
name: string
originCountry: string
}>
ProductionCountries: Array<{
iso31661: string
name: string
}>
Seasons: Array<{
airDate: string | null
episodeCount: number
id: number
name: string
overview: string
posterPath: string | null
seasonNumber: number
voteAverage: number
}>
SpokenLanguages: Array<{
englishName: string
iso6391: string
name: string
}>
Status: string
Tagline: string
Type: string
VoteAverage: number
VoteCount: number
}
+162
View File
@@ -0,0 +1,162 @@
import type { TvSeriesDetailsType } from "./TvSeries"
type TMDBTvSeriesDetails = {
adult: boolean
backdrop_path: string | null
created_by: Array<{
id: number
credit_id: string
name: string
gender: number
profile_path: string | null
}>
episode_run_time: number[]
first_air_date: string
genres: Array<{ id: number; name: string }>
homepage: string
id: number
in_production: boolean
languages: string[]
last_air_date: string
last_episode_to_air: {
id: number
name: string
overview: string
vote_average: number
vote_count: number
air_date: string
episode_number: number
production_code: string
runtime: number
season_number: number
show_id: number
still_path: string | null
} | null
name: string
networks: Array<{
id: number
logo_path: string | null
name: string
origin_country: string
}>
number_of_episodes: number
number_of_seasons: number
origin_country: string[]
original_language: string
original_name: string
overview: string
popularity: number
poster_path: string | null
production_companies: Array<{
id: number
logo_path: string | null
name: string
origin_country: string
}>
production_countries: Array<{
iso_3166_1: string
name: string
}>
seasons: Array<{
air_date: string | null
episode_count: number
id: number
name: string
overview: string
poster_path: string | null
season_number: number
vote_average: number
}>
spoken_languages: Array<{
english_name: string
iso_639_1: string
name: string
}>
status: string
tagline: string
type: string
vote_average: number
vote_count: number
}
export const mapTvSeriesDetails = (tv: TMDBTvSeriesDetails): TvSeriesDetailsType => ({
Adult: tv.adult,
BackdropPath: tv.backdrop_path ?? '',
CreatedBy: tv.created_by.map(creator => ({
id: creator.id,
creditId: creator.credit_id,
name: creator.name,
gender: creator.gender,
profilePath: creator.profile_path
})),
EpisodeRunTime: tv.episode_run_time,
FirstAirDate: tv.first_air_date ?? '',
Genres: tv.genres.map(genre => ({
id: genre.id,
name: genre.name
})),
Homepage: tv.homepage,
Id: tv.id,
InProduction: tv.in_production,
Languages: tv.languages,
LastAirDate: tv.last_air_date ?? '',
LastEpisodeToAir: tv.last_episode_to_air ? {
id: tv.last_episode_to_air.id,
name: tv.last_episode_to_air.name,
overview: tv.last_episode_to_air.overview,
voteAverage: tv.last_episode_to_air.vote_average,
voteCount: tv.last_episode_to_air.vote_count,
airDate: tv.last_episode_to_air.air_date ?? '',
episodeNumber: tv.last_episode_to_air.episode_number,
productionCode: tv.last_episode_to_air.production_code,
runtime: tv.last_episode_to_air.runtime ?? 0,
seasonNumber: tv.last_episode_to_air.season_number,
showId: tv.last_episode_to_air.show_id,
stillPath: tv.last_episode_to_air.still_path
} : null,
Name: tv.name,
Networks: tv.networks.map(network => ({
id: network.id,
logoPath: network.logo_path,
name: network.name,
originCountry: network.origin_country
})),
NumberOfEpisodes: tv.number_of_episodes,
NumberOfSeasons: tv.number_of_seasons,
OriginCountry: tv.origin_country,
OriginalLanguage: tv.original_language,
OriginalName: tv.original_name,
Overview: tv.overview,
Popularity: tv.popularity,
PosterPath: tv.poster_path ?? '',
ProductionCompanies: tv.production_companies.map(company => ({
id: company.id,
logoPath: company.logo_path,
name: company.name,
originCountry: company.origin_country
})),
ProductionCountries: tv.production_countries.map(country => ({
iso31661: country.iso_3166_1,
name: country.name
})),
Seasons: tv.seasons.map(season => ({
airDate: season.air_date,
episodeCount: season.episode_count,
id: season.id,
name: season.name,
overview: season.overview,
posterPath: season.poster_path,
seasonNumber: season.season_number,
voteAverage: season.vote_average
})),
SpokenLanguages: tv.spoken_languages.map(lang => ({
englishName: lang.english_name,
iso6391: lang.iso_639_1,
name: lang.name
})),
Status: tv.status,
Tagline: tv.tagline,
Type: tv.type,
VoteAverage: tv.vote_average,
VoteCount: tv.vote_count,
})
+6 -5
View File
@@ -21,15 +21,16 @@ async function searchMovie() {
isSearching.value = true isSearching.value = true
searchPage.value = 1 searchPage.value = 1
const result = await searchMovies(searchQuery.value) const result = await searchMovies(searchQuery.value)
if (!result.Response) { console.log(result)
if (result.totalResults === 0) {
movies.value = [] movies.value = []
seachError.value = result.ErrorMessage seachError.value = 'No results found'
return return
} }
seachError.value = '' seachError.value = ''
movies.value = result.Search movies.value = result.Results
state.setState(searchPage.value, searchQuery.value, result.Search) state.setState(searchPage.value, searchQuery.value, result.Results)
} catch (error) { } catch (error) {
movies.value = [] movies.value = []
console.error(error) console.error(error)
@@ -44,7 +45,7 @@ async function loadMore() {
isLoadingMore.value = true isLoadingMore.value = true
searchPage.value++ searchPage.value++
const result = await loadMoreMovies(searchQuery.value, searchPage.value) const result = await loadMoreMovies(searchQuery.value, searchPage.value)
movies.value?.push(...result.Search) movies.value?.push(...result.Results)
state.setState(searchPage.value, searchQuery.value, movies.value ? movies.value : []) state.setState(searchPage.value, searchQuery.value, movies.value ? movies.value : [])
} catch (error) { } catch (error) {
searchPage.value = 1 searchPage.value = 1