Showing
30 changed files
with
542 additions
and
469 deletions
| 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 | }; | 
.prettierignore
deleted
100644 → 0
.prettierrc
deleted
100644 → 0
| 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(/(<([^>]+)>)/ig, '')); | 80 | // console.log(formData.message.replace(/(<([^>]+)>)/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 | -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 | /** | 
- 
Please register or login to post a comment