Bruno Predot

Merge branch 'release/0.2.0'

  1 +0.2.0:
  2 +- Mise en place du CHANGELOG_RELEASE.
  3 +- Ajout page index.
  4 +- Modification app.vue afin d'initialiser l'app avec vuetify et NuxtPage pour démarrer sur la page index.
  5 +- Création du composant MoviesList.
  6 +- Création du composant SearchBar.
  7 +- Création du composant SkeletonMoviesLoader.
  8 +- Installation et paramétrage de pinia-orm.
  9 +- Ajout du model Movie.
  10 +- Création de la page movies/[id] vierge pour le détail d'un film.
  11 +
1 0.1.2: 12 0.1.2:
2 - Ajout fichier .prettierignore. 13 - Ajout fichier .prettierignore.
3 - Installation module vuetify + modif script lint dans package.json. 14 - Installation module vuetify + modif script lint dans package.json.
  1 +------ Dispo à la prochaine release ------------
  1 +<script lang="ts" setup></script>
  2 +
1 <template> 3 <template>
2 - <div> 4 + <v-locale-provider>
3 - <NuxtRouteAnnouncer /> 5 + <v-app>
4 - <NuxtWelcome /> 6 + <v-main class="min-h-screen bg-gray-900 text-white">
5 - </div> 7 + <NuxtPage />
  8 + </v-main>
  9 + </v-app>
  10 + </v-locale-provider>
