Bruno Predot

Mise en place d'un model pour le fetch des crédits + interface.

Ajout fonction dans le composable useTMDB.
Modification interface et model Movie pour ajouter la liaison avec Credit.
Ajout du fetch des credit dans la page détails.
import type { RuntimeConfig } from "nuxt/schema";
export const useTMDB = function() {
export const useTMDB = function () {
const runtimeconfig: RuntimeConfig = useRuntimeConfig();
const apiUrl = runtimeconfig.public.apiTMDBUrl;
const apiKey = runtimeconfig.public.apiTMDBSecret;
... ... @@ -11,9 +11,7 @@ export const useTMDB = function() {
*/
const fetchPopularMovies = async (page: number) => {
try {
const response = await fetch(
`${apiUrl}/movie/popular?api_key=${apiKey}&language=fr-FR&page=${page}`,
);
const response = await fetch(`${apiUrl}/movie/popular?api_key=${apiKey}&language=fr-FR&page=${page}`);
if (!response.ok) {
console.error("An error occurred when fetching popular movies:");
} else {
... ... @@ -48,11 +46,9 @@ export const useTMDB = function() {
* Fetch movie details by id.
* @param id
*/
const fetchMovieDetails = async (id: number|string) => {
const fetchMovieDetails = async (id: number | string) => {
try {
const response = await fetch(
`${apiUrl}/movie/${id}?api_key=${apiKey}&language=fr-FR`,
);
const response = await fetch(`${apiUrl}/movie/${id}?api_key=${apiKey}&language=fr-FR`);
if (!response.ok) {
console.error("An error occurred when fetching movie details:");
} else {
... ... @@ -63,5 +59,21 @@ export const useTMDB = function() {
}
};
return { fetchPopularMovies, searchMovies, fetchMovieDetails }
}
\ No newline at end of file
/**
* Fetch movie credits
*/
const fetchMovieCredits = async (id: number | string) => {
try {
const response = await fetch(`${apiUrl}/movie/${id}/credits?api_key=${apiKey}&language=fr-FR`);
if (!response.ok) {
console.error("An error occurred when fetching movie credits:");
} else {
return await response.json();
}
} catch (error) {
console.error("Error fetching movie credits:", error);
}
};
return { fetchPopularMovies, searchMovies, fetchMovieDetails, fetchMovieCredits };
};
... ...
import type { MovieInterface } from "~/interfaces/movie";
export interface CreditInterface {
id: number;
name: string;
job?: string;
character?: string;
}
export type CreditsResponse = {
id: number;
cast: CreditInterface[],
crew: CreditInterface[],
movie_id: unknown;
movie: MovieInterface;
}
... ...
import type { CreditInterface } from "~/interfaces/credit";
export interface MovieInterface {
id: number;
adult: boolean;
... ... @@ -15,6 +17,7 @@ export interface MovieInterface {
video: boolean;
vote_average: number;
vote_count: number;
credit: CreditInterface;
}
type Genre = {
... ...
import { Model } from "pinia-orm";
import { Movie } from "~/models/movie";
export class Credit extends Model {
/**
*
* @return {string}
*/
static get entity() {
return "Credit";
}
/**
*
* @return {string}
*/
static get primaryKey() {
return "id";
}
static fields() {
return {
// Attributs.
id: this.number(null),
cast: this.attr([]),
crew: this.attr([]),
// Relations.
movie_id: this.attr(null),
movie: this.belongsTo(Movie, "movie_id", "id"),
};
}
static piniaOptions = {
persist: true,
};
}
... ...
import { Model } from "pinia-orm";
import { Credit } from "~/models/credit";
export class Movie extends Model {
/**
... ... @@ -48,11 +49,11 @@ export class Movie extends Model {
vote_average: this.number(null),
vote_count: this.number(null),
// Relations.
credit: this.hasOne(Credit, "movie_id", "id"),
};
}
static piniaOptions = {
persist: true,
};
}
... ...
<script setup lang="ts">
<script lang="ts" setup>
//#region --import--.
import { ArrowLeftIcon, FilmIcon } from "lucide-vue-next";
import { useTMDB } from "~/composables/tMDB";
import { onMounted } from "vue";
import { onMounted, ref } from "vue";
import { Movie } from "~/models/movie";
import type { MovieInterface } from "~/interfaces/movie";
import { Credit } from "~/models/credit";
import type { CreditInterface, CreditsResponse } from "~/interfaces/credit";
//#endregion
//#region --Declaration--.
const { fetchPopularMovies, searchMovies, fetchMovieDetails } = useTMDB();
const { fetchMovieDetails, fetchMovieCredits } = useTMDB();
//#endregion
//#region --Declaration--.
const { currentRoute } = useRouter();
//#endregion
//#region --Data/ref--.
const isLoading = ref(true);
//#endregion
//#region --Computed--.
const movieId = computed(() => {
if (currentRoute.value.params.id) {
if (typeof currentRoute.value.params.id === 'string') {
if (typeof currentRoute.value.params.id === "string") {
if (typeof Number(+currentRoute.value.params.id) === "number") {
return +currentRoute.value.params.id as number;
} else {
... ... @@ -35,48 +41,44 @@ const movieId = computed(() => {
const movie = computed(() => {
if (unref(movieId)) {
// Todo : revoir ici.
return useRepo(Movie).query().where('id', movieId.value).withAll().first() as unknown as MovieInterface;
return useRepo(Movie).query().where("id", movieId.value).withAll().first() as unknown as MovieInterface;
} else {
return null;
}
});
//#endregion
//#region --Function--.
/**
* Fetch movie details
*/
const fetchDetails = async (id: number|string) => {
const fetchDetails = async (id: number | string) => {
try {
// isLoading.value = true
isLoading.value = true;
const data = await fetchMovieDetails(id);
console.log('data', data)
// Add to store collection.
useRepo(Movie).save(data);
} catch (error) {
console.error('Error fetching movie details:', error)
// movie.value = null
console.error("Error fetching movie details:", error);
} finally {
// isLoading.value = false
isLoading.value = false;
}
}
};
/**
* Format runtime
* @param minutes
*/
const formatRuntime = (minutes: number) => {
if (!minutes) return 'Durée inconnue';
if (!minutes) return "Durée inconnue";
// Find nb hours.
const hours = Math.floor(minutes / 60);
// Find last minutes.
const mins = minutes % 60;
return `${hours}h ${mins}min`;
}
};
/**
* Format vote count if > 1000.
... ... @@ -84,21 +86,33 @@ const formatRuntime = (minutes: number) => {
*/
const formatVoteCount = (count: number) => {
if (count >= 1000) {
return `${(count / 1000).toFixed(1)}k votes`
return `${(count / 1000).toFixed(1)}k votes`;
}
return `${count} votes`;
};
async function fetchCredits(id: number|string) {
try {
const data = await fetchMovieCredits(id) as CreditsResponse;
data.movie_id = id;
// Add to store collection.
console.log('credit response', data)
useRepo(Credit).save(data);
} catch (error) {
console.error("Error fetching movie credits:", error);
}
return `${count} votes`
}
//#endregion
//#region --Global event--.
onMounted(() => {
// Fetch data on component mount.
if (unref(movieId)) {
const id = unref(movieId) as string|number;
fetchDetails(id)
const id = unref(movieId) as string | number;
fetchDetails(id);
fetchCredits(id)
}
// fetchMovieCredits()
// loadComments()
});
//#endregion
... ... @@ -107,19 +121,19 @@ onMounted(() => {
<template>
<section>
<!-- Skeleton loader pendant le chargement -->
<ui-components-skeleton-movie-detail-loader />
<ui-components-skeleton-movie-detail-loader v-if="isLoading" />
<!-- Contenu du film -->
<div v-if="movie" class="relative">
<div v-else-if="movie" class="relative">
<!-- Backdrop image -->
<div class="absolute inset-0 h-[500px] overflow-hidden z-0">
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-gray-900"/>
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-gray-900" />
<img
v-if="movie.backdrop_path"
:src="`https://image.tmdb.org/t/p/original${movie.backdrop_path}`"
:alt="movie.title"
:src="`https://image.tmdb.org/t/p/original${movie.backdrop_path}`"
class="w-full h-full object-cover opacity-30"
>
/>
</div>
<!-- Contenu principal -->
... ... @@ -138,10 +152,10 @@ onMounted(() => {
<div class="rounded-lg overflow-hidden shadow-lg bg-gray-800">
<img
v-if="movie.poster_path"
:src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
:alt="movie.title"
:src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
class="w-full h-auto"
>
/>
<div v-else class="aspect-[2/3] bg-gray-700 flex items-center justify-center">
<FilmIcon :size="64" class="text-gray-500" />
</div>
... ... @@ -169,11 +183,7 @@ onMounted(() => {
<!-- Genres -->
<div class="mb-6">
<div class="flex flex-wrap gap-2">
<span
v-for="genre in movie.genres"
:key="genre.id"
class="px-3 py-1 bg-gray-800 rounded-full text-sm"
>
<span v-for="genre in movie.genres" :key="genre.id" class="px-3 py-1 bg-gray-800 rounded-full text-sm">
{{ genre.name }}
</span>
</div>
... ... @@ -182,9 +192,8 @@ onMounted(() => {
<!-- Synopsis -->
<div class="mb-6">
<h2 class="text-xl font-bold mb-2">Synopsis</h2>
<p class="text-gray-300">{{ movie.overview || 'Aucun synopsis disponible.' }}</p>
<p class="text-gray-300">{{ movie.overview || "Aucun synopsis disponible." }}</p>
</div>
</div>
</div>
</div>
... ... @@ -192,6 +201,4 @@ onMounted(() => {
</section>
</template>
<style scoped>
</style>
\ No newline at end of file
<style scoped></style>
\ No newline at end of file
... ...