Bruno Predot

Merge branch 'hotfix/0.4.0'

1 module.exports = { 1 module.exports = {
  2 + // https://dev.to/tao/adding-eslint-and-prettier-to-nuxt-3-2023-5bg
2 root: true, 3 root: true,
3 - extends: ["@nuxtjs/eslint-config", "plugin:prettier/recommended"], 4 + extends: ["@nuxtjs/eslint-config"],
  5 + env: {
  6 + browser: true,
  7 + node: true,
  8 + },
  9 + parser: "vue-eslint-parser",
  10 + parserOptions: {
  11 + parser: "@typescript-eslint/parser",
  12 + },
  13 + plugins: [],
  14 + // add your custom rules here
  15 + rules: {},
4 }; 16 };
1 -node_modules  
2 -  
3 -logs  
4 -*.log*  
5 -  
6 -.output  
7 -.nuxt  
8 -.nitro  
9 -.cache  
10 -dist  
11 -  
12 -.gitignore  
13 -  
14 -.env  
15 -.env.*  
16 -!.env.example  
17 -  
18 -CHANGELOG.md  
19 -CHANGELOG_RELEASE.md  
20 -  
21 -.idea  
1 -{  
2 - "semi": true,  
3 - "trailingComma": "all",  
4 - "singleQuote": false,  
5 - "printWidth": 120,  
6 - "arrowParens": "always",  
7 - "useTabs": false,  
8 - "tabWidth": 2  
9 -}  
  1 +0.4.0:
  2 +- Modification de la config eslint avec la suppression de tout ce qui concerne prettier, suppression du module @nuxt/eslint, contenant le module eslint/recommanded, et remplacement par le module @antfu/eslint-config, plus complet et simple.
  3 +- Lintfix selon les règles de @antfu/eslint-config.
  4 +- Typage des props.
  5 +- Ré écriture des emits.
  6 +- Personalisation de rules antfu.
  7 +- Factorisation en incluant les guards clauses et ternaires.
  8 +- Nettoyage.
  9 +
  10 +0.3.4:
  11 +- Mise à jour dépendance avec ajout de typescript-eslint + fix
  12 +
  13 +0.3.3:
  14 +- Modif des script dans le package.json.
  15 +- Ajout lint exception.
  16 +- Fin config es-lint.
  17 +
  18 +0.3.2:
  19 +- Amélioration config es-lint + fix.
  20 +
1 0.3.1: 21 0.3.1:
2 - ajout fichier de test MovieGender.spec.ts. 22 - ajout fichier de test MovieGender.spec.ts.
3 - ajout fichier de test ScoreAndVote.spec.ts. 23 - ajout fichier de test ScoreAndVote.spec.ts.
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Props--. 2 +import type { MovieInterface } from "~/interfaces/movie";
  3 +// #region --Props--.