6 </template> 11 </template>
  12 +
  13 +<style scoped></style>
  1 +<script lang="ts" setup>
  2 +//#region --import--.
  3 +import SearchBar from "~/components/SearchBar.vue";
  4 +import { onBeforeUnmount, ref } from "vue";
  5 +import { useTMDB } from "~/composables/tMDB";
  6 +import { Movie } from "~/models/movie";
  7 +import { FilmIcon, SearchXIcon } from "lucide-vue-next";
  8 +import type { MovieInterface } from "~/interfaces/movie";
  9 +import { useDateFormat } from "@vueuse/core";
  10 +//#endregion
  11 +
  12 +//#region --Declaration--.
  13 +const { fetchPopularMovies, searchMovies } = useTMDB();
  14 +//#endregion
  15 +
  16 +//#region --Data/refs--.
  17 +const isInitialLoading = ref(true);
  18 +const isLoadingMore = ref(false);
  19 +const currentPage = ref(1);
  20 +const totalPages = ref(0);
  21 +const searchQuery = ref("");
  22 +/** Elément observé pour le défilement infini. */
  23 +const loadMoreTrigger = ref<HTMLElement | null>(null);
  24 +/** Instance de IntersectionObserver */
  25 +const observer = ref<IntersectionObserver | null>(null);
  26 +//#endregion
  27 +
  28 +//#region --Computed--.
  29 +const movies = computed(() => {
  30 + return useRepo(Movie).query().orderBy("popularity", "desc").get() as unknown as MovieInterface[];
  31 +});
  32 +//#endregion
  33 +
  34 +//#region --Function--.
  35 +/**
  36 + * Fetch popular movies
  37 + * @param page
  38 + */
  39 +const fetchMovies = async (page: number) => {
  40 + try {
  41 + isLoadingMore.value = true;
  42 + const data = await fetchPopularMovies(page);
  43 + // Save in Movie model.
  44 + if (isInitialLoading.value) {
  45 + // First fetch, erase old data before save.
  46 + useRepo(Movie).fresh(data.results);
  47 + } else {
  48 + // Add to store collection.
  49 + useRepo(Movie).save(data.results);
  50 + }
  51 + totalPages.value = data.total_pages;
  52 + currentPage.value = page;
  53 + } catch (error) {
  54 + console.error("Error fetching popular movies:", error);
  55 + } finally {
  56 + isInitialLoading.value = false;
  57 + isLoadingMore.value = false;
  58 + }
  59 +};
  60 +
  61 +/**
  62 + * Search movies
  63 + * @param query
  64 + * @param page
  65 + */
  66 +const search = async (query: string, page: number) => {
  67 + // If empty search, fetch popular movies.
  68 + if (!query.trim()) {
  69 + await fetchMovies(1);
  70 + return;
  71 + }
  72 + try {
  73 + isLoadingMore.value = true;
  74 + if (page === 1) {
  75 + isInitialLoading.value = true;
  76 + }
  77 + const data = await searchMovies(query, page);
  78 + // Save in Movie model.
  79 + if (isInitialLoading.value) {
  80 + // First fetch, erase old data before save.
  81 + useRepo(Movie).fresh(data.results);
  82 + } else {
  83 + // Add to store collection.
  84 + useRepo(Movie).save(data.results);
  85 + }
  86 + totalPages.value = data.total_pages;
  87 + currentPage.value = page;
  88 + } catch (error) {
  89 + console.error("Error searching movies:", error);
  90 + } finally {
  91 + isInitialLoading.value = false;
  92 + isLoadingMore.value = false;
  93 + }
  94 +};
  95 +
  96 +function createIntersectionObserver() {
  97 + return new IntersectionObserver(
  98 + (entries) => {
  99 + const [entry] = entries;
  100 + if (entry.isIntersecting && !isLoadingMore.value && currentPage.value < totalPages.value) {
  101 + if (searchQuery.value) {
  102 + // Continue searching query if already active.
  103 + search(searchQuery.value, currentPage.value + 1)
  104 + } else {
  105 + // Continue fetching popular movies.
  106 + fetchMovies(currentPage.value + 1);
  107 + }
  108 + }
  109 + },
  110 + { threshold: 1.0 },
  111 + );
  112 +}
  113 +
  114 +function handleSearchEvent(event: string) {
  115 + currentPage.value = 1;
  116 + searchQuery.value = event;
  117 + search(event, 1);
  118 +}
  119 +
  120 +function handleClearSearchEvent() {
  121 + searchQuery.value = '';
  122 + currentPage.value = 1;
  123 + // Fetch popular movies after clear.
  124 + fetchMovies(1);
  125 +}
  126 +
  127 +//#endregion
  128 +
  129 +//#region --Global event--.
  130 +onMounted(() => {
  131 + // First loading.
  132 + fetchMovies(1);
  133 + // Création et stockage dans la ref de l'instance IntersectionObserver.
  134 + observer.value = createIntersectionObserver();
  135 + if (loadMoreTrigger.value) {
  136 + // Début d'observation de la div pour le défilement infini.
  137 + observer.value.observe(loadMoreTrigger.value);
  138 + }
  139 +
  140 + if (loadMoreTrigger.value) {
  141 + observer.value.observe(loadMoreTrigger.value);
  142 + }
  143 +});
  144 +
  145 +onBeforeUnmount(() => {
  146 + // Disconnect the observer when the component is unmounted.
  147 + if (observer.value) {
  148 + observer.value.disconnect();
  149 + }
  150 +});
  151 +//#endregion
  152 +</script>
  153 +
  154 +<template>
  155 + <section>
  156 + <h1 class="text-4xl font-bold mb-8 text-center">Découvrez les films populaires</h1>
  157 + <!-- Barre de recherche -->
  158 + <search-bar
  159 + placeholder="Rechercher un film..."
  160 + @event:search="handleSearchEvent"
  161 + @event:clear_search="handleClearSearchEvent"
  162 + />
  163 + <!-- Loading Skeleton -->
  164 + <skeleton-movies-loader v-if="isInitialLoading" :is-initial-loading="isInitialLoading" :skeleton-number="20" />
  165 + <!-- Liste des films -->
  166 + <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">
  167 + <div
  168 + v-for="movie in movies"
  169 + :key="movie.id"
  170 + class="bg-gray-800 rounded-lg overflow-hidden shadow-lg transition-transform duration-300 hover:scale-105 cursor-pointer"
  171 + @click="navigateTo(`/movies/${movie.id}`)"
  172 + >
  173 + <div class="relative pb-[150%]">
  174 + <img
  175 + v-if="movie.poster_path"
  176 + :alt="movie.title"
  177 + :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
  178 + class="absolute inset-0 w-full h-full object-cover"
  179 + />
  180 + <div v-else class="absolute inset-0 w-full h-full bg-gray-700 flex items-center justify-center">
  181 + <FilmIcon :size="48" class="text-gray-500" />
  182 + </div>
  183 + <div
  184 + class="absolute top-2 right-2 bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold"
  185 + >
  186 + {{ movie.vote_average.toFixed(1) }}
  187 + </div>
  188 + </div>
  189 + <div class="p-4">
  190 + <h2 class="text-lg font-bold mb-1 line-clamp-1">{{ movie.title }}</h2>
  191 + <p class="text-sm text-gray-400">{{ useDateFormat(movie.release_date, "DD-MM-YYYY") }}</p>
  192 + </div>
  193 + </div>
  194 + </div>
  195 +
  196 + <!-- Message si aucun film trouvé -->
  197 + <section v-else-if="searchQuery && !movies.length" class="text-center py-12">
  198 + <SearchXIcon :size="64" class="mx-auto mb-4 text-gray-600" />
  199 + <h3 class="text-xl font-bold mb-2">Aucun film trouvé</h3>
  200 + <p class="text-gray-400">Essayez avec un autre terme de recherche</p>
  201 + </section>
  202 +
  203 + <!-- Loader pour le chargement de plus de films -->
  204 + <section v-if="isLoadingMore && !isInitialLoading" class="flex justify-center mt-8">
  205 + <div class="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />
  206 + </section>
  207 +
  208 + <!-- Élément observé pour le défilement infini -->
  209 + <div ref="loadMoreTrigger" class="h-10 mt-4" />
  210 + </section>
  211 +</template>
  212 +
  213 +<style scoped></style>
  1 +<script lang="ts" setup>
  2 +//#region --import--.
  3 +import { SearchIcon, XIcon } from "lucide-vue-next";
  4 +import { ref } from "vue";
  5 +import { useDebounceFn } from "@vueuse/core";
  6 +//#endregion
  7 +
  8 +//#region --Emits--.
  9 +const emit = defineEmits(['event:search', 'event:clear_search']);
  10 +//#endregion
  11 +
  12 +//#region --Props--.
  13 +defineProps({
  14 + placeholder: {
  15 + type: String,
  16 + required: false,
  17 + nullable: false,
  18 + default: "",
  19 + },
  20 +});
  21 +//#endregion
  22 +
  23 +//#region --Data/refs--.
  24 +const searchQuery = ref("");
  25 +//#endregion
  26 +
  27 +//#region --Function--.
  28 +/**
  29 + * Debounced function
  30 + */
  31 +const handleSearchEvent = useDebounceFn(() => {
  32 + emit('event:search', searchQuery.value);
  33 +}, 500);
  34 +
  35 +function handleClearSearchEvent() {
  36 + searchQuery.value = '';
  37 + emit('event:clear_search')
  38 +}
  39 +//#endregion
  40 +</script>
  41 +
  42 +<template>
  43 + <!-- Barre de recherche -->
  44 + <section class="mb-8">
  45 + <div class="relative max-w-xl mx-auto">
  46 + <input
  47 + v-model="searchQuery"
  48 + :placeholder="placeholder"
  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"
  51 + @input="handleSearchEvent"
  52 + />
  53 + <button
  54 + v-if="searchQuery"
  55 + class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white"
  56 + @click="handleClearSearchEvent"
  57 + >
  58 + <XIcon :size="20" />
  59 + </button>
  60 + <button v-else class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400">
  61 + <SearchIcon :size="20" />
  62 + </button>
  63 + </div>
  64 + </section>
  65 +</template>
  66 +
  67 +<style scoped></style>
  1 +<script lang="ts" setup>
  2 +//#region --Props--.
  3 +defineProps({
  4 + isInitialLoading: {
  5 + type: Boolean,
  6 + required: true,
  7 + nullable: false,
  8 + },
  9 + skeletonNumber: {
  10 + type: Number,
  11 + required: false,
  12 + nullable: false,
  13 + default: 12,
  14 + },
  15 +});
  16 +//#endregion
  17 +</script>
  18 +
  19 +<template>
  20 + <!-- 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">
  22 + <div v-for="i in skeletonNumber" :key="i" class="bg-gray-800 rounded-lg overflow-hidden shadow-lg animate-pulse">
  23 + <div class="h-80 bg-gray-700" />
  24 + <div class="p-4">
  25 + <div class="h-6 bg-gray-700 rounded mb-3" />
  26 + <div class="h-4 bg-gray-700 rounded w-2/3" />
  27 + </div>
  28 + </div>
  29 + </section>
  30 +</template>
  31 +
  32 +<style scoped></style>
