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.
1 import type { RuntimeConfig } from "nuxt/schema"; 1 import type { RuntimeConfig } from "nuxt/schema";
2 2
3 -export const useTMDB = function() { 3 +export const useTMDB = function () {
4 const runtimeconfig: RuntimeConfig = useRuntimeConfig(); 4 const runtimeconfig: RuntimeConfig = useRuntimeConfig();
5 const apiUrl = runtimeconfig.public.apiTMDBUrl; 5 const apiUrl = runtimeconfig.public.apiTMDBUrl;
6 const apiKey = runtimeconfig.public.apiTMDBSecret; 6 const apiKey = runtimeconfig.public.apiTMDBSecret;
@@ -11,9 +11,7 @@ export const useTMDB = function() { @@ -11,9 +11,7 @@ export const useTMDB = function() {
11 */ 11 */
12 const fetchPopularMovies = async (page: number) => { 12 const fetchPopularMovies = async (page: number) => {
13 try { 13 try {
14 - const response = await fetch( 14 + const response = await fetch(`${apiUrl}/movie/popular?api_key=${apiKey}&language=fr-FR&page=${page}`);
15 - `${apiUrl}/movie/popular?api_key=${apiKey}&language=fr-FR&page=${page}`,  
16 - );  
17 if (!response.ok) { 15 if (!response.ok) {
18 console.error("An error occurred when fetching popular movies:"); 16 console.error("An error occurred when fetching popular movies:");
19 } else { 17 } else {
@@ -48,11 +46,9 @@ export const useTMDB = function() { @@ -48,11 +46,9 @@ export const useTMDB = function() {
48 * Fetch movie details by id. 46 * Fetch movie details by id.
49 * @param id 47 * @param id
50 */ 48 */
51 - const fetchMovieDetails = async (id: number|string) => { 49 + const fetchMovieDetails = async (id: number | string) => {
52 try { 50 try {
53 - const response = await fetch( 51 + const response = await fetch(`${apiUrl}/movie/${id}?api_key=${apiKey}&language=fr-FR`);
54 - `${apiUrl}/movie/${id}?api_key=${apiKey}&language=fr-FR`,  
55 - );  
56 if (!response.ok) { 52 if (!response.ok) {
57 console.error("An error occurred when fetching movie details:"); 53 console.error("An error occurred when fetching movie details:");
58 } else { 54 } else {
@@ -63,5 +59,21 @@ export const useTMDB = function() { @@ -63,5 +59,21 @@ export const useTMDB = function() {
63 } 59 }
64 }; 60 };
65 61
66 - return { fetchPopularMovies, searchMovies, fetchMovieDetails } 62 + /**
67 -} 63 + * Fetch movie credits
  64 + */
  65 + const fetchMovieCredits = async (id: number | string) => {
  66 + try {
  67 + const response = await fetch(`${apiUrl}/movie/${id}/credits?api_key=${apiKey}&language=fr-FR`);
  68 + if (!response.ok) {
  69 + console.error("An error occurred when fetching movie credits:");
  70 + } else {
  71 + return await response.json();
  72 + }
  73 + } catch (error) {
  74 + console.error("Error fetching movie credits:", error);
  75 + }
  76 + };
  77 +
  78 + return { fetchPopularMovies, searchMovies, fetchMovieDetails, fetchMovieCredits };
  79 +};
  1 +import type { MovieInterface } from "~/interfaces/movie";
  2 +
  3 +export interface CreditInterface {
  4 + id: number;
  5 + name: string;
  6 + job?: string;
  7 + character?: string;
  8 +}
  9 +
  10 +export type CreditsResponse = {
  11 + id: number;
  12 + cast: CreditInterface[],
  13 + crew: CreditInterface[],
  14 + movie_id: unknown;
  15 + movie: MovieInterface;
  16 +}
  1 +import type { CreditInterface } from "~/interfaces/credit";
  2 +
1 export interface MovieInterface { 3 export interface MovieInterface {
2 id: number; 4 id: number;
3 adult: boolean; 5 adult: boolean;
@@ -15,6 +17,7 @@ export interface MovieInterface { @@ -15,6 +17,7 @@ export interface MovieInterface {
15 video: boolean; 17 video: boolean;
16 vote_average: number; 18 vote_average: number;
17 vote_count: number; 19 vote_count: number;
  20 + credit: CreditInterface;
18 } 21 }
19 22
20 type Genre = { 23 type Genre = {
  1 +import { Model } from "pinia-orm";
  2 +import { Movie } from "~/models/movie";
  3 +
  4 +export class Credit extends Model {
  5 + /**
  6 + *
  7 + * @return {string}
  8 + */
  9 + static get entity() {
  10 + return "Credit";
  11 + }
  12 +
  13 + /**
  14 + *
  15 + * @return {string}
  16 + */
  17 + static get primaryKey() {
  18 + return "id";
  19 + }
  20 +
  21 + static fields() {
  22 + return {
  23 + // Attributs.
  24 + id: this.number(null),
  25 + cast: this.attr([]),
  26 + crew: this.attr([]),
  27 + // Relations.
  28 + movie_id: this.attr(null),
  29 + movie: this.belongsTo(Movie, "movie_id", "id"),
  30 + };
  31 + }
  32 +
  33 + static piniaOptions = {
  34 + persist: true,
  35 + };
  36 +}
1 import { Model } from "pinia-orm"; 1 import { Model } from "pinia-orm";
  2 +import { Credit } from "~/models/credit";
2 3
3 export class Movie extends Model { 4 export class Movie extends Model {
4 /** 5 /**
@@ -48,11 +49,11 @@ export class Movie extends Model { @@ -48,11 +49,11 @@ export class Movie extends Model {
48 vote_average: this.number(null), 49 vote_average: this.number(null),
49 vote_count: this.number(null), 50 vote_count: this.number(null),
50 // Relations. 51 // Relations.
  52 + credit: this.hasOne(Credit, "movie_id", "id"),
51 }; 53 };
52 } 54 }
53 55
54 static piniaOptions = { 56 static piniaOptions = {
55 persist: true, 57 persist: true,
56 }; 58 };
57 -  
58 } 59 }
1 -<script setup lang="ts"> 1 +<script lang="ts" setup>
2 //#region --import--. 2 //#region --import--.
3 import { ArrowLeftIcon, FilmIcon } from "lucide-vue-next"; 3 import { ArrowLeftIcon, FilmIcon } from "lucide-vue-next";
4 import { useTMDB } from "~/composables/tMDB"; 4 import { useTMDB } from "~/composables/tMDB";
5 -import { onMounted } from "vue"; 5 +import { onMounted, ref } from "vue";
6 import { Movie } from "~/models/movie"; 6 import { Movie } from "~/models/movie";
7 import type { MovieInterface } from "~/interfaces/movie"; 7 import type { MovieInterface } from "~/interfaces/movie";
  8 +import { Credit } from "~/models/credit";
  9 +import type { CreditInterface, CreditsResponse } from "~/interfaces/credit";
8 //#endregion 10 //#endregion
9 11
10 //#region --Declaration--. 12 //#region --Declaration--.
11 -const { fetchPopularMovies, searchMovies, fetchMovieDetails } = useTMDB(); 13 +const { fetchMovieDetails, fetchMovieCredits } = useTMDB();
12 //#endregion 14 //#endregion
13 15
14 //#region --Declaration--. 16 //#region --Declaration--.
15 const { currentRoute } = useRouter(); 17 const { currentRoute } = useRouter();
16 //#endregion 18 //#endregion
17 19
  20 +//#region --Data/ref--.
  21 +const isLoading = ref(true);
  22 +//#endregion
  23 +
18 //#region --Computed--. 24 //#region --Computed--.
19 const movieId = computed(() => { 25 const movieId = computed(() => {
20 if (currentRoute.value.params.id) { 26 if (currentRoute.value.params.id) {
21 - if (typeof currentRoute.value.params.id === 'string') { 27 + if (typeof currentRoute.value.params.id === "string") {
22 if (typeof Number(+currentRoute.value.params.id) === "number") { 28 if (typeof Number(+currentRoute.value.params.id) === "number") {
23 return +currentRoute.value.params.id as number; 29 return +currentRoute.value.params.id as number;
24 } else { 30 } else {
@@ -35,48 +41,44 @@ const movieId = computed(() => { @@ -35,48 +41,44 @@ const movieId = computed(() => {
35 const movie = computed(() => { 41 const movie = computed(() => {
36 if (unref(movieId)) { 42 if (unref(movieId)) {
37 // Todo : revoir ici. 43 // Todo : revoir ici.
38 - return useRepo(Movie).query().where('id', movieId.value).withAll().first() as unknown as MovieInterface; 44 + return useRepo(Movie).query().where("id", movieId.value).withAll().first() as unknown as MovieInterface;
39 } else { 45 } else {
40 return null; 46 return null;
41 } 47 }
42 }); 48 });
43 //#endregion 49 //#endregion
44 50
45 -  
46 -  
47 //#region --Function--. 51 //#region --Function--.
48 /** 52 /**
49 * Fetch movie details 53 * Fetch movie details
50 */ 54 */
51 -const fetchDetails = async (id: number|string) => { 55 +const fetchDetails = async (id: number | string) => {
52 try { 56 try {
53 - // isLoading.value = true 57 + isLoading.value = true;
54 58
55 const data = await fetchMovieDetails(id); 59 const data = await fetchMovieDetails(id);
56 - console.log('data', data)  
57 // Add to store collection. 60 // Add to store collection.
58 useRepo(Movie).save(data); 61 useRepo(Movie).save(data);
59 } catch (error) { 62 } catch (error) {
60 - console.error('Error fetching movie details:', error) 63 + console.error("Error fetching movie details:", error);
61 - // movie.value = null  
62 } finally { 64 } finally {
63 - // isLoading.value = false 65 + isLoading.value = false;
64 } 66 }
65 -} 67 +};
66 68
67 /** 69 /**
68 * Format runtime 70 * Format runtime
69 * @param minutes 71 * @param minutes
70 */ 72 */
71 const formatRuntime = (minutes: number) => { 73 const formatRuntime = (minutes: number) => {
72 - if (!minutes) return 'Durée inconnue'; 74 + if (!minutes) return "Durée inconnue";
73 // Find nb hours. 75 // Find nb hours.
74 const hours = Math.floor(minutes / 60); 76 const hours = Math.floor(minutes / 60);
75 // Find last minutes. 77 // Find last minutes.
76 const mins = minutes % 60; 78 const mins = minutes % 60;
77 79
78 return `${hours}h ${mins}min`; 80 return `${hours}h ${mins}min`;
79 -} 81 +};
80 82
81 /** 83 /**
82 * Format vote count if > 1000. 84 * Format vote count if > 1000.
@@ -84,21 +86,33 @@ const formatRuntime = (minutes: number) => { @@ -84,21 +86,33 @@ const formatRuntime = (minutes: number) => {
84 */ 86 */
85 const formatVoteCount = (count: number) => { 87 const formatVoteCount = (count: number) => {
86 if (count >= 1000) { 88 if (count >= 1000) {
87 - return `${(count / 1000).toFixed(1)}k votes` 89 + return `${(count / 1000).toFixed(1)}k votes`;
  90 + }
  91 + return `${count} votes`;
  92 +};
  93 +
  94 +async function fetchCredits(id: number|string) {
  95 + try {
  96 + const data = await fetchMovieCredits(id) as CreditsResponse;
  97 + data.movie_id = id;
  98 + // Add to store collection.
  99 + console.log('credit response', data)
  100 +
  101 + useRepo(Credit).save(data);
  102 + } catch (error) {
  103 + console.error("Error fetching movie credits:", error);
88 } 104 }
89 - return `${count} votes`  
90 } 105 }
91 //#endregion 106 //#endregion
92 107
93 -  
94 //#region --Global event--. 108 //#region --Global event--.
95 onMounted(() => { 109 onMounted(() => {
96 // Fetch data on component mount. 110 // Fetch data on component mount.
97 if (unref(movieId)) { 111 if (unref(movieId)) {
98 - const id = unref(movieId) as string|number; 112 + const id = unref(movieId) as string | number;
99 - fetchDetails(id) 113 + fetchDetails(id);
  114 + fetchCredits(id)
100 } 115 }
101 - // fetchMovieCredits()  
102 // loadComments() 116 // loadComments()
103 }); 117 });
104 //#endregion 118 //#endregion
@@ -107,19 +121,19 @@ onMounted(() => { @@ -107,19 +121,19 @@ onMounted(() => {
107 <template> 121 <template>
108 <section> 122 <section>
109 <!-- Skeleton loader pendant le chargement --> 123 <!-- Skeleton loader pendant le chargement -->
110 - <ui-components-skeleton-movie-detail-loader /> 124 + <ui-components-skeleton-movie-detail-loader v-if="isLoading" />
111 125
112 <!-- Contenu du film --> 126 <!-- Contenu du film -->
113 - <div v-if="movie" class="relative"> 127 + <div v-else-if="movie" class="relative">
114 <!-- Backdrop image --> 128 <!-- Backdrop image -->
115 <div class="absolute inset-0 h-[500px] overflow-hidden z-0"> 129 <div class="absolute inset-0 h-[500px] overflow-hidden z-0">
116 - <div class="absolute inset-0 bg-gradient-to-b from-transparent to-gray-900"/> 130 + <div class="absolute inset-0 bg-gradient-to-b from-transparent to-gray-900" />
117 <img 131 <img
118 v-if="movie.backdrop_path" 132 v-if="movie.backdrop_path"
119 - :src="`https://image.tmdb.org/t/p/original${movie.backdrop_path}`"  
120 :alt="movie.title" 133 :alt="movie.title"
  134 + :src="`https://image.tmdb.org/t/p/original${movie.backdrop_path}`"
121 class="w-full h-full object-cover opacity-30" 135 class="w-full h-full object-cover opacity-30"
122 - > 136 + />
123 </div> 137 </div>
124 138
125 <!-- Contenu principal --> 139 <!-- Contenu principal -->
@@ -138,10 +152,10 @@ onMounted(() => { @@ -138,10 +152,10 @@ onMounted(() => {
138 <div class="rounded-lg overflow-hidden shadow-lg bg-gray-800"> 152 <div class="rounded-lg overflow-hidden shadow-lg bg-gray-800">
139 <img 153 <img
140 v-if="movie.poster_path" 154 v-if="movie.poster_path"
141 - :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"  
142 :alt="movie.title" 155 :alt="movie.title"
  156 + :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
143 class="w-full h-auto" 157 class="w-full h-auto"
144 - > 158 + />
145 <div v-else class="aspect-[2/3] bg-gray-700 flex items-center justify-center"> 159 <div v-else class="aspect-[2/3] bg-gray-700 flex items-center justify-center">
146 <FilmIcon :size="64" class="text-gray-500" /> 160 <FilmIcon :size="64" class="text-gray-500" />
147 </div> 161 </div>
@@ -169,11 +183,7 @@ onMounted(() => { @@ -169,11 +183,7 @@ onMounted(() => {
169 <!-- Genres --> 183 <!-- Genres -->
170 <div class="mb-6"> 184 <div class="mb-6">
171 <div class="flex flex-wrap gap-2"> 185 <div class="flex flex-wrap gap-2">
172 - <span 186 + <span v-for="genre in movie.genres" :key="genre.id" class="px-3 py-1 bg-gray-800 rounded-full text-sm">
173 - v-for="genre in movie.genres"  
174 - :key="genre.id"  
175 - class="px-3 py-1 bg-gray-800 rounded-full text-sm"  
176 - >  
177 {{ genre.name }} 187 {{ genre.name }}
178 </span> 188 </span>
179 </div> 189 </div>
@@ -182,9 +192,8 @@ onMounted(() => { @@ -182,9 +192,8 @@ onMounted(() => {
182 <!-- Synopsis --> 192 <!-- Synopsis -->
183 <div class="mb-6"> 193 <div class="mb-6">
184 <h2 class="text-xl font-bold mb-2">Synopsis</h2> 194 <h2 class="text-xl font-bold mb-2">Synopsis</h2>
185 - <p class="text-gray-300">{{ movie.overview || 'Aucun synopsis disponible.' }}</p> 195 + <p class="text-gray-300">{{ movie.overview || "Aucun synopsis disponible." }}</p>
186 </div> 196 </div>
187 -  
188 </div> 197 </div>
189 </div> 198 </div>
190 </div> 199 </div>
@@ -192,6 +201,4 @@ onMounted(() => { @@ -192,6 +201,4 @@ onMounted(() => {
192 </section> 201 </section>
193 </template> 202 </template>
194 203
195 -<style scoped> 204 +<style scoped></style>
196 -  
197 -</style>