MoviesList.vue
4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<script lang="ts" setup>
//#region --import--.
import SearchBar from "~/components/SearchBar.vue";
import { onBeforeUnmount, ref } from "vue";
import { useTMDB } from "~/composables/tMDB";
import { Movie } from "~/models/movie";
import { FilmIcon } from "lucide-vue-next";
import type { MovieInterface } from "~/interfaces/movie";
import { useDateFormat } from '@vueuse/core'
//#endregion
//#region --Declaration--.
const { fetchPopularMovies } = useTMDB();
//#endregion
//#region --Data/refs--.
const isInitialLoading = ref(true);
const isLoadingMore = ref(false);
const currentPage = ref(1);
const totalPages = ref(0);
/** Elément observé pour le défilement infini. */
const loadMoreTrigger = ref<HTMLElement | null>(null);
/** Instance de IntersectionObserver */
const observer = ref<IntersectionObserver | null>(null);
//#endregion
//#region --Computed--.
const movies = computed(() => {
return useRepo(Movie).query().orderBy("popularity", 'desc').get() as unknown as MovieInterface[];
});
//#endregion
//#region --Function--.
/**
* Fetch popular movies
* @param page
*/
const fetchMovies = async (page: number) => {
try {
isLoadingMore.value = true;
const data = await fetchPopularMovies(page);
// Save in Movie model.
isInitialLoading.value ? useRepo(Movie).fresh(data.results) : useRepo(Movie).save(data.results);
totalPages.value = data.total_pages;
currentPage.value = page;
} catch (error) {
console.error("Error fetching popular movies:", error);
} finally {
isInitialLoading.value = false;
isLoadingMore.value = false;
}
};
function createIntersectionObserver() {
return new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && !isLoadingMore.value && currentPage.value < totalPages.value) {
fetchMovies(currentPage.value + 1);
}
},
{ threshold: 1.0 },
);
}
//#endregion
//#region --Global event--.
onMounted(() => {
// Chargement des premiers films.
fetchMovies(1);
// Création et stockage dans la ref de l'instance IntersectionObserver.
observer.value = createIntersectionObserver();
if (loadMoreTrigger.value) {
// Début d'observation de la div pour le défilement infini.
observer.value.observe(loadMoreTrigger.value);
}
if (loadMoreTrigger.value) {
observer.value.observe(loadMoreTrigger.value);
}
});
onBeforeUnmount(() => {
// Disconnect the observer when the component is unmounted
if (observer.value) {
observer.value.disconnect();
}
});
//#endregion
</script>
<template>
<section>
<h1 class="text-4xl font-bold mb-8 text-center">Découvrez les films populaires</h1>
<!-- Barre de recherche -->
<search-bar placeholder="Rechercher un film..." />
<!-- Loading Skeleton -->
<skeleton-movies-loader v-if="isInitialLoading" :is-initial-loading="isInitialLoading" :skeleton-number="20" />
<!-- Liste des films -->
<div v-else-if="movies.length > 0" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<div
v-for="movie in movies"
:key="movie.id"
class="bg-gray-800 rounded-lg overflow-hidden shadow-lg transition-transform duration-300 hover:scale-105 cursor-pointer"
@click="navigateTo(`/movies/${movie.id}`)"
>
<div class="relative pb-[150%]">
<img
v-if="movie.poster_path"
:src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
:alt="movie.title"
class="absolute inset-0 w-full h-full object-cover"
>
<div v-else class="absolute inset-0 w-full h-full bg-gray-700 flex items-center justify-center">
<FilmIcon :size="48" class="text-gray-500" />
</div>
<div
class="absolute top-2 right-2 bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold"
>
{{ movie.vote_average.toFixed(1) }}
</div>
</div>
<div class="p-4">
<h2 class="text-lg font-bold mb-1 line-clamp-1">{{ movie.title }}</h2>
<p class="text-sm text-gray-400">{{ useDateFormat(movie.release_date, 'DD-MM-YYYY') }}</p>
</div>
</div>
</div>
<!-- Élément observé pour le défilement infini -->
<div ref="loadMoreTrigger" class="h-10 mt-4"/>
</section>
</template>
<style scoped></style>