@@ -2,9 +2,47 @@ import type { RuntimeConfig } from "nuxt/schema"; @@ -2,9 +2,47 @@ import type { RuntimeConfig } from "nuxt/schema";
2 2
3 export const useTMDB = function() { 3 export const useTMDB = function() {
4 const runtimeconfig: RuntimeConfig = useRuntimeConfig(); 4 const runtimeconfig: RuntimeConfig = useRuntimeConfig();
5 -  
6 const apiUrl = runtimeconfig.public.apiTMDBUrl; 5 const apiUrl = runtimeconfig.public.apiTMDBUrl;
7 const apiKey = runtimeconfig.public.apiTMDBSecret; 6 const apiKey = runtimeconfig.public.apiTMDBSecret;
8 7
9 - return {apiUrl, apiKey} 8 + /**
  9 + * Fetch popular movies.
  10 + * @param page
  11 + */
  12 + const fetchPopularMovies = async (page: number) => {
  13 + try {
  14 + const response = await fetch(
  15 + `${apiUrl}/movie/popular?api_key=${apiKey}&language=fr-FR&page=${page}`,
  16 + );
  17 + if (!response.ok) {
  18 + console.error("An error occurred when fetching popular movies:");
  19 + } else {
  20 + return await response.json();
  21 + }
  22 + } catch (error) {
  23 + console.error("Error fetching popular movies:", error);
  24 + }
  25 + };
  26 +
  27 + /**
  28 + * Search movies
  29 + * @param query
  30 + * @param page
  31 + */
  32 + const searchMovies = async (query: string, page: number) => {
  33 + try {
  34 + const response = await fetch(
  35 + `${apiUrl}/search/movie?api_key=${apiKey}&language=fr-FR&query=${encodeURIComponent(query)}&page=${page}`,
  36 + );
  37 + if (!response.ok) {
  38 + console.error("An error occurred when searching movies:");
  39 + } else {
  40 + return await response.json();
  41 + }
  42 + } catch (error) {
  43 + console.error("Error searching movies:", error);
  44 + }
  45 + };
  46 +
  47 + return { fetchPopularMovies, searchMovies }
10 } 48 }
  1 +export interface MovieInterface {
  2 + id: number;
  3 + title: string;
  4 + poster_path: string | null;
  5 + vote_average: number;
  6 + release_date: string;
  7 +}
  1 +import { Model } from "pinia-orm";
  2 +
  3 +export class Movie extends Model {
  4 + /**
  5 + *
  6 + * @return {string}
  7 + */
  8 + static get entity() {
  9 + return "Movie";
  10 + }
  11 +
  12 + /**
  13 + *
  14 + * @return {string}
  15 + */
  16 + static get primaryKey() {
  17 + return "id";
  18 + }
  19 +
  20 + static fields() {
  21 + return {
  22 + // Attributs.
  23 + id: this.number(null),
  24 + adult: this.boolean(false),
  25 + backdrop_pat: this.string(null),
  26 + genre_ids: this.attr([]),
  27 + original_language: this.string(null),
  28 + original_title: this.string(null),
  29 + overview: this.string(null),
  30 + popularity: this.number(null),
  31 + poster_path: this.string(null),
  32 + release_date: this.string(null),
  33 + title: this.string(null),
  34 + video: this.boolean(false),
  35 + vote_average: this.number(null),
  36 + vote_count: this.number(null),
  37 + // Relations.
  38 + };
  39 + }
  40 +
  41 + static piniaOptions = {
  42 + persist: true,
  43 + };
  44 +
  45 +}