3 import { useDateFormat } from "@vueuse/core"; 4 import { useDateFormat } from "@vueuse/core";
4 import { FilmIcon } from "lucide-vue-next"; 5 import { FilmIcon } from "lucide-vue-next";
5 -//#endregion 6 +// #endregion
6 7
7 -//#region --Props--. 8 +// #region --Props--.
8 -defineProps({ 9 +/** Typescript typage */
9 - movie: { 10 +defineProps<{
10 - type: Object, 11 + movie: MovieInterface;
11 - required: true, 12 +}>();
12 - nullable: false, 13 +// #endregion
13 - },  
14 -});  
15 -//#endregion  
16 </script> 14 </script>
17 15
18 <template> 16 <template>
@@ -26,9 +24,15 @@ defineProps({ @@ -26,9 +24,15 @@ defineProps({
26 :alt="movie.title" 24 :alt="movie.title"
27 :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`" 25 :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
28 class="absolute inset-0 w-full h-full object-cover" 26 class="absolute inset-0 w-full h-full object-cover"
  27 + >
  28 + <div
  29 + v-else
  30 + class="absolute inset-0 w-full h-full bg-gray-700 flex items-center justify-center"
  31 + >
  32 + <FilmIcon
  33 + :size="48"
  34 + class="text-gray-500"
29 /> 35 />
30 - <div v-else class="absolute inset-0 w-full h-full bg-gray-700 flex items-center justify-center">  
31 - <FilmIcon :size="48" class="text-gray-500" />  
32 </div> 36 </div>
33 <div 37 <div
34 class="absolute top-2 right-2 bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold" 38 class="absolute top-2 right-2 bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold"
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Import--. 2 +// #region --Import--.
3 import type { MovieCommentInterface } from "~/interfaces/movieComment"; 3 import type { MovieCommentInterface } from "~/interfaces/movieComment";
4 import { MessageSquareIcon } from "lucide-vue-next"; 4 import { MessageSquareIcon } from "lucide-vue-next";
5 -//#endregion 5 +// #endregion
6 6
7 -//#region --Props--. 7 +// #region --Props--.
8 -const props = defineProps({ 8 +/** Typescript typage */
9 - comments: { 9 +const props = defineProps<{
10 - type: Array<MovieCommentInterface>, 10 + comments: Array<MovieCommentInterface>;
11 - required: true, 11 +}>();
12 - nullable: false, 12 +// #endregion
13 - },  
14 -});  
15 -//#endregion  
16 13
17 -//#region --Watch--. 14 +// #region --Watch--.
18 watch( 15 watch(
19 () => props.comments, 16 () => props.comments,
20 (comments) => { 17 (comments) => {
@@ -29,33 +26,55 @@ watch( @@ -29,33 +26,55 @@ watch(
29 }, 26 },
30 { immediate: true }, 27 { immediate: true },
31 ); 28 );
32 -//#endregion 29 +// #endregion
33 </script> 30 </script>
34 31
35 <template> 32 <template>
36 <section> 33 <section>
37 <!-- Liste des commentaires --> 34 <!-- Liste des commentaires -->
38 - <section v-if="comments.length > 0" class="mt-10"> 35 + <section
  36 + v-if="comments.length > 0"
  37 + class="mt-10"
  38 + >
39 <h2>Commentaires publiés</h2> 39 <h2>Commentaires publiés</h2>
40 - <div v-for="(comment, index) in comments" :key="index" class="bg-gray-800 rounded-lg p-6 mb-4"> 40 + <div
  41 + v-for="(comment, index) in comments"
  42 + :key="index"
  43 + class="bg-gray-800 rounded-lg p-6 mb-4"
  44 + >
41 <div class="flex justify-between items-start mb-2"> 45 <div class="flex justify-between items-start mb-2">
42 <section> 46 <section>
43 - <h4 class="font-bold text-lg">Par {{ comment.username }}</h4> 47 + <h4 class="font-bold text-lg">
44 - <p class="text-sm text-gray-400">Le {{ useDateFormat(comment.createdAt, "DD-MM-YYYY") }}</p> 48 + Par {{ comment.username }}
  49 + </h4>
  50 + <p class="text-sm text-gray-400">
  51 + Le {{ useDateFormat(comment.createdAt, "DD-MM-YYYY") }}
  52 + </p>
45 </section> 53 </section>
46 <section class="bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold"> 54 <section class="bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold">
47 {{ comment.rating }} 55 {{ comment.rating }}
48 </section> 56 </section>
49 </div> 57 </div>
50 - <p :id="`message${index}`" class="text-gray-300"> 58 + <p
  59 + :id="`message${index}`"
  60 + class="text-gray-300"
  61 + >
51 {{ comment.message }} 62 {{ comment.message }}
52 </p> 63 </p>
53 </div> 64 </div>
54 </section> 65 </section>
55 <!-- Si aucun commentaire --> 66 <!-- Si aucun commentaire -->
56 - <section v-else class="text-center py-8 bg-gray-800 rounded-lg mt-10"> 67 + <section
57 - <MessageSquareIcon :size="48" class="mx-auto mb-3 text-gray-600" /> 68 + v-else
58 - <p class="text-gray-400">Aucun commentaire pour le moment. Soyez le premier à donner votre avis !</p> 69 + class="text-center py-8 bg-gray-800 rounded-lg mt-10"
  70 + >
  71 + <MessageSquareIcon
  72 + :size="48"
  73 + class="mx-auto mb-3 text-gray-600"
  74 + />
  75 + <p class="text-gray-400">
  76 + Aucun commentaire pour le moment. Soyez le premier à donner votre avis !
  77 + </p>
59 </section> 78 </section>
60 </section> 79 </section>
61 </template> 80 </template>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --import--. 2 +import type { MovieInterface } from "~/interfaces/movie";
  3 +import { SearchXIcon } from "lucide-vue-next";
  4 +// #region --import--.
3 import { onBeforeUnmount, ref } from "vue"; 5 import { onBeforeUnmount, ref } from "vue";
4 import { useTMDB } from "~/composables/tMDB"; 6 import { useTMDB } from "~/composables/tMDB";
5 import { Movie } from "~/models/movie"; 7 import { Movie } from "~/models/movie";
6 -import { SearchXIcon } from "lucide-vue-next"; 8 +// #endregion
7 -import type { MovieInterface } from "~/interfaces/movie";  
8 -//#endregion  
9 9
10 -//#region --Declaration--. 10 +// #region --Declaration--.
11 const { fetchPopularMovies, searchMovies } = useTMDB(); 11 const { fetchPopularMovies, searchMovies } = useTMDB();
12 -//#endregion 12 +// #endregion
13 13
14 -//#region --Data/refs--. 14 +// #region --Data/refs--.
15 const isInitialLoading = ref(true); 15 const isInitialLoading = ref(true);
16 const isLoadingMore = ref(false); 16 const isLoadingMore = ref(false);
17 const currentPage = ref(1); 17 const currentPage = ref(1);
@@ -21,47 +21,43 @@ const searchQuery = ref(""); @@ -21,47 +21,43 @@ const searchQuery = ref("");
21 const loadMoreTrigger = ref<HTMLElement | null>(null); 21 const loadMoreTrigger = ref<HTMLElement | null>(null);
22 /** Instance de IntersectionObserver */ 22 /** Instance de IntersectionObserver */
23 const observer = ref<IntersectionObserver | null>(null); 23 const observer = ref<IntersectionObserver | null>(null);
24 -//#endregion 24 +// #endregion
25 25
26 -//#region --Computed--. 26 +// #region --Computed--.
27 const movies = computed(() => { 27 const movies = computed(() => {
28 return useRepo(Movie).query().orderBy("popularity", "desc").get() as unknown as MovieInterface[]; 28 return useRepo(Movie).query().orderBy("popularity", "desc").get() as unknown as MovieInterface[];
29 }); 29 });
30 -//#endregion 30 +// #endregion
31 31
32 -//#region --Function--. 32 +// #region --Function--.
33 /** 33 /**
34 * Fetch popular movies 34 * Fetch popular movies
35 * @param page 35 * @param page
36 */ 36 */
37 -const fetchMovies = async (page: number) => { 37 +async function fetchMovies(page: number) {
38 try { 38 try {
39 isLoadingMore.value = true; 39 isLoadingMore.value = true;
40 const data = await fetchPopularMovies(page); 40 const data = await fetchPopularMovies(page);
41 - // Save in Movie model. 41 + // Save in Movie model. If first fetch, erase old data before save or, add to store collection.
42 - if (isInitialLoading.value) { 42 + isInitialLoading.value ? useRepo(Movie).fresh(data.results) : useRepo(Movie).save(data.results);
43 - // First fetch, erase old data before save.  
44 - useRepo(Movie).fresh(data.results);  
45 - } else {  
46 - // Add to store collection.  
47 - useRepo(Movie).save(data.results);  
48 - }  
49 totalPages.value = data.total_pages; 43 totalPages.value = data.total_pages;
50 currentPage.value = page; 44 currentPage.value = page;
51 - } catch (error) { 45 + }
52 - console.error("Error fetching popular movies:", error); 46 + catch (error) {
53 - } finally { 47 + throw new Error(`Error fetching popular movies: ${error}`);
  48 + }
  49 + finally {
54 isInitialLoading.value = false; 50 isInitialLoading.value = false;
55 isLoadingMore.value = false; 51 isLoadingMore.value = false;
56 } 52 }
57 -}; 53 +}
58 54
59 /** 55 /**
60 * Search movies 56 * Search movies
61 * @param query 57 * @param query
62 * @param page 58 * @param page
63 */ 59 */
64 -const search = async (query: string, page: number) => { 60 +async function search(query: string, page: number) {
65 // If empty search, fetch popular movies. 61 // If empty search, fetch popular movies.
66 if (!query.trim()) { 62 if (!query.trim()) {
67 await fetchMovies(1); 63 await fetchMovies(1);
@@ -69,41 +65,29 @@ const search = async (query: string, page: number) => { @@ -69,41 +65,29 @@ const search = async (query: string, page: number) => {
69 } 65 }
70 try { 66 try {
71 isLoadingMore.value = true; 67 isLoadingMore.value = true;
72 - if (page === 1) { 68 + if (page === 1) isInitialLoading.value = true;
73 - isInitialLoading.value = true;  
74 - }  
75 const data = await searchMovies(query, page); 69 const data = await searchMovies(query, page);
76 - // Save in Movie model. 70 +
77 - if (isInitialLoading.value) { 71 + // Save in Movie model. If first fetch, erase old data before save or, add to store collection.
78 - // First fetch, erase old data before save. 72 + isInitialLoading.value ? useRepo(Movie).fresh(data.results) : useRepo(Movie).save(data.results);
79 - useRepo(Movie).fresh(data.results);  
80 - } else {  
81 - // Add to store collection.  
82 - useRepo(Movie).save(data.results);  
83 - }  
84 totalPages.value = data.total_pages; 73 totalPages.value = data.total_pages;
85 currentPage.value = page; 74 currentPage.value = page;
86 - } catch (error) { 75 + }
87 - console.error("Error searching movies:", error); 76 + catch (error) {
88 - } finally { 77 + throw new Error(`Error searching movies: ${error}`);
  78 + }
  79 + finally {
89 isInitialLoading.value = false; 80 isInitialLoading.value = false;
90 isLoadingMore.value = false; 81 isLoadingMore.value = false;
91 } 82 }
92 -}; 83 +}
93 84
94 function createIntersectionObserver() { 85 function createIntersectionObserver() {
95 return new IntersectionObserver( 86 return new IntersectionObserver(
96 (entries) => { 87 (entries) => {
97 const [entry] = entries; 88 const [entry] = entries;
98 - if (entry.isIntersecting && !isLoadingMore.value && currentPage.value < totalPages.value) { 89 + // Continue searching query if already active or, continue fetching popular movies.
99 - if (searchQuery.value) { 90 + if (entry.isIntersecting && !isLoadingMore.value && currentPage.value < totalPages.value) searchQuery.value ? search(searchQuery.value, currentPage.value + 1) : fetchMovies(currentPage.value + 1);
100 - // Continue searching query if already active.  
101 - search(searchQuery.value, currentPage.value + 1);  
102 - } else {  
103 - // Continue fetching popular movies.  
104 - fetchMovies(currentPage.value + 1);  
105 - }  
106 - }  
107 }, 91 },
108 { threshold: 1.0 }, 92 { threshold: 1.0 },
109 ); 93 );
@@ -122,41 +106,37 @@ function handleClearSearchEvent() { @@ -122,41 +106,37 @@ function handleClearSearchEvent() {
122 fetchMovies(1); 106 fetchMovies(1);
123 } 107 }
124 108
125 -//#endregion 109 +// #endregion
126 110
127 -//#region --Global event--. 111 +// #region --Global event--.
128 onMounted(() => { 112 onMounted(() => {
129 // First loading. 113 // First loading.
130 fetchMovies(1); 114 fetchMovies(1);
131 // Création et stockage dans la ref de l'instance IntersectionObserver. 115 // Création et stockage dans la ref de l'instance IntersectionObserver.
132 observer.value = createIntersectionObserver(); 116 observer.value = createIntersectionObserver();
133 - if (loadMoreTrigger.value) {  
134 // Début d'observation de la div pour le défilement infini. 117 // Début d'observation de la div pour le défilement infini.
135 - observer.value.observe(loadMoreTrigger.value); 118 + if (loadMoreTrigger.value) observer.value.observe(loadMoreTrigger.value);
136 - }  
137 119
138 - if (loadMoreTrigger.value) { 120 + if (loadMoreTrigger.value) observer.value.observe(loadMoreTrigger.value);
139 - observer.value.observe(loadMoreTrigger.value);  
140 - }  
141 }); 121 });
142 122
143 onBeforeUnmount(() => { 123 onBeforeUnmount(() => {
144 // Disconnect the observer when the component is unmounted. 124 // Disconnect the observer when the component is unmounted.
145 - if (observer.value) { 125 + if (observer.value) observer.value.disconnect();
146 - observer.value.disconnect();  
147 - }  
148 }); 126 });
149 -//#endregion 127 +// #endregion
150 </script> 128 </script>
151 129
152 <template> 130 <template>
153 <section> 131 <section>
154 - <h1 class="text-4xl font-bold mb-8 text-center">Découvrez les films populaires</h1> 132 + <h1 class="text-4xl font-bold mb-8 text-center">
  133 + Découvrez les films populaires
  134 + </h1>
155 <!-- Barre de recherche --> 135 <!-- Barre de recherche -->
156 <ui-components-search-bar 136 <ui-components-search-bar
157 placeholder="Rechercher un film..." 137 placeholder="Rechercher un film..."
158 - @event:search="handleSearchEvent" 138 + @event-search="handleSearchEvent"
159 - @event:clear_search="handleClearSearchEvent" 139 + @event-clear-search="handleClearSearchEvent"
160 /> 140 />
161 141
162 <!-- Loading Skeleton --> 142 <!-- Loading Skeleton -->
@@ -167,24 +147,46 @@ onBeforeUnmount(() => { @@ -167,24 +147,46 @@ onBeforeUnmount(() => {
167 /> 147 />
168 148
169 <!-- Liste des films --> 149 <!-- Liste des films -->
170 - <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"> 150 + <div
171 - <div v-for="movie in movies" :key="movie.id"> 151 + v-else-if="movies.length > 0"
  152 + class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"
  153 + >
  154 + <div
  155 + v-for="movie in movies"
  156 + :key="movie.id"
  157 + >
172 <movie-card :movie="movie" /> 158 <movie-card :movie="movie" />
173 </div> 159 </div>
174 </div> 160 </div>
175 161
176 <!-- Message si aucun film trouvé --> 162 <!-- Message si aucun film trouvé -->
177 - <section v-else-if="searchQuery && !movies.length" class="text-center py-12"> 163 + <section
178 - <SearchXIcon :size="64" class="mx-auto mb-4 text-gray-600" /> 164 + v-else-if="searchQuery && !movies.length"
179 - <h3 class="text-xl font-bold mb-2">Aucun film trouvé</h3> 165 + class="text-center py-12"
180 - <p class="text-gray-400">Essayez avec un autre terme de recherche</p> 166 + >
  167 + <SearchXIcon
  168 + :size="64"
  169 + class="mx-auto mb-4 text-gray-600"
  170 + />
  171 + <h3 class="text-xl font-bold mb-2">
  172 + Aucun film trouvé
  173 + </h3>
  174 + <p class="text-gray-400">
  175 + Essayez avec un autre terme de recherche
  176 + </p>
181 </section> 177 </section>
182 178
183 <!-- Loader pour le chargement de plus de films --> 179 <!-- Loader pour le chargement de plus de films -->
184 - <ui-components-loader :is-initial-loading="isInitialLoading" :is-loading="isLoadingMore" /> 180 + <ui-components-loader
  181 + :is-initial-loading="isInitialLoading"
  182 + :is-loading="isLoadingMore"
  183 + />
185 184
186 <!-- Élément observé pour le défilement infini --> 185 <!-- Élément observé pour le défilement infini -->
187 - <div ref="loadMoreTrigger" class="h-10 mt-4" /> 186 + <div
  187 + ref="loadMoreTrigger"
  188 + class="h-10 mt-4"
  189 + />
188 </section> 190 </section>
189 </template> 191 </template>
190 192
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Import--. 2 +// #region --Import--.
3 import type { Genre } from "~/interfaces/movie"; 3 import type { Genre } from "~/interfaces/movie";
4 -//#endregion 4 +// #endregion
5 5
6 -//#region --Props--. 6 +// #region --Props--.
7 -defineProps({ 7 +defineProps<{
8 - genres: { 8 + genres: Array<Genre>;
9 - type: Array<Genre>, 9 +}>();
10 - required: true, 10 +// #endregion
11 - nullable: false,  
12 - },  
13 -});  
14 -//#endregion  
15 </script> 11 </script>
16 12
17 <template> 13 <template>
18 <section class="mb-6"> 14 <section class="mb-6">
19 <div class="flex flex-wrap gap-2"> 15 <div class="flex flex-wrap gap-2">
20 - <span v-for="genre in genres" :key="genre.id" class="px-3 py-1 bg-gray-800 rounded-full text-sm"> 16 + <span
  17 + v-for="genre in genres"
  18 + :key="genre.id"
  19 + class="px-3 py-1 bg-gray-800 rounded-full text-sm"
  20 + >
21 {{ genre.name }} 21 {{ genre.name }}
22 </span> 22 </span>
23 </div> 23 </div>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Props--. 2 +// #region --Props--.
3 -defineProps({ 3 +/** Typescript typage */
4 - score: { 4 +defineProps<{
5 - type: Number, 5 + score: number;
6 - required: true, 6 + nbVote: number;
7 - nullable: false, 7 +}>();
8 - }, 8 +// #endregion
9 - nbVote: {  
10 - type: Number,  
11 - required: true,  
12 - nullable: false,  
13 - },  
14 -});  
15 -//#endregion  
16 9
17 -//#region --Function--. 10 +// #region --Function--.
18 /** 11 /**
19 * Format vote count if > 1000. 12 * Format vote count if > 1000.
20 * @param count 13 * @param count
21 */ 14 */
22 -const formatVoteCount = (count: number) => { 15 +function formatVoteCount(count: number) {
23 - if (count >= 1000) { 16 + if (count >= 1000) return `${(count / 1000).toFixed(1)}k votes`;
24 - return `${(count / 1000).toFixed(1)}k votes`;  
25 - }  
26 return `${count} votes`; 17 return `${count} votes`;
27 -}; 18 +}
28 -//#endregion 19 +// #endregion
29 </script> 20 </script>
30 21
31 <template> 22 <template>
@@ -34,7 +25,9 @@ const formatVoteCount = (count: number) => { @@ -34,7 +25,9 @@ const formatVoteCount = (count: number) => {
34 {{ score.toFixed(1) }} 25 {{ score.toFixed(1) }}
35 </section> 26 </section>
36 <section> 27 <section>
37 - <p class="font-semibold">Note TMDB</p> 28 + <p class="font-semibold">
  29 + Note TMDB
  30 + </p>
38 <div class="text-sm text-gray-400"> 31 <div class="text-sm text-gray-400">
39 {{ formatVoteCount(nbVote) }} 32 {{ formatVoteCount(nbVote) }}
40 </div> 33 </div>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Import--. 2 +import type { Comment } from "~/type/commentForm";
  3 +// #region --Import--.
3 import { useVuelidate } from "@vuelidate/core"; 4 import { useVuelidate } from "@vuelidate/core";
4 import { helpers, maxLength, maxValue, minLength, minValue, required } from "@vuelidate/validators"; 5 import { helpers, maxLength, maxValue, minLength, minValue, required } from "@vuelidate/validators";
5 -import type { Comment } from "~/type/commentForm"; 6 +// #endregion
6 -//#endregion  
7 -  
8 -//#region --Emit--.  
9 -const emit = defineEmits(["event:submit"]);  
10 -//#endregion  
11 7
12 -//#region --Props--. 8 +// #region --Props--.
13 -defineProps({ 9 +withDefaults(defineProps<{
14 - isSubmitting: { 10 + isSubmitting?: boolean;
15 - type: Boolean, 11 +}>(), {
16 - required: false, 12 + isSubmitting: false,
17 - nullable: false,  
18 - default: false,  
19 - },  
20 }); 13 });
21 -//#endregion 14 +// #endregion
  15 +
  16 +// #region --Emit--.
  17 +const emit = defineEmits<{
  18 + eventSubmit: [formData: any];
  19 +}>();
  20 +// #endregion
22 21
23 -//#region --Data/ref--. 22 +// #region --Data/ref--.
24 const initialState: Comment = { 23 const initialState: Comment = {
25 username: "", 24 username: "",
26 message: "", 25 message: "",
@@ -35,6 +34,7 @@ const rules = { @@ -35,6 +34,7 @@ const rules = {
35 maxLength: helpers.withMessage("Le nom d'utilisateur ne peut pas dépasser 50 caractères", maxLength(50)), 34 maxLength: helpers.withMessage("Le nom d'utilisateur ne peut pas dépasser 50 caractères", maxLength(50)),
36 alpha: helpers.withMessage( 35 alpha: helpers.withMessage(
37 "Le nom d'utilisateur ne peut contenir que des lettres", 36 "Le nom d'utilisateur ne peut contenir que des lettres",
  37 + // eslint-disable-next-line regexp/no-obscure-range
38 helpers.regex(/^[a-zA-ZÀ-ÿ\s]+$/), 38 helpers.regex(/^[a-zA-ZÀ-ÿ\s]+$/),
39 ), 39 ),
40 }, 40 },
@@ -54,15 +54,15 @@ const formData = reactive({ @@ -54,15 +54,15 @@ const formData = reactive({
54 ...initialState, 54 ...initialState,
55 }); 55 });
56 const v$ = useVuelidate(rules, formData); 56 const v$ = useVuelidate(rules, formData);
57 -//#endregion 57 +// #endregion
58 58
59 // const errormessages = computed(() => { 59 // const errormessages = computed(() => {
60 // return v$.value.message.$errors.map((e) => e.$message); 60 // return v$.value.message.$errors.map((e) => e.$message);
61 // }); 61 // });
62 62
63 -//#region --Function--. 63 +// #region --Function--.
64 async function submitComment() { 64 async function submitComment() {
65 - emit("event:submit", formData); 65 + emit("eventSubmit", formData);
66 } 66 }
67 67
68 function clear() { 68 function clear() {
@@ -80,7 +80,7 @@ function handleMessageEvent(event: string) { @@ -80,7 +80,7 @@ function handleMessageEvent(event: string) {
80 // console.log(formData.message.replace(/(&lt;([^&gt;]+)&gt;)/ig, '')); 80 // console.log(formData.message.replace(/(&lt;([^&gt;]+)&gt;)/ig, ''));
81 } 81 }
82 82
83 -//#endregion 83 +// #endregion
84 </script> 84 </script>
85 85
86 <template> 86 <template>
@@ -105,11 +105,11 @@ function handleMessageEvent(event: string) { @@ -105,11 +105,11 @@ function handleMessageEvent(event: string) {
105 @blur="v$.rating.$touch" 105 @blur="v$.rating.$touch"
106 @input="v$.rating.$touch" 106 @input="v$.rating.$touch"
107 /> 107 />
108 - <!-- <pre>{{ errormessages }}</pre>--> 108 + <!-- <pre>{{ errormessages }}</pre> -->
109 <ui-components-tiny-mce-field-editor 109 <ui-components-tiny-mce-field-editor
110 :error-message="v$?.message?.$errors[0]?.$message ? (v$.message.$errors[0].$message as string) : ''" 110 :error-message="v$?.message?.$errors[0]?.$message ? (v$.message.$errors[0].$message as string) : ''"
111 :model-value="formData.message" 111 :model-value="formData.message"
112 - @update:model-value="handleMessageEvent" 112 + @update-model-value="handleMessageEvent"
113 /> 113 />
114 <v-btn 114 <v-btn
115 class="mt-6 mr-4" 115 class="mt-6 mr-4"
@@ -117,19 +117,26 @@ function handleMessageEvent(event: string) { @@ -117,19 +117,26 @@ function handleMessageEvent(event: string) {
117 @click=" 117 @click="
118 async () => { 118 async () => {
119 const validForm = await v$.$validate(); 119 const validForm = await v$.$validate();
120 - if (validForm) { 120 + if (validForm) submitComment();
121 - submitComment();  
122 - }  
123 } 121 }
124 " 122 "
125 > 123 >
126 - <span v-if="isSubmitting" class="flex items-center justify-center"> 124 + <span
  125 + v-if="isSubmitting"
  126 + class="flex items-center justify-center"
  127 + >
127 <span class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" /> 128 <span class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
128 Envoi en cours... 129 Envoi en cours...
129 </span> 130 </span>
130 <span v-else>Publier le commentaire</span> 131 <span v-else>Publier le commentaire</span>
131 </v-btn> 132 </v-btn>
132 - <v-btn class="mt-6 mr-4" color="primary" @click="clear"> effacer </v-btn> 133 + <v-btn
  134 + class="mt-6 mr-4"
  135 + color="primary"
  136 + @click="clear"
  137 + >
  138 + effacer
  139 + </v-btn>
133 </VForm> 140 </VForm>
134 </section> 141 </section>
135 </template> 142 </template>
1 -import { describe, expect, it } from "vitest";  
2 import { mount } from "@vue/test-utils"; 1 import { mount } from "@vue/test-utils";
  2 +import { describe, expect, it } from "vitest";
3 3
4 import HelloWorld from "./HelloWorld.vue"; 4 import HelloWorld from "./HelloWorld.vue";
5 5
6 -describe("HelloWorld", () => { 6 +describe("helloWorld", () => {
7 it("component renders Hello world properly", () => { 7 it("component renders Hello world properly", () => {
8 const wrapper = mount(HelloWorld); 8 const wrapper = mount(HelloWorld);
9 expect(wrapper.text()).toContain("Hello world"); 9 expect(wrapper.text()).toContain("Hello world");
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Props--. 2 +// #region --Props--.
3 -defineProps({ 3 +defineProps<{
4 - src: { 4 + src: string;
5 - type: String, 5 + title: string;
6 - required: true, 6 +}>();
7 - nullable: false, 7 +// #endregion
8 - },  
9 - title: {  
10 - type: String,  
11 - required: true,  
12 - nullable: false,  
13 - },  
14 -});  
15 -//#endregion  
16 8
17 -//#region --Declaration--. 9 +// #region --Declaration--.
18 const w: Window = window; 10 const w: Window = window;
19 -//#endregion 11 +// #endregion
20 </script> 12 </script>
21 13
22 <template> 14 <template>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Props--. 2 +// #region --Props--.
3 -defineProps({ 3 +withDefaults(defineProps<{
4 - isLoading: { 4 + isLoading: boolean;
5 - type: Boolean, 5 + isInitialLoading?: boolean;
6 - required: true, 6 +}>(), {
7 - nullable: false, 7 + isInitialLoading: false,
8 - },  
9 - isInitialLoading: {  
10 - type: Boolean,  
11 - required: false,  
12 - nullable: false,  
13 - default: false,  
14 - },  
15 }); 8 });
16 -//#endregion 9 +// #endregion
17 </script> 10 </script>
18 11
19 <template> 12 <template>
20 - <section v-if="isLoading && !isInitialLoading" class="flex justify-center mt-8"> 13 + <section
  14 + v-if="isLoading && !isInitialLoading"
  15 + class="flex justify-center mt-8"
  16 + >
21 <div class="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" /> 17 <div class="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />
22 </section> 18 </section>
23 </template> 19 </template>
1 <script setup lang="ts"> 1 <script setup lang="ts">
2 -//#region --Props--. 2 +// #region --Props--.
3 import { FilmIcon } from "lucide-vue-next"; 3 import { FilmIcon } from "lucide-vue-next";
4 4
5 -defineProps({ 5 +defineProps<{
6 - src: { 6 + src: string;
7 - type: String, 7 + title: string;
8 - required: true, 8 +}>();
9 - nullable: false, 9 +// #endregion
10 - },  
11 - title: {  
12 - type: String,  
13 - required: true,  
14 - nullable: false,  
15 - },  
16 -});  
17 -//#endregion  
18 </script> 10 </script>
19 11
20 <template> 12 <template>
21 <section class="w-full md:w-1/3 lg:w-1/4"> 13 <section class="w-full md:w-1/3 lg:w-1/4">
22 <div class="rounded-lg overflow-hidden shadow-lg bg-gray-800"> 14 <div class="rounded-lg overflow-hidden shadow-lg bg-gray-800">
23 - <v-img v-if="src" :alt="title" :src="`https://image.tmdb.org/t/p/w500${src}`" class="w-full h-auto" /> 15 + <v-img
24 - <div v-else class="aspect-[2/3] bg-gray-700 flex items-center justify-center"> 16 + v-if="src"
25 - <FilmIcon :size="64" class="text-gray-500" /> 17 + :alt="title"
  18 + :src="`https://image.tmdb.org/t/p/w500${src}`"
  19 + class="w-full h-auto"
  20 + />
  21 + <div
  22 + v-else
  23 + class="aspect-[2/3] bg-gray-700 flex items-center justify-center"
  24 + >
  25 + <FilmIcon
  26 + :size="64"
  27 + class="text-gray-500"
  28 + />
26 </div> 29 </div>
27 </div> 30 </div>
28 </section> 31 </section>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --import--. 2 +import { useDebounceFn } from "@vueuse/core";
  3 +// #region --import--.
3 import { SearchIcon, XIcon } from "lucide-vue-next"; 4 import { SearchIcon, XIcon } from "lucide-vue-next";
4 import { ref } from "vue"; 5 import { ref } from "vue";
5 -import { useDebounceFn } from "@vueuse/core"; 6 +// #endregion
6 -//#endregion  
7 7
8 -//#region --Emits--. 8 +// #region --Props--.
9 -const emit = defineEmits(["event:search", "event:clear_search"]); 9 +withDefaults(defineProps<{
10 -//#endregion 10 + placeholder?: string;
11 - 11 +}>(), {
12 -//#region --Props--. 12 + placeholder: "",
13 -defineProps({  
14 - placeholder: {  
15 - type: String,  
16 - required: false,  
17 - nullable: false,  
18 - default: "",  
19 - },  
20 }); 13 });
21 -//#endregion 14 +// #endregion
  15 +
  16 +// #region --Emits--.
  17 +const emit = defineEmits<{
  18 + eventSearch: [search: string];
  19 + eventClearSearch: [];
  20 +}>();
  21 +// #endregion
22 22
23 -//#region --Data/refs--. 23 +// #region --Data/refs--.
24 const searchQuery = ref(""); 24 const searchQuery = ref("");
25 -//#endregion 25 +// #endregion
26 26
27 -//#region --Function--. 27 +// #region --Function--.
28 /** 28 /**
29 * Debounced function 29 * Debounced function
30 */ 30 */
31 const handleSearchEvent = useDebounceFn(() => { 31 const handleSearchEvent = useDebounceFn(() => {
32 - emit("event:search", searchQuery.value); 32 + emit("eventSearch", searchQuery.value);
33 }, 500); 33 }, 500);
34 34
35 function handleClearSearchEvent() { 35 function handleClearSearchEvent() {
36 searchQuery.value = ""; 36 searchQuery.value = "";
37 - emit("event:clear_search"); 37 + emit("eventClearSearch");
38 } 38 }
39 -//#endregion 39 +// #endregion
40 </script> 40 </script>
41 41
42 <template> 42 <template>
@@ -49,7 +49,7 @@ function handleClearSearchEvent() { @@ -49,7 +49,7 @@ function handleClearSearchEvent() {
49 class="w-full px-4 py-3 bg-gray-800 rounded-full text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary" 49 class="w-full px-4 py-3 bg-gray-800 rounded-full text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary"
50 type="text" 50 type="text"
51 @input="handleSearchEvent" 51 @input="handleSearchEvent"
52 - /> 52 + >
53 <button 53 <button
54 v-if="searchQuery" 54 v-if="searchQuery"
55 class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white" 55 class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white"
@@ -57,7 +57,10 @@ function handleClearSearchEvent() { @@ -57,7 +57,10 @@ function handleClearSearchEvent() {
57 > 57 >
58 <XIcon :size="20" /> 58 <XIcon :size="20" />
59 </button> 59 </button>
60 - <button v-else class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400"> 60 + <button
  61 + v-else
  62 + class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400"
  63 + >
61 <SearchIcon :size="20" /> 64 <SearchIcon :size="20" />
62 </button> 65 </button>
63 </div> 66 </div>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Props--. 2 +// #region --Props--.
3 -defineProps({ 3 +withDefaults(defineProps<{
4 - isInitialLoading: { 4 + isInitialLoading: boolean;
5 - type: Boolean, 5 + skeletonNumber?: number;
6 - required: true, 6 +}>(), {
7 - nullable: false, 7 + skeletonNumber: 12,
8 - },  
9 - skeletonNumber: {  
10 - type: Number,  
11 - required: false,  
12 - nullable: false,  
13 - default: 12,  
14 - },  
15 }); 8 });
16 -//#endregion 9 +// #endregion
17 </script> 10 </script>
18 11
19 <template> 12 <template>
20 <!-- Skeleton loader pendant le chargement initial --> 13 <!-- Skeleton loader pendant le chargement initial -->
21 - <section v-if="isInitialLoading" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 14 + <section
22 - <div v-for="i in skeletonNumber" :key="i" class="bg-gray-800 rounded-lg overflow-hidden shadow-lg animate-pulse"> 15 + v-if="isInitialLoading"
  16 + class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"
  17 + >
  18 + <div
  19 + v-for="i in skeletonNumber"
  20 + :key="i"
  21 + class="bg-gray-800 rounded-lg overflow-hidden shadow-lg animate-pulse"
  22 + >
23 <div class="h-80 bg-gray-700" /> 23 <div class="h-80 bg-gray-700" />
24 <div class="p-4"> 24 <div class="p-4">
25 <div class="h-6 bg-gray-700 rounded mb-3" /> 25 <div class="h-6 bg-gray-700 rounded mb-3" />
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --Import--. 2 +// #region --Import--.
3 import Editor from "@tinymce/tinymce-vue"; 3 import Editor from "@tinymce/tinymce-vue";
4 import { ref, watch } from "vue"; 4 import { ref, watch } from "vue";
5 -//#endregion 5 +// #endregion
6 6
7 -//#region --Declaration--. 7 +// #region --Props--.
8 -const runtimeConfig = useRuntimeConfig();  
9 -//#endregion  
10 -  
11 -//#region --Emit--.  
12 -const emit = defineEmits<{  
13 - (e: "update:modelValue", value: string): void;  
14 -}>();  
15 -//#endregion  
16 -  
17 -//#region --Props--.  
18 const props = defineProps<{ 8 const props = defineProps<{
19 modelValue: string; 9 modelValue: string;
20 errorMessage: string; 10 errorMessage: string;
21 }>(); 11 }>();
22 -//#endregion 12 +// #endregion
  13 +
  14 +// #region --Emit--.
  15 +const emit = defineEmits<{
  16 + (e: "updateModelValue", value: string): void;
  17 +}>();
  18 +// #endregion
  19 +
  20 +// #region --Declaration--.
  21 +const runtimeConfig = useRuntimeConfig();
  22 +// #endregion
23 23
24 -//#region --Data/ref--. 24 +// #region --Data/ref--.
25 const content = ref(props.modelValue); 25 const content = ref(props.modelValue);
26 const init = { 26 const init = {
27 height: 300, 27 height: 300,
@@ -48,10 +48,10 @@ const init = { @@ -48,10 +48,10 @@ const init = {
48 "wordcount", 48 "wordcount",
49 ], 49 ],
50 toolbar: 50 toolbar:
51 - "undo redo | blocks | bold italic underline strikethrough |" + 51 + "undo redo | blocks | bold italic underline strikethrough |"
52 - "bold italic forecolor | alignleft aligncenter " + 52 + + "bold italic forecolor | alignleft aligncenter "
53 - "alignright alignjustify | bullist numlist outdent indent | " + 53 + + "alignright alignjustify | bullist numlist outdent indent | "
54 - "removeformat | help", 54 + + "removeformat | help",
55 content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:14px }", 55 content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
56 skin: "oxide-dark", 56 skin: "oxide-dark",
57 content_css: "dark", 57 content_css: "dark",
@@ -59,29 +59,34 @@ const init = { @@ -59,29 +59,34 @@ const init = {
59 // valid_elements: [], 59 // valid_elements: [],
60 // entity_encoding : "raw", 60 // entity_encoding : "raw",
61 }; 61 };
62 -//#endregion 62 +// #endregion
63 63
64 -//#region --Watch--. 64 +// #region --Watch--.
65 watch(content, (newValue) => { 65 watch(content, (newValue) => {
66 - emit("update:modelValue", newValue); 66 + emit("updateModelValue", newValue);
67 }); 67 });
68 68
69 watch( 69 watch(
70 () => props.modelValue, 70 () => props.modelValue,
71 (newValue) => { 71 (newValue) => {
72 - if (newValue !== content.value) { 72 + if (newValue !== content.value) content.value = newValue;
73 - content.value = newValue;  
74 - }  
75 }, 73 },
76 ); 74 );
77 -//#endregion 75 +// #endregion
78 </script> 76 </script>
79 77
80 <template> 78 <template>
81 <div> 79 <div>
82 - <editor v-model="content" :api-key="runtimeConfig.public.apiTinyMceSecret" :init="init" /> 80 + <Editor
  81 + v-model="content"
  82 + :api-key="runtimeConfig.public.apiTinyMceSecret"
  83 + :init="init"
  84 + />
83 </div> 85 </div>
84 - <div v-if="errorMessage" class="text-red-500 text-sm mt-1"> 86 + <div
  87 + v-if="errorMessage"
  88 + class="text-red-500 text-sm mt-1"
  89 + >
85 {{ errorMessage }} 90 {{ errorMessage }}
86 </div> 91 </div>
87 </template> 92 </template>
@@ -3,7 +3,10 @@ @@ -3,7 +3,10 @@
3 <template> 3 <template>
4 <v-container class="bg-gray-900"> 4 <v-container class="bg-gray-900">
5 <v-row class="bg-gray-900"> 5 <v-row class="bg-gray-900">
6 - <v-col cols="12" sm="4"> 6 + <v-col
  7 + cols="12"
  8 + sm="4"
  9 + >
7 <v-skeleton-loader 10 <v-skeleton-loader
8 class="mx-auto border bg-gray-800" 11 class="mx-auto border bg-gray-800"
9 color="#1f2937" 12 color="#1f2937"
@@ -12,7 +15,10 @@ @@ -12,7 +15,10 @@
12 type="paragraph, image" 15 type="paragraph, image"
13 /> 16 />
14 </v-col> 17 </v-col>
15 - <v-col cols="12" sm="8"> 18 + <v-col
  19 + cols="12"
  20 + sm="8"
  21 + >
16 <v-skeleton-loader 22 <v-skeleton-loader
17 class="mx-auto mt-10" 23 class="mx-auto mt-10"
18 color="#1f2937" 24 color="#1f2937"
1 import type { RuntimeConfig } from "nuxt/schema"; 1 import type { RuntimeConfig } from "nuxt/schema";
2 2
3 -export const useTMDB = function () { 3 +export function useTMDB() {
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;
@@ -12,13 +12,11 @@ export const useTMDB = function () { @@ -12,13 +12,11 @@ export const useTMDB = function () {
12 const fetchPopularMovies = async (page: number) => { 12 const fetchPopularMovies = async (page: number) => {
13 try { 13 try {
14 const response = await fetch(`${apiUrl}/movie/popular?api_key=${apiKey}&language=fr-FR&page=${page}`); 14 const response = await fetch(`${apiUrl}/movie/popular?api_key=${apiKey}&language=fr-FR&page=${page}`);
15 - if (!response.ok) { 15 + if (!response.ok) throw new Error("An error occurred when fetching popular movies");
16 - console.error("An error occurred when fetching popular movies:");  
17 - } else {  
18 return await response.json(); 16 return await response.json();
19 } 17 }
20 - } catch (error) { 18 + catch (error) {
21 - console.error("Error fetching popular movies:", error); 19 + throw new Error(`Error fetching popular movies: ${error}`);
22 } 20 }
23 }; 21 };
24 22
@@ -32,13 +30,11 @@ export const useTMDB = function () { @@ -32,13 +30,11 @@ export const useTMDB = function () {
32 const response = await fetch( 30 const response = await fetch(
33 `${apiUrl}/search/movie?api_key=${apiKey}&language=fr-FR&query=${encodeURIComponent(query)}&page=${page}`, 31 `${apiUrl}/search/movie?api_key=${apiKey}&language=fr-FR&query=${encodeURIComponent(query)}&page=${page}`,
34 ); 32 );
35 - if (!response.ok) { 33 + if (!response.ok) throw new Error("An error occurred when searching movies");
36 - console.error("An error occurred when searching movies:");  
37 - } else {  
38 return await response.json(); 34 return await response.json();
39 } 35 }
40 - } catch (error) { 36 + catch (error) {
41 - console.error("Error searching movies:", error); 37 + throw new Error(`Error searching movies: ${error}`);
42 } 38 }
43 }; 39 };
44 40
@@ -49,13 +45,11 @@ export const useTMDB = function () { @@ -49,13 +45,11 @@ export const useTMDB = function () {
49 const fetchMovieDetails = async (id: number | string) => { 45 const fetchMovieDetails = async (id: number | string) => {
50 try { 46 try {
51 const response = await fetch(`${apiUrl}/movie/${id}?api_key=${apiKey}&language=fr-FR`); 47 const response = await fetch(`${apiUrl}/movie/${id}?api_key=${apiKey}&language=fr-FR`);
52 - if (!response.ok) { 48 + if (!response.ok) throw new Error("An error occurred when fetching movie details");
53 - console.error("An error occurred when fetching movie details:");  
54 - } else {  
55 return await response.json(); 49 return await response.json();
56 } 50 }
57 - } catch (error) { 51 + catch (error) {
58 - console.error("Error fetching details:", error); 52 + throw new Error(`Error fetching details: ${error}`);
59 } 53 }
60 }; 54 };
61 55
@@ -65,15 +59,13 @@ export const useTMDB = function () { @@ -65,15 +59,13 @@ export const useTMDB = function () {
65 const fetchMovieCredits = async (id: number | string) => { 59 const fetchMovieCredits = async (id: number | string) => {
66 try { 60 try {
67 const response = await fetch(`${apiUrl}/movie/${id}/credits?api_key=${apiKey}&language=fr-FR`); 61 const response = await fetch(`${apiUrl}/movie/${id}/credits?api_key=${apiKey}&language=fr-FR`);
68 - if (!response.ok) { 62 + if (!response.ok) throw new Error("An error occurred when fetching movie credits");
69 - console.error("An error occurred when fetching movie credits:");  
70 - } else {  
71 return await response.json(); 63 return await response.json();
72 } 64 }
73 - } catch (error) { 65 + catch (error) {
74 - console.error("Error fetching movie credits:", error); 66 + throw new Error(`Error fetching movie credits: ${error}`);
75 } 67 }
76 }; 68 };
77 69
78 return { fetchPopularMovies, searchMovies, fetchMovieDetails, fetchMovieCredits }; 70 return { fetchPopularMovies, searchMovies, fetchMovieDetails, fetchMovieCredits };
79 -}; 71 +}
1 -// @ts-check 1 +import antfu from "@antfu/eslint-config";
2 -import withNuxt from "./.nuxt/eslint.config.mjs";  
3 -import js from "@eslint/js";  
4 -import eslintPluginVue from "eslint-plugin-vue";  
5 -import ts from "typescript-eslint";  
6 2
7 -const TsConfigRecommended = ts.configs.recommended; 3 +export default antfu({
  4 + // `.eslintignore` is no longer supported in Flat config, use `ignores` instead.
  5 + ignores: [
  6 + "**/fixtures",
  7 + "**/.cache",
  8 + "**/.data",
  9 + "**/.gitignore",
  10 + "**/.env",
  11 + "**/.env.dist",
  12 + "**/.output",
  13 + "**/.nitro",
  14 + "**/.nuxt",
  15 + "**/assets",
  16 + "**/dist",
  17 + "**/logs",
  18 + "**/node_modules",
  19 + "**/public",
  20 + "**/server",
  21 + ],
8 22
9 -export default withNuxt( 23 + // Disable jsonc and yaml support.
10 - // Your custom configs here 24 + jsonc: false,
11 - js.configs.recommended, 25 + markdown: false,
12 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment 26 +
13 - // @ts-expect-error 27 + // personnal rules.
14 - ...TsConfigRecommended,  
15 - ...eslintPluginVue.configs["flat/recommended"],  
16 - {  
17 - files: ["*.vue", "**/*.vue"],  
18 - languageOptions: {  
19 - parserOptions: {  
20 - parser: "@typescript-eslint/parser",  
21 - },  
22 - },  
23 rules: { 28 rules: {
24 - "vue/multi-word-component-names": "off", 29 + "antfu/if-newline": 0,
  30 + "antfu/curly": 0,
25 }, 31 },
  32 +
  33 + // Enable stylistic formatting rules.
  34 + // stylistic: true,
  35 + // Or customize the stylistic rules.
  36 + stylistic: {
  37 + indent: 2, // 4, or 'tab'
  38 + semi: true,
  39 + stylistic: true,
  40 + quotes: "double", // 'single' or 'double'.
26 }, 41 },
27 - // your custom flat configs go here, for example: 42 +
28 - // { 43 + // Type of the project. 'lib' for libraries, the default is 'app'.
29 - // files: ['**/*.ts', '**/*.tsx'], 44 + type: "app",
30 - // rules: { 45 +
31 - // 'no-console': 'off' // allow console.log in TypeScript files 46 + // TypeScript and Vue are autodetected, you can also explicitly enable them:
32 - // } 47 + typescript: true,
33 - // }, 48 + vue: true,
34 - // { 49 +
35 - // ... 50 + yaml: false,
36 - // } 51 +});
37 -);  
@@ -7,10 +7,10 @@ export interface CreditInterface { @@ -7,10 +7,10 @@ export interface CreditInterface {
7 character?: string; 7 character?: string;
8 } 8 }
9 9
10 -export type CreditsResponse = { 10 +export interface CreditsResponse {
11 id: number; 11 id: number;
12 cast: CreditInterface[]; 12 cast: CreditInterface[];
13 crew: CreditInterface[]; 13 crew: CreditInterface[];
14 movie_id: unknown; 14 movie_id: unknown;
15 movie: MovieInterface; 15 movie: MovieInterface;
16 -}; 16 +}
@@ -20,7 +20,7 @@ export interface MovieInterface { @@ -20,7 +20,7 @@ export interface MovieInterface {
20 credit: CreditsResponse; 20 credit: CreditsResponse;
21 } 21 }
22 22
23 -export type Genre = { 23 +export interface Genre {
24 id: number; 24 id: number;
25 name: string; 25 name: string;
26 -}; 26 +}
  1 +import process from "node:process";
  2 +
1 // https://nuxt.com/docs/api/configuration/nuxt-config 3 // https://nuxt.com/docs/api/configuration/nuxt-config
2 export default defineNuxtConfig({ 4 export default defineNuxtConfig({
3 compatibilityDate: "2024-11-01", 5 compatibilityDate: "2024-11-01",
@@ -21,8 +23,13 @@ export default defineNuxtConfig({ @@ -21,8 +23,13 @@ export default defineNuxtConfig({
21 23
22 // css: ['~/assets/css/main.scss'], 24 // css: ['~/assets/css/main.scss'],
23 25
  26 + eslint: {
  27 + config: {
  28 + stylistic: true,
  29 + },
  30 + },
  31 +
24 modules: [ 32 modules: [
25 - "@nuxt/eslint",  
26 "@nuxt/icon", 33 "@nuxt/icon",
27 "@nuxt/image", 34 "@nuxt/image",
28 "@nuxt/test-utils/module", 35 "@nuxt/test-utils/module",
This diff could not be displayed because it is too large.
1 { 1 {
2 "name": "nuxt-app", 2 "name": "nuxt-app",
3 - "version": "0.3.1", 3 + "version": "0.4.0",
4 "private": true, 4 "private": true,
5 "type": "module", 5 "type": "module",
6 "scripts": { 6 "scripts": {
@@ -10,14 +10,11 @@ @@ -10,14 +10,11 @@
10 "preview": "nuxt preview", 10 "preview": "nuxt preview",
11 "postinstall": "nuxt prepare", 11 "postinstall": "nuxt prepare",
12 "lint:js": "eslint --ext \".ts,.vue\" . --fix", 12 "lint:js": "eslint --ext \".ts,.vue\" . --fix",
13 - "lint:prettier": "prettier --write \"{components,pages,plugins,middleware,layouts,composables,assets}/**/*.{js,jsx,ts,tsx,vue,html,css,scss,json,md}\" .", 13 + "lint": "npm run lint:js",
14 - "lint": "npm run lint:js && npm run lint:prettier", 14 + "lintfix": "eslint . --fix",
15 - "format": "prettier --write \"{components,pages,plugins,middleware,layouts,composables,assets}/**/*.{js,jsx,ts,tsx,vue,html,css,scss,json,md}\" --list-different .",  
16 - "lintfix": "npm run format && npm run lint:js",  
17 "test": "vitest" 15 "test": "vitest"
18 }, 16 },
19 "dependencies": { 17 "dependencies": {
20 - "@nuxt/eslint": "^1.3.0",  
21 "@nuxt/icon": "^1.12.0", 18 "@nuxt/icon": "^1.12.0",
22 "@nuxt/image": "^1.10.0", 19 "@nuxt/image": "^1.10.0",
23 "@nuxt/scripts": "^0.11.6", 20 "@nuxt/scripts": "^0.11.6",
@@ -30,8 +27,8 @@ @@ -30,8 +27,8 @@
30 "@vitejs/plugin-vue": "^5.2.3", 27 "@vitejs/plugin-vue": "^5.2.3",
31 "@vuelidate/core": "^2.0.3", 28 "@vuelidate/core": "^2.0.3",
32 "@vuelidate/validators": "^2.0.4", 29 "@vuelidate/validators": "^2.0.4",
33 - "@vueuse/core": "^13.1.0", 30 + "@vueuse/core": "^13.2.0",
34 - "@vueuse/nuxt": "^13.1.0", 31 + "@vueuse/nuxt": "^13.2.0",
35 "eslint": "^9.25.1", 32 "eslint": "^9.25.1",
36 "lucide-vue-next": "^0.503.0", 33 "lucide-vue-next": "^0.503.0",
37 "nuxt": "^3.16.2", 34 "nuxt": "^3.16.2",
@@ -42,16 +39,16 @@ @@ -42,16 +39,16 @@
42 "vuetify-nuxt-module": "^0.18.6" 39 "vuetify-nuxt-module": "^0.18.6"
43 }, 40 },
44 "devDependencies": { 41 "devDependencies": {
  42 + "@antfu/eslint-config": "^4.13.0",
45 "@nuxt/test-utils": "^3.17.2", 43 "@nuxt/test-utils": "^3.17.2",
46 "@nuxtjs/tailwindcss": "^6.13.2", 44 "@nuxtjs/tailwindcss": "^6.13.2",
  45 + "@typescript-eslint/parser": "^8.32.1",
47 "@vue/test-utils": "^2.4.6", 46 "@vue/test-utils": "^2.4.6",
48 - "eslint-config-prettier": "^10.1.2",  
49 - "eslint-plugin-prettier": "^5.2.6",  
50 "happy-dom": "^17.4.4", 47 "happy-dom": "^17.4.4",
51 "jsdom": "^26.1.0", 48 "jsdom": "^26.1.0",
52 "playwright-core": "^1.52.0", 49 "playwright-core": "^1.52.0",
53 - "prettier": "^3.5.3",  
54 "typescript-eslint": "^8.32.1", 50 "typescript-eslint": "^8.32.1",
55 - "vitest": "^3.1.2" 51 + "vitest": "^3.1.2",
  52 + "vue-eslint-parser": "^10.1.3"
56 } 53 }
57 } 54 }
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -//#region --import--. 2 +import type { WhereSecondaryClosure } from "pinia-orm";
  3 +import type { CreditsResponse } from "~/interfaces/credit";
  4 +import type { MovieInterface } from "~/interfaces/movie";
  5 +import type { MovieCommentInterface } from "~/interfaces/movieComment";
  6 +// #region --import--.
3 import { AlertTriangleIcon, ArrowLeftIcon } from "lucide-vue-next"; 7 import { AlertTriangleIcon, ArrowLeftIcon } from "lucide-vue-next";
4 -import { useTMDB } from "~/composables/tMDB";  
5 import { computed, onMounted, ref } from "vue"; 8 import { computed, onMounted, ref } from "vue";
6 -import { Movie } from "~/models/movie"; 9 +import { useTMDB } from "~/composables/tMDB";
7 -import type { MovieInterface } from "~/interfaces/movie";  
8 import { Credit } from "~/models/credit"; 10 import { Credit } from "~/models/credit";
9 -import type { CreditsResponse } from "~/interfaces/credit"; 11 +import { Movie } from "~/models/movie";
10 -import type { MovieCommentInterface } from "~/interfaces/movieComment";  
11 import { MovieComment } from "~/models/movieComment"; 12 import { MovieComment } from "~/models/movieComment";
12 -import type { WhereSecondaryClosure } from "pinia-orm"; 13 +// #endregion
13 -//#endregion  
14 14
15 -//#region --Declaration--. 15 +// #region --Declaration--.
16 const { fetchMovieDetails, fetchMovieCredits } = useTMDB(); 16 const { fetchMovieDetails, fetchMovieCredits } = useTMDB();
17 -//#endregion 17 +// #endregion
18 18
19 -//#region --Declaration--. 19 +// #region --Declaration--.
20 const { currentRoute } = useRouter(); 20 const { currentRoute } = useRouter();
21 -//#endregion 21 +// #endregion
22 22
23 -//#region --Data/ref--. 23 +// #region --Data/ref--.
24 const isLoading = ref(true); 24 const isLoading = ref(true);
25 const isSubmitting = ref(false); 25 const isSubmitting = ref(false);
26 -//#endregion 26 +// #endregion
27 27
28 -//#region --Computed--. 28 +// #region --Computed--.
29 const movieId = computed(() => { 29 const movieId = computed(() => {
30 - if (currentRoute.value.params.id) { 30 + if (!currentRoute.value.params.id) return null;
31 - if (typeof currentRoute.value.params.id === "string") { 31 + if (typeof currentRoute.value.params.id !== "string") return null;
32 - if (typeof Number(+currentRoute.value.params.id) === "number") { 32 +
33 - return +currentRoute.value.params.id as number; 33 + if (typeof Number(+currentRoute.value.params.id) === "number") return +currentRoute.value.params.id as number;
34 - } else {  
35 return currentRoute.value.params.id as string; 34 return currentRoute.value.params.id as string;
36 - }  
37 - } else {  
38 - return null;  
39 - }  
40 - } else {  
41 - return null;  
42 - }  
43 }); 35 });
44 36
45 const movie = computed(() => { 37 const movie = computed(() => {
46 - if (unref(movieId)) { 38 + if (!unref(movieId)) return null;
47 return useRepo(Movie) 39 return useRepo(Movie)
48 .query() 40 .query()
49 .where("id", movieId.value as WhereSecondaryClosure<never> | null | undefined) 41 .where("id", movieId.value as WhereSecondaryClosure<never> | null | undefined)
50 .withAll() 42 .withAll()
51 .first() as unknown as MovieInterface; 43 .first() as unknown as MovieInterface;
52 - } else {  
53 - return null;  
54 - }  
55 }); 44 });
56 45
57 /** 46 /**
58 * Computed property for director 47 * Computed property for director
59 */ 48 */
60 const director = computed(() => { 49 const director = computed(() => {
61 - if (unref(movie)?.credit?.crew) { 50 + if (!unref(movie)?.credit?.crew) return null;
62 - return movie.value?.credit.crew.find((person) => person.job === "Director"); 51 + return movie.value?.credit.crew.find(person => person.job === "Director");
63 - } else {  
64 - return null;  
65 - }  
66 }); 52 });
67 53
68 /** 54 /**
@@ -78,31 +64,33 @@ const comments = computed(() => { @@ -78,31 +64,33 @@ const comments = computed(() => {
78 .orderBy("createdAt", "desc") 64 .orderBy("createdAt", "desc")
79 .get(); 65 .get();
80 }); 66 });
81 -//#endregion 67 +// #endregion
82 68
83 -//#region --Function--. 69 +// #region --Function--.
84 /** 70 /**
85 * Fetch movie details 71 * Fetch movie details
86 */ 72 */
87 -const fetchDetails = async (id: number | string) => { 73 +async function fetchDetails(id: number | string) {
88 try { 74 try {
89 isLoading.value = true; 75 isLoading.value = true;
90 76
91 const data = await fetchMovieDetails(id); 77 const data = await fetchMovieDetails(id);
92 // Add to store collection. 78 // Add to store collection.
93 useRepo(Movie).save(data); 79 useRepo(Movie).save(data);
94 - } catch (error) { 80 + }
95 - console.error("Error fetching movie details:", error); 81 + catch (error) {
96 - } finally { 82 + throw new Error(`Error fetching movie details: ${error}`);
  83 + }
  84 + finally {
97 isLoading.value = false; 85 isLoading.value = false;
98 } 86 }
99 -}; 87 +}
100 88
101 /** 89 /**
102 * Format runtime 90 * Format runtime
103 * @param minutes 91 * @param minutes
104 */ 92 */
105 -const formatRuntime = (minutes: number) => { 93 +function formatRuntime(minutes: number) {
106 if (!minutes) return "Durée inconnue"; 94 if (!minutes) return "Durée inconnue";
107 // Find nb hours. 95 // Find nb hours.
108 const hours = Math.floor(minutes / 60); 96 const hours = Math.floor(minutes / 60);
@@ -110,7 +98,7 @@ const formatRuntime = (minutes: number) => { @@ -110,7 +98,7 @@ const formatRuntime = (minutes: number) => {
110 const mins = minutes % 60; 98 const mins = minutes % 60;
111 99
112 return `${hours}h ${mins}min`; 100 return `${hours}h ${mins}min`;
113 -}; 101 +}
114 102
115 async function fetchCredits(id: number | string) { 103 async function fetchCredits(id: number | string) {
116 try { 104 try {
@@ -118,12 +106,13 @@ async function fetchCredits(id: number | string) { @@ -118,12 +106,13 @@ async function fetchCredits(id: number | string) {
118 data.movie_id = id; 106 data.movie_id = id;
119 // Add to store collection. 107 // Add to store collection.
120 useRepo(Credit).save(data); 108 useRepo(Credit).save(data);
121 - } catch (error) { 109 + }
122 - console.error("Error fetching movie credits:", error); 110 + catch (error) {
  111 + throw new Error(`Error fetching movie credits: ${error}`);
123 } 112 }
124 } 113 }
125 114
126 -//Ce n'est pas le film du siècle cependant, il est suffisamment intéressant pour passer une bonne soirée ! 115 +// Ce n'est pas le film du siècle cependant, il est suffisamment intéressant pour passer une bonne soirée !
127 function handleSubmitEvent(event: MovieCommentInterface) { 116 function handleSubmitEvent(event: MovieCommentInterface) {
128 isSubmitting.value = true; 117 isSubmitting.value = true;
129 event.movie_id = unref(movieId); 118 event.movie_id = unref(movieId);
@@ -132,9 +121,9 @@ function handleSubmitEvent(event: MovieCommentInterface) { @@ -132,9 +121,9 @@ function handleSubmitEvent(event: MovieCommentInterface) {
132 isSubmitting.value = false; 121 isSubmitting.value = false;
133 } 122 }
134 123
135 -//#endregion 124 +// #endregion
136 125
137 -//#region --Global event--. 126 +// #region --Global event--.
138 onMounted(() => { 127 onMounted(() => {
139 // Fetch data on component mount. 128 // Fetch data on component mount.
140 if (unref(movieId)) { 129 if (unref(movieId)) {
@@ -144,7 +133,7 @@ onMounted(() => { @@ -144,7 +133,7 @@ onMounted(() => {
144 } 133 }
145 // loadComments() 134 // loadComments()
146 }); 135 });
147 -//#endregion 136 +// #endregion
148 </script> 137 </script>
149 138
150 <template> 139 <template>
@@ -153,9 +142,16 @@ onMounted(() => { @@ -153,9 +142,16 @@ onMounted(() => {
153 <ui-components-skeleton-movie-detail-loader v-if="isLoading" /> 142 <ui-components-skeleton-movie-detail-loader v-if="isLoading" />
154 143
155 <!-- Contenu du film --> 144 <!-- Contenu du film -->
156 - <div v-else-if="movie" class="relative"> 145 + <div
  146 + v-else-if="movie"
  147 + class="relative"
  148 + >
157 <!-- Backdrop image --> 149 <!-- Backdrop image -->
158 - <ui-components-backdrop-image v-if="movie.backdrop_path" :src="movie.backdrop_path" :title="movie.title" /> 150 + <ui-components-backdrop-image
  151 + v-if="movie.backdrop_path"
  152 + :src="movie.backdrop_path"
  153 + :title="movie.title"
  154 + />
159 155
160 <!-- Contenu principal --> 156 <!-- Contenu principal -->
161 <div class="container mx-auto px-4 py-8 relative z-10 pt-20"> 157 <div class="container mx-auto px-4 py-8 relative z-10 pt-20">
@@ -163,41 +159,64 @@ onMounted(() => { @@ -163,41 +159,64 @@ onMounted(() => {
163 class="flex items-center text-gray-400 hover:text-white mb-8 transition-colors" 159 class="flex items-center text-gray-400 hover:text-white mb-8 transition-colors"
164 @click="navigateTo('/')" 160 @click="navigateTo('/')"
165 > 161 >
166 - <ArrowLeftIcon :size="20" class="mr-2" /> 162 + <ArrowLeftIcon
  163 + :size="20"
  164 + class="mr-2"
  165 + />
167 Retour 166 Retour
168 </button> 167 </button>
169 168
170 <div class="flex flex-col md:flex-row gap-8"> 169 <div class="flex flex-col md:flex-row gap-8">
171 <!-- Poster --> 170 <!-- Poster -->
172 - <ui-components-poster v-if="movie.poster_path" :src="movie.poster_path" :title="movie.title" /> 171 + <ui-components-poster
  172 + v-if="movie.poster_path"
  173 + :src="movie.poster_path"
  174 + :title="movie.title"
  175 + />
173 176
174 <!-- Informations du film --> 177 <!-- Informations du film -->
175 <section class="w-full md:w-2/3 lg:w-3/4"> 178 <section class="w-full md:w-2/3 lg:w-3/4">
176 <h1 class="text-3xl md:text-4xl font-bold mb-2"> 179 <h1 class="text-3xl md:text-4xl font-bold mb-2">
177 {{ movie.title }} 180 {{ movie.title }}
178 </h1> 181 </h1>
179 - <p v-if="movie.release_date" class="text-gray-400 mb-4"> 182 + <p
  183 + v-if="movie.release_date"
  184 + class="text-gray-400 mb-4"
  185 + >
180 {{ useDateFormat(movie.release_date, "DD-MM-YYYY") }} • {{ formatRuntime(movie.runtime) }} 186 {{ useDateFormat(movie.release_date, "DD-MM-YYYY") }} • {{ formatRuntime(movie.runtime) }}
181 </p> 187 </p>
182 188
183 <!-- Note et votes --> 189 <!-- Note et votes -->
184 - <details-score-and-vote :nb-vote="movie.vote_count" :score="movie.vote_average" /> 190 + <details-score-and-vote
  191 + :nb-vote="movie.vote_count"
  192 + :score="movie.vote_average"
  193 + />
185 194
186 <!-- Genres --> 195 <!-- Genres -->
187 <details-movie-gender :genres="movie.genres" /> 196 <details-movie-gender :genres="movie.genres" />
188 197
189 <!-- Synopsis --> 198 <!-- Synopsis -->
190 <div class="mb-6"> 199 <div class="mb-6">
191 - <h2 class="text-xl font-bold mb-2">Synopsis</h2> 200 + <h2 class="text-xl font-bold mb-2">
  201 + Synopsis
  202 + </h2>
192 <p class="text-gray-300"> 203 <p class="text-gray-300">
193 {{ movie.overview || "Aucun synopsis disponible." }} 204 {{ movie.overview || "Aucun synopsis disponible." }}
194 </p> 205 </p>
195 </div> 206 </div>
196 207
197 <!-- Réalisateur et têtes d'affiche --> 208 <!-- Réalisateur et têtes d'affiche -->
198 - <div v-if="movie.credit" class="mb-6"> 209 + <div
199 - <h2 class="text-xl font-bold mb-2">Équipe</h2> 210 + v-if="movie.credit"
200 - <div v-if="director" class="mb-2"> 211 + class="mb-6"
  212 + >
  213 + <h2 class="text-xl font-bold mb-2">
  214 + Équipe
  215 + </h2>
  216 + <div
  217 + v-if="director"
  218 + class="mb-2"
  219 + >
201 <span class="font-semibold">Réalisateur:</span> {{ director.name }} 220 <span class="font-semibold">Réalisateur:</span> {{ director.name }}
202 </div> 221 </div>
203 <div v-if="movie.credit.cast.length > 0"> 222 <div v-if="movie.credit.cast.length > 0">
@@ -212,8 +231,10 @@ onMounted(() => { @@ -212,8 +231,10 @@ onMounted(() => {
212 </div> 231 </div>
213 </div> 232 </div>
214 <!-- Comments form. --> 233 <!-- Comments form. -->
215 - <h3 class="text-xl font-bold mt-8 mb-4">Ajouter un commentaire</h3> 234 + <h3 class="text-xl font-bold mt-8 mb-4">
216 - <form-movie-comment-form @event:submit="handleSubmitEvent" /> 235 + Ajouter un commentaire
  236 + </h3>
  237 + <form-movie-comment-form @event-submit="handleSubmitEvent" />
217 238
218 <!-- Liste des commentaires --> 239 <!-- Liste des commentaires -->
219 <movie-comment-list :comments="comments as unknown as MovieCommentInterface[]" /> 240 <movie-comment-list :comments="comments as unknown as MovieCommentInterface[]" />
@@ -223,10 +244,20 @@ onMounted(() => { @@ -223,10 +244,20 @@ onMounted(() => {
223 </div> 244 </div>
224 245
225 <!-- Erreur --> 246 <!-- Erreur -->
226 - <section v-else class="container mx-auto px-4 py-16 text-center"> 247 + <section
227 - <AlertTriangleIcon :size="64" class="mx-auto mb-4 text-red-500" /> 248 + v-else
228 - <h2 class="text-2xl font-bold mb-2">Film non trouvé</h2> 249 + class="container mx-auto px-4 py-16 text-center"
229 - <p class="text-gray-400 mb-6">Nous n'avons pas pu trouver le film que vous cherchez.</p> 250 + >
  251 + <AlertTriangleIcon
  252 + :size="64"
  253 + class="mx-auto mb-4 text-red-500"
  254 + />
  255 + <h2 class="text-2xl font-bold mb-2">
  256 + Film non trouvé
  257 + </h2>
  258 + <p class="text-gray-400 mb-6">
  259 + Nous n'avons pas pu trouver le film que vous cherchez.
  260 + </p>
230 <button 261 <button
231 class="px-6 py-2 bg-primary text-white font-bold rounded-md hover:bg-primary-dark transition-colors" 262 class="px-6 py-2 bg-primary text-white font-bold rounded-md hover:bg-primary-dark transition-colors"
232 @click="navigateTo('/')" 263 @click="navigateTo('/')"
1 -//#region --Import--. 1 +import type { Genre } from "~/interfaces/movie";
2 -import { describe, expect, it } from "vitest";  
3 import { mount } from "@vue/test-utils"; 2 import { mount } from "@vue/test-utils";
  3 +// #region --Import--.
  4 +import { describe, expect, it } from "vitest";
4 import MovieGender from "../../components/details/MovieGender.vue"; 5 import MovieGender from "../../components/details/MovieGender.vue";
5 -import type { Genre } from "~/interfaces/movie"; 6 +// #endregion
6 -//#endregion  
7 7
8 -describe("MovieGender", () => { 8 +describe("movieGender", () => {
9 it("affiche correctement les genres", () => { 9 it("affiche correctement les genres", () => {
10 // Données de test. 10 // Données de test.
11 const genres: Genre[] = [ 11 const genres: Genre[] = [
1 -//#region --Import--.  
2 -import { describe, expect, it } from "vitest";  
3 import { mount } from "@vue/test-utils"; 1 import { mount } from "@vue/test-utils";
  2 +// #region --Import--.
  3 +import { describe, expect, it } from "vitest";
4 import ScoreAndVote from "../../components/details/ScoreAndVote.vue"; 4 import ScoreAndVote from "../../components/details/ScoreAndVote.vue";
5 -//#endregion 5 +// #endregion
6 6
7 -describe("ScoreAndVote", () => { 7 +describe("scoreAndVote", () => {
8 it("affiche correctement le score", () => { 8 it("affiche correctement le score", () => {
9 // Monter le composant avec ses props. 9 // Monter le composant avec ses props.
10 const wrapper = mount(ScoreAndVote, { 10 const wrapper = mount(ScoreAndVote, {
1 -export type Comment = { 1 +export interface Comment {
2 username: string; 2 username: string;
3 message: string; 3 message: string;
4 rating: number; 4 rating: number;
5 -}; 5 +}
1 -import { defineVitestConfig } from "@nuxt/test-utils/config";  
2 import { fileURLToPath } from "node:url"; 1 import { fileURLToPath } from "node:url";
  2 +import { defineVitestConfig } from "@nuxt/test-utils/config";
3 3
4 export default defineVitestConfig({ 4 export default defineVitestConfig({
5 /** 5 /**