Bruno Predot

Dans la pade détails, ajout d'un computed pour récupération dyynanique de l'id c…

…ontenu dans la route + d'un computed pour récupérer le film concerné dans le store.
Mise à jour du Model Movie + de linterface.
Ajout du fetch des détails.
Ajout du contenu principal du film dans le template.
1 export interface MovieInterface { 1 export interface MovieInterface {
2 id: number; 2 id: number;
3 adult: boolean; 3 adult: boolean;
4 - backdrop_pat: string; 4 + backdrop_path: string;
5 genre_ids: number[]; 5 genre_ids: number[];
  6 + genres: Genre[];
6 original_language: string; 7 original_language: string;
7 original_title: string; 8 original_title: string;
8 overview: string; 9 overview: string;
9 popularity: number; 10 popularity: number;
10 poster_path: string | null; 11 poster_path: string | null;
11 release_date: string; 12 release_date: string;
  13 + runtime: number
12 title: string; 14 title: string;
13 video: boolean; 15 video: boolean;
14 vote_average: number; 16 vote_average: number;
15 vote_count: number; 17 vote_count: number;
  18 +}
  19 +
  20 +type Genre = {
  21 + id: number,
  22 + name: string,
16 } 23 }
@@ -22,7 +22,7 @@ export class Movie extends Model { @@ -22,7 +22,7 @@ export class Movie extends Model {
22 // Attributs. 22 // Attributs.
23 id: this.number(null), 23 id: this.number(null),
24 adult: this.boolean(false), 24 adult: this.boolean(false),
25 - backdrop_pat: this.string(null), 25 + backdrop_path: this.string(null),
26 belongs_to_collection: this.attr(null), 26 belongs_to_collection: this.attr(null),
27 budget: this.number(null), 27 budget: this.number(null),
28 genre_ids: this.attr([]), 28 genre_ids: this.attr([]),
1 <script setup lang="ts"> 1 <script setup lang="ts">
2 -import { ArrowLeftIcon } from "lucide-vue-next"; 2 +//#region --import--.
  3 +import { ArrowLeftIcon, FilmIcon } from "lucide-vue-next";
  4 +import { useTMDB } from "~/composables/tMDB";
  5 +import { onMounted } from "vue";
  6 +import { Movie } from "~/models/movie";
  7 +import type { MovieInterface } from "~/interfaces/movie";
  8 +//#endregion
  9 +
  10 +//#region --Declaration--.
  11 +const { fetchPopularMovies, searchMovies, fetchMovieDetails } = useTMDB();
  12 +//#endregion
  13 +
  14 +//#region --Declaration--.
  15 +const { currentRoute } = useRouter();
  16 +//#endregion
  17 +
  18 +//#region --Computed--.
  19 +const movieId = computed(() => {
  20 + if (currentRoute.value.params.id) {
  21 + if (typeof currentRoute.value.params.id === 'string') {
  22 + if (typeof Number(+currentRoute.value.params.id) === "number") {
  23 + return +currentRoute.value.params.id as number;
  24 + } else {
  25 + return currentRoute.value.params.id as string;
  26 + }
  27 + } else {
  28 + return null;
  29 + }
  30 + } else {
  31 + return null;
  32 + }
  33 +});
  34 +
  35 +const movie = computed(() => {
  36 + if (unref(movieId)) {
  37 + // Todo : revoir ici.
  38 + return useRepo(Movie).query().where('id', movieId.value).withAll().first() as unknown as MovieInterface;
  39 + } else {
  40 + return null;
  41 + }
  42 +});
  43 +//#endregion
  44 +
  45 +
  46 +
  47 +//#region --Function--.
  48 +/**
  49 + * Fetch movie details
  50 + */
  51 +const fetchDetails = async (id: number|string) => {
  52 + try {
  53 + // isLoading.value = true
  54 +
  55 + const data = await fetchMovieDetails(id);
  56 + console.log('data', data)
  57 + // Add to store collection.
  58 + useRepo(Movie).save(data);
  59 + } catch (error) {
  60 + console.error('Error fetching movie details:', error)
  61 + // movie.value = null
  62 + } finally {
  63 + // isLoading.value = false
  64 + }
  65 +}
  66 +
  67 +/**
  68 + * Format runtime
  69 + * @param minutes
  70 + */
  71 +const formatRuntime = (minutes: number) => {
  72 + if (!minutes) return 'Durée inconnue';
  73 + // Find nb hours.
  74 + const hours = Math.floor(minutes / 60);
  75 + // Find last minutes.
  76 + const mins = minutes % 60;
  77 +
  78 + return `${hours}h ${mins}min`;
  79 +}
  80 +
  81 +/**
  82 + * Format vote count if > 1000.
  83 + * @param count
  84 + */
  85 +const formatVoteCount = (count: number) => {
  86 + if (count >= 1000) {
  87 + return `${(count / 1000).toFixed(1)}k votes`
  88 + }
  89 + return `${count} votes`
  90 +}
  91 +//#endregion
  92 +
  93 +
  94 +//#region --Global event--.
  95 +onMounted(() => {
  96 + // Fetch data on component mount.
  97 + if (unref(movieId)) {
  98 + const id = unref(movieId) as string|number;
  99 + fetchDetails(id)
  100 + }
  101 + // fetchMovieCredits()
  102 + // loadComments()
  103 +});
  104 +//#endregion
3 </script> 105 </script>
4 106
5 <template> 107 <template>
6 <section> 108 <section>
7 <!-- Skeleton loader pendant le chargement --> 109 <!-- Skeleton loader pendant le chargement -->
8 <ui-components-skeleton-movie-detail-loader /> 110 <ui-components-skeleton-movie-detail-loader />
9 - <button  
10 - class="flex items-center text-gray-400 hover:text-white mb-8 transition-colors"  
11 - @click="navigateTo('/')"  
12 - >  
13 - <ArrowLeftIcon :size="20" class="mr-2" />  
14 - Retour  
15 - </button>  
16 111
  112 + <!-- Contenu du film -->
  113 + <div v-if="movie" class="relative">
  114 + <!-- Backdrop image -->
  115 + <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"/>
  117 + <img
  118 + v-if="movie.backdrop_path"
  119 + :src="`https://image.tmdb.org/t/p/original${movie.backdrop_path}`"
  120 + :alt="movie.title"
  121 + class="w-full h-full object-cover opacity-30"
  122 + >
  123 + </div>
  124 +
  125 + <!-- Contenu principal -->
  126 + <div class="container mx-auto px-4 py-8 relative z-10 pt-20">
  127 + <button
  128 + class="flex items-center text-gray-400 hover:text-white mb-8 transition-colors"
  129 + @click="navigateTo('/')"
  130 + >
  131 + <ArrowLeftIcon :size="20" class="mr-2" />
  132 + Retour
  133 + </button>
  134 +
  135 + <div class="flex flex-col md:flex-row gap-8">
  136 + <!-- Poster -->
  137 + <div class="w-full md:w-1/3 lg:w-1/4">
  138 + <div class="rounded-lg overflow-hidden shadow-lg bg-gray-800">
  139 + <img
  140 + v-if="movie.poster_path"
  141 + :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
  142 + :alt="movie.title"
  143 + class="w-full h-auto"
  144 + >
  145 + <div v-else class="aspect-[2/3] bg-gray-700 flex items-center justify-center">
  146 + <FilmIcon :size="64" class="text-gray-500" />
  147 + </div>
  148 + </div>
  149 + </div>
  150 +
  151 + <!-- Informations du film -->
  152 + <div class="w-full md:w-2/3 lg:w-3/4">
  153 + <h1 class="text-3xl md:text-4xl font-bold mb-2">{{ movie.title }}</h1>
  154 + <p v-if="movie.release_date" class="text-gray-400 mb-4">
  155 + {{ useDateFormat(movie.release_date, "DD-MM-YYYY") }} • {{ formatRuntime(movie.runtime) }}
  156 + </p>
  157 +
  158 + <!-- Note et votes -->
  159 + <div class="flex items-center mb-6">
  160 + <div class="bg-primary text-white rounded-full w-12 h-12 flex items-center justify-center font-bold mr-3">
  161 + {{ movie.vote_average.toFixed(1) }}
  162 + </div>
  163 + <div>
  164 + <div class="font-semibold">Note TMDB</div>
  165 + <div class="text-sm text-gray-400">{{ formatVoteCount(movie.vote_count) }}</div>
  166 + </div>
  167 + </div>
  168 +
  169 + <!-- Genres -->
  170 + <div class="mb-6">
  171 + <div class="flex flex-wrap gap-2">
  172 + <span
  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 }}
  178 + </span>
  179 + </div>
  180 + </div>
  181 +
  182 + <!-- Synopsis -->
  183 + <div class="mb-6">
  184 + <h2 class="text-xl font-bold mb-2">Synopsis</h2>
  185 + <p class="text-gray-300">{{ movie.overview || 'Aucun synopsis disponible.' }}</p>
  186 + </div>
  187 +
  188 + </div>
  189 + </div>
  190 + </div>
  191 + </div>
17 </section> 192 </section>
18 </template> 193 </template>
19 194