@@ -35,6 +35,16 @@ export default defineNuxtConfig({ @@ -35,6 +35,16 @@ export default defineNuxtConfig({
35 ], 35 ],
36 }, 36 },
37 ], 37 ],
  38 + [
  39 + "@pinia-orm/nuxt",
  40 + {
  41 + autoImports: [
  42 + // automatically imports `useRepo`.
  43 + "useRepo", // import { useRepo } from 'pinia-orm'.
  44 + ["useRepo", "usePinaRepo"], // import { useRepo as usePinaRepo } from 'pinia-orm'.
  45 + ],
  46 + },
  47 + ],
38 "pinia-plugin-persistedstate/nuxt", 48 "pinia-plugin-persistedstate/nuxt",
39 "@nuxt/scripts", 49 "@nuxt/scripts",
40 "@nuxt/test-utils", 50 "@nuxt/test-utils",
1 { 1 {
2 "name": "nuxt-app", 2 "name": "nuxt-app",
3 - "version": "0.1.0", 3 + "version": "0.2.0",
4 "lockfileVersion": 3, 4 "lockfileVersion": 3,
5 "requires": true, 5 "requires": true,
6 "packages": { 6 "packages": {
7 "": { 7 "": {
8 "name": "nuxt-app", 8 "name": "nuxt-app",
9 - "version": "0.1.0", 9 + "version": "0.2.0",
10 "hasInstallScript": true, 10 "hasInstallScript": true,
11 "dependencies": { 11 "dependencies": {
12 "@nuxt/eslint": "^1.3.0", 12 "@nuxt/eslint": "^1.3.0",
@@ -15,14 +15,15 @@ @@ -15,14 +15,15 @@
15 "@nuxt/scripts": "^0.11.6", 15 "@nuxt/scripts": "^0.11.6",
16 "@nuxt/test-utils": "^3.17.2", 16 "@nuxt/test-utils": "^3.17.2",
17 "@nuxt/ui": "^2.22.0", 17 "@nuxt/ui": "^2.22.0",
18 - "@pinia/nuxt": "^0.11.0", 18 + "@pinia-orm/nuxt": "^1.10.2",
  19 + "@pinia/nuxt": "^0.9.0",
19 "@unhead/vue": "^2.0.8", 20 "@unhead/vue": "^2.0.8",
20 "@vueuse/core": "^13.1.0", 21 "@vueuse/core": "^13.1.0",
21 "@vueuse/nuxt": "^13.1.0", 22 "@vueuse/nuxt": "^13.1.0",
22 "eslint": "^9.25.1", 23 "eslint": "^9.25.1",
23 "lucide-vue-next": "^0.503.0", 24 "lucide-vue-next": "^0.503.0",
24 "nuxt": "^3.16.2", 25 "nuxt": "^3.16.2",
25 - "pinia": "^3.0.2", 26 + "pinia": "^2.3.1",
26 "pinia-plugin-persistedstate": "^4.2.0", 27 "pinia-plugin-persistedstate": "^4.2.0",
27 "vue": "^3.5.13", 28 "vue": "^3.5.13",
28 "vue-router": "^4.5.0", 29 "vue-router": "^4.5.0",
@@ -3820,10 +3821,35 @@ @@ -3820,10 +3821,35 @@
3820 "url": "https://opencollective.com/parcel" 3821 "url": "https://opencollective.com/parcel"
3821 } 3822 }
3822 }, 3823 },
  3824 + "node_modules/@pinia-orm/normalizr": {
  3825 + "version": "1.10.2",
  3826 + "resolved": "https://registry.npmjs.org/@pinia-orm/normalizr/-/normalizr-1.10.2.tgz",
  3827 + "integrity": "sha512-lgcCb7ST/leYXJwUT/y7RvTn+5U6OOmvSUuNGs/Mpqrx99IG3R9DSWA3w7n/wl7yDt5+35J0ERK3bebQW1STsQ==",
  3828 + "license": "MIT",
  3829 + "funding": {
  3830 + "url": "https://github.com/sponsors/codedredd"
  3831 + }
  3832 + },
  3833 + "node_modules/@pinia-orm/nuxt": {
  3834 + "version": "1.10.2",
  3835 + "resolved": "https://registry.npmjs.org/@pinia-orm/nuxt/-/nuxt-1.10.2.tgz",
  3836 + "integrity": "sha512-I/dNHuFR2V8K9X6oi5P+EKCbiSRSenbMfMXySmxQOqaMU81f2XVaPVTaq9dgomo9i5mE7x/QqRK49ne8av51ag==",
  3837 + "license": "MIT",
  3838 + "dependencies": {
  3839 + "@nuxt/kit": "^3.12.3",
  3840 + "pinia-orm": "1.10.2"
  3841 + },
  3842 + "funding": {
  3843 + "url": "https://github.com/sponsors/codedredd"
  3844 + },
  3845 + "peerDependencies": {
  3846 + "@pinia/nuxt": "<=0.9.0"
  3847 + }
  3848 + },
3823 "node_modules/@pinia/nuxt": { 3849 "node_modules/@pinia/nuxt": {
3824 - "version": "0.11.0", 3850 + "version": "0.9.0",
3825 - "resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.11.0.tgz", 3851 + "resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.9.0.tgz",
3826 - "integrity": "sha512-QGFlUAkeVAhPCTXacrtNP4ti24sGEleVzmxcTALY9IkS6U5OUox7vmNL1pkqBeW39oSNq/UC5m40ofDEPHB1fg==", 3852 + "integrity": "sha512-2yeRo7LeyCF68AbNeL3xu2h6uw0617RkcsYxmA8DJM0R0PMdz5wQHnc44KeENQxR/Mrq8T910XVT6buosqsjBQ==",
3827 "license": "MIT", 3853 "license": "MIT",
3828 "dependencies": { 3854 "dependencies": {
3829 "@nuxt/kit": "^3.9.0" 3855 "@nuxt/kit": "^3.9.0"
@@ -3832,7 +3858,7 @@ @@ -3832,7 +3858,7 @@
3832 "url": "https://github.com/sponsors/posva" 3858 "url": "https://github.com/sponsors/posva"
3833 }, 3859 },
3834 "peerDependencies": { 3860 "peerDependencies": {
3835 - "pinia": "^3.0.2" 3861 + "pinia": "^2.3.0"
3836 } 3862 }
3837 }, 3863 },
3838 "node_modules/@pkgjs/parseargs": { 3864 "node_modules/@pkgjs/parseargs": {
@@ -13014,12 +13040,13 @@ @@ -13014,12 +13040,13 @@
13014 } 13040 }
13015 }, 13041 },
13016 "node_modules/pinia": { 13042 "node_modules/pinia": {
13017 - "version": "3.0.2", 13043 + "version": "2.3.1",
13018 - "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.2.tgz", 13044 + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
13019 - "integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==", 13045 + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
13020 "license": "MIT", 13046 "license": "MIT",
13021 "dependencies": { 13047 "dependencies": {
13022 - "@vue/devtools-api": "^7.7.2" 13048 + "@vue/devtools-api": "^6.6.3",
  13049 + "vue-demi": "^0.14.10"
13023 }, 13050 },
13024 "funding": { 13051 "funding": {
13025 "url": "https://github.com/sponsors/posva" 13052 "url": "https://github.com/sponsors/posva"
@@ -13034,6 +13061,22 @@ @@ -13034,6 +13061,22 @@
13034 } 13061 }
13035 } 13062 }
13036 }, 13063 },
  13064 + "node_modules/pinia-orm": {
  13065 + "version": "1.10.2",
  13066 + "resolved": "https://registry.npmjs.org/pinia-orm/-/pinia-orm-1.10.2.tgz",
  13067 + "integrity": "sha512-Q8QwFFdAmhc347QY6ndXtLZX4kE+46dUQbyy0ha6URmdIaz1jf8FbZEJ8BhHMLGPx+PeO/QJraxvvETx62lMQA==",
  13068 + "license": "MIT",
  13069 + "dependencies": {
  13070 + "@pinia-orm/normalizr": "1.10.2",
  13071 + "vue-demi": "^0.14.10"
  13072 + },
  13073 + "funding": {
  13074 + "url": "https://github.com/sponsors/codedredd"
  13075 + },
  13076 + "peerDependencies": {
  13077 + "pinia": "^2.1.7"
  13078 + }
  13079 + },
13037 "node_modules/pinia-plugin-persistedstate": { 13080 "node_modules/pinia-plugin-persistedstate": {
13038 "version": "4.2.0", 13081 "version": "4.2.0",
13039 "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.2.0.tgz", 13082 "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.2.0.tgz",
@@ -13058,15 +13101,6 @@ @@ -13058,15 +13101,6 @@
13058 } 13101 }
13059 } 13102 }
13060 }, 13103 },
13061 - "node_modules/pinia/node_modules/@vue/devtools-api": {  
13062 - "version": "7.7.5",  
13063 - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.5.tgz",  
13064 - "integrity": "sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==",  
13065 - "license": "MIT",  
13066 - "dependencies": {  
13067 - "@vue/devtools-kit": "^7.7.5"  
13068 - }  
13069 - },  
13070 "node_modules/pirates": { 13104 "node_modules/pirates": {
13071 "version": "4.0.7", 13105 "version": "4.0.7",
13072 "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", 13106 "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@@ -17155,6 +17189,32 @@ @@ -17155,6 +17189,32 @@
17155 "ufo": "^1.5.4" 17189 "ufo": "^1.5.4"
17156 } 17190 }
17157 }, 17191 },
  17192 + "node_modules/vue-demi": {
  17193 + "version": "0.14.10",
  17194 + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
  17195 + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
  17196 + "hasInstallScript": true,
  17197 + "license": "MIT",
  17198 + "bin": {
  17199 + "vue-demi-fix": "bin/vue-demi-fix.js",
  17200 + "vue-demi-switch": "bin/vue-demi-switch.js"
  17201 + },
  17202 + "engines": {
  17203 + "node": ">=12"
  17204 + },
  17205 + "funding": {
  17206 + "url": "https://github.com/sponsors/antfu"
  17207 + },
  17208 + "peerDependencies": {
  17209 + "@vue/composition-api": "^1.0.0-rc.1",
  17210 + "vue": "^3.0.0-0 || ^2.6.0"
  17211 + },
  17212 + "peerDependenciesMeta": {
  17213 + "@vue/composition-api": {
  17214 + "optional": true
  17215 + }
  17216 + }
  17217 + },
17158 "node_modules/vue-devtools-stub": { 17218 "node_modules/vue-devtools-stub": {
17159 "version": "0.1.0", 17219 "version": "0.1.0",
17160 "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", 17220 "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz",
1 { 1 {
2 "name": "nuxt-app", 2 "name": "nuxt-app",
3 - "version": "0.1.0", 3 + "version": "0.2.0",
4 "private": true, 4 "private": true,
5 "type": "module", 5 "type": "module",
6 "scripts": { 6 "scripts": {
@@ -21,14 +21,15 @@ @@ -21,14 +21,15 @@
21 "@nuxt/scripts": "^0.11.6", 21 "@nuxt/scripts": "^0.11.6",
22 "@nuxt/test-utils": "^3.17.2", 22 "@nuxt/test-utils": "^3.17.2",
23 "@nuxt/ui": "^2.22.0", 23 "@nuxt/ui": "^2.22.0",
24 - "@pinia/nuxt": "^0.11.0", 24 + "@pinia-orm/nuxt": "^1.10.2",
  25 + "@pinia/nuxt": "^0.9.0",
25 "@unhead/vue": "^2.0.8", 26 "@unhead/vue": "^2.0.8",
26 "@vueuse/core": "^13.1.0", 27 "@vueuse/core": "^13.1.0",
27 "@vueuse/nuxt": "^13.1.0", 28 "@vueuse/nuxt": "^13.1.0",
28 "eslint": "^9.25.1", 29 "eslint": "^9.25.1",
29 "lucide-vue-next": "^0.503.0", 30 "lucide-vue-next": "^0.503.0",
30 "nuxt": "^3.16.2", 31 "nuxt": "^3.16.2",
31 - "pinia": "^3.0.2", 32 + "pinia": "^2.3.1",
32 "pinia-plugin-persistedstate": "^4.2.0", 33 "pinia-plugin-persistedstate": "^4.2.0",
33 "vue": "^3.5.13", 34 "vue": "^3.5.13",
34 "vue-router": "^4.5.0", 35 "vue-router": "^4.5.0",
  1 +<script lang="ts" setup></script>
  2 +
  3 +<template>
  4 + <v-container class="mx-auto px-4 py-8">
  5 + <movies-list />
  6 + </v-container>
  7 +</template>
  8 +
  9 +<style scoped></style>
  1 +<script setup lang="ts">
  2 +
  3 +</script>
  4 +
  5 +<template>
  6 + <section>
  7 + composant détail d'un film.
  8 + </section>
  9 +</template>
  10 +
  11 +<style scoped>
  12 +
  13 +</style>