Переглянути джерело

add the authors index page with expansion panels

Olivier Massot 6 місяців тому
батько
коміт
066aedc294

+ 0 - 15
app/.eslintrc.js

@@ -1,15 +0,0 @@
-module.exports = {
-  root: true,
-  env: {
-    browser: true,
-    node: true
-  },
-  extends: [
-    '@nuxtjs/eslint-config-typescript',
-    'plugin:nuxt/recommended'
-  ],
-  plugins: [
-  ],
-  // add your custom rules here
-  rules: {}
-}

+ 0 - 69
app/README.md

@@ -1,69 +0,0 @@
-# App
-
-## Build Setup
-
-```bash
-# install dependencies
-$ yarn install
-
-# serve with hot reload at localhost:3000
-$ yarn dev
-
-# build for production and launch server
-$ yarn build
-$ yarn start
-
-# generate static project
-$ yarn generate
-```
-
-For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
-
-## Special Directories
-
-You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
-
-### `assets`
-
-The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
-
-### `components`
-
-The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
-
-### `layouts`
-
-Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
-
-
-### `pages`
-
-This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
-
-### `plugins`
-
-The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
-
-### `static`
-
-This directory contains your static files. Each file inside this directory is mapped to `/`.
-
-Example: `/static/robots.txt` is mapped as `/robots.txt`.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
-
-### `store`
-
-This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
-
-More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).

+ 36 - 32
app/components/AuthorPanel.vue

@@ -2,24 +2,34 @@
   <div>
     <v-expansion-panel
       class="my-1"
-      @update:model-value="fetchSongs"
+      @group:selected="fetchSongs"
     >
-      <v-expansion-panel-title>
+      <v-expansion-panel-title class="font-weight-bold">
         {{ author.name }}
       </v-expansion-panel-title>
+
       <v-expansion-panel-text>
         <v-card v-if="loadingSongs" class="text-center pa-4">
           <v-progress-circular :indeterminate="true" />
-          <div class="mt-2">Loading songs...</div>
+          <div class="mt-2">
+            Loading songs...
+          </div>
         </v-card>
+
         <v-card v-else-if="songError" class="text-center pa-4 error--text">
           <v-icon color="error" large>mdi-alert-circle</v-icon>
-          <div class="mt-2">Error loading songs: {{ songError }}</div>
+          <div class="mt-2">
+            Error loading songs: {{ songError }}
+          </div>
         </v-card>
+
         <v-list v-else>
           <v-list-item v-for="song in songs" :key="song.id">
-            <v-list-item-title>{{ song.title }}</v-list-item-title>
+            <v-list-item-title>
+              - {{ song.title }}
+            </v-list-item-title>
           </v-list-item>
+
           <v-list-item v-if="songs.length === 0">
             <v-list-item-title class="text-center">No songs found for this author</v-list-item-title>
           </v-list-item>
@@ -30,59 +40,53 @@
 </template>
 
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { ref, PropType } from 'vue'
+import type Author from '~/types/interfaces'
 
 // Define props
-const props = defineProps<{
+const props = defineProps({
   author: {
-    id: number;
-    name: string;
-  };
-}>();
+    type: Object as PropType<Author>,
+    required: true
+  }
+})
 
-// Define types
-interface Song {
-  id: number;
-  title: string;
-  author: {
-    id: number;
-    name: string;
-  } | string; // Can be either an object or an IRI string
-}
+const runtimeConfig = useRuntimeConfig()
+
+const apiBaseUrl = runtimeConfig.apiBaseUrl ?? runtimeConfig.public.apiBaseUrl
 
-// State for songs
 const songs = ref<Song[]>([]);
 const loadingSongs = ref(false);
 const songError = ref<string | null>(null);
 
 // Fetch songs for the author
 const fetchSongs = async () => {
-  console.log('fetchSongs', props.author.id);
+  console.log('fetchSongs', props.author.id)
 
   // Skip if already loaded
   if (songs.value.length > 0) {
-    return;
+    return
   }
 
-  loadingSongs.value = true;
+  loadingSongs.value = true
 
   try {
-    const response = await fetch(`https://local.api.snc-demo.fr/api/songs?author=${props.author.id}`);
+    const response = await fetch( apiBaseUrl + `/api/songs?author=${props.author.id}`)
     if (!response.ok) {
-      throw new Error(`HTTP error. Status: ${response.status}`);
+      throw new Error(`HTTP error. Status: ${response.status}`)
     }
-    const data = await response.json();
-    songs.value = data['member'] || [];
+    const data = await response.json()
+    songs.value = data.member || []
   } catch (err) {
     songError.value = err instanceof Error ? err.message : 'Unknown error';
-    console.error(`Error fetching songs for author ${props.author.id}:`, err);
+    console.error(`Error fetching songs for author ${props.author.id}:`, err)
   } finally {
-    loadingSongs.value = false;
+    loadingSongs.value = false
   }
-};
+}
 
 // Expose methods to parent component
 defineExpose({
   fetchSongs
-});
+})
 </script>

+ 0 - 130
app/components/AuthorSongsList.vue

@@ -1,130 +0,0 @@
-<template>
-  <div>
-    <v-expansion-panels v-model="modelValue" @update:modelValue="onModelValueChange">
-      <v-expansion-panel
-        v-for="author in authors"
-        :key="author.id"
-      >
-        <v-expansion-panel-title>
-          {{ author.name }}
-        </v-expansion-panel-title>
-        <v-expansion-panel-text>
-          <v-card v-if="loadingSongs[author.id]" class="text-center pa-4">
-            <v-progress-circular :indeterminate="true" />
-            <div class="mt-2">Loading songs...</div>
-          </v-card>
-          <v-card v-else-if="songErrors[author.id]" class="text-center pa-4 error--text">
-            <v-icon color="error" large>mdi-alert-circle</v-icon>
-            <div class="mt-2">Error loading songs: {{ songErrors[author.id] }}</div>
-          </v-card>
-          <v-list v-else>
-            <v-list-item v-for="song in authorSongs[author.id]" :key="song.id">
-              <v-list-item-title>{{ song.title }}</v-list-item-title>
-            </v-list-item>
-            <v-list-item v-if="authorSongs[author.id] && authorSongs[author.id].length === 0">
-              <v-list-item-title class="text-center">No songs found for this author</v-list-item-title>
-            </v-list-item>
-          </v-list>
-        </v-expansion-panel-text>
-      </v-expansion-panel>
-    </v-expansion-panels>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, onMounted, watch } from 'vue'
-
-// Define props
-const props = defineProps<{
-  authors: Array<{
-    id: number;
-    name: string;
-  }>;
-  modelValue?: number | number[];
-}>();
-
-// Define emits
-const emit = defineEmits<{
-  (e: 'update:modelValue', value: number | number[]): void;
-}>();
-
-// Local model value
-const modelValue = ref<number | number[]>(props.modelValue ?? -1);
-
-// Define types
-interface Song {
-  id: number;
-  title: string;
-  author: {
-    id: number;
-    name: string;
-  } | string; // Can be either an object or an IRI string
-}
-
-// State for songs
-const authorSongs = ref<Record<number, Song[]>>({});
-const loadingSongs = ref<Record<number, boolean>>({});
-const songErrors = ref<Record<number, string>>({});
-
-// Watch for expansion panel changes
-const fetchSongsForAuthor = async (authorId: number) => {
-  // Skip if already loaded
-  if (authorSongs.value[authorId]) {
-    return;
-  }
-
-  loadingSongs.value[authorId] = true;
-
-  try {
-    const response = await fetch(`https://local.api.snc-demo.fr/api/songs?author=${authorId}`);
-    if (!response.ok) {
-      throw new Error(`HTTP error. Status: ${response.status}`);
-    }
-    const data = await response.json();
-    authorSongs.value[authorId] = data['hydra:member'] || [];
-  } catch (err) {
-    songErrors.value[authorId] = err instanceof Error ? err.message : 'Unknown error';
-    console.error(`Error fetching songs for author ${authorId}:`, err);
-  } finally {
-    loadingSongs.value[authorId] = false;
-  }
-};
-
-// Method to be called when an expansion panel is opened
-const onPanelChange = (panel: number | number[]) => {
-  if (Array.isArray(panel)) {
-    // Multiple panels can be open in some configurations
-    panel.forEach(index => {
-      if (index >= 0 && index < props.authors.length) {
-        fetchSongsForAuthor(props.authors[index].id);
-      }
-    });
-  } else if (panel >= 0 && panel < props.authors.length) {
-    fetchSongsForAuthor(props.authors[panel].id);
-  }
-};
-
-// Handle model value change
-const onModelValueChange = (panel: number | number[]) => {
-  // Update local model value
-  modelValue.value = panel;
-
-  // Emit the update event
-  emit('update:modelValue', panel);
-
-  // Fetch songs for the opened panel(s)
-  onPanelChange(panel);
-};
-
-// Watch for prop changes
-watch(() => props.modelValue, (newValue) => {
-  if (newValue !== undefined) {
-    modelValue.value = newValue;
-  }
-});
-
-// Expose method to parent component
-defineExpose({
-  onPanelChange
-});
-</script>

+ 0 - 11
app/components/NuxtLogo.vue

@@ -1,11 +0,0 @@
-<template>
-  <svg class="nuxt-logo" viewBox="0 0 45 30" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82" />
-  </svg>
-</template>
-
-<style>
-.nuxt-logo {
-  height: 180px;
-}
-</style>

Різницю між файлами не показано, бо вона завелика
+ 0 - 6
app/components/Tutorial.vue


+ 0 - 22
app/components/VuetifyLogo.vue

@@ -1,22 +0,0 @@
-<template>
-  <img
-    class="vuetify-logo"
-    alt="Vuetify Logo"
-    src="/vuetify-logo.svg"
-  >
-</template>
-
-<style>
-.vuetify-logo {
-  height: 180px;
-  width: 180px;
-  transform: rotateY(560deg);
-  animation: turn 3.5s ease-out forwards 1s;
-}
-
-@keyframes turn {
-  100% {
-    transform: rotateY(0deg);
-  }
-}
-</style>

+ 19 - 0
app/eslint.config.mjs

@@ -0,0 +1,19 @@
+import withNuxt from './.nuxt/eslint.config.mjs'
+
+// Configuration de base personnalisée
+const customConfig = [
+  {
+    ignores: [
+      '**/.nuxt',
+      'vendor/*',
+      'dist/*',
+    ],
+  },
+  {
+    rules: {
+      'no-console': 0,
+    },
+  },
+]
+
+export default withNuxt(customConfig)

+ 0 - 4
app/layouts/default.vue

@@ -53,10 +53,6 @@ const items = [
     title: 'Authors',
     to: '/'
   },
-  {
-    title: 'Songs',
-    to: '/songs'
-  },
   {
     title: 'About',
     to: '/about'

+ 10 - 0
app/nuxt.config.js

@@ -1,5 +1,15 @@
 export default defineNuxtConfig({
   ssr: true,
+
+  runtimeConfig: {
+    env: '',
+    apiBaseUrl: '',
+    public:  {
+      env: '',
+      apiBaseUrl: '',
+    }
+  },
+
   // Global page headers: https://go.nuxtjs.dev/config-head
   app: {
     head: {

+ 1 - 3
app/package.json

@@ -20,9 +20,7 @@
     "vuetify": "3.8.8"
   },
   "devDependencies": {
-    "@nuxtjs/eslint-config": "^12.0.0",
-    "@nuxtjs/eslint-config-typescript": "^12.1.0",
-    "@nuxtjs/eslint-module": "^4.1.0",
+    "@typescript-eslint/eslint-plugin": "^8.22.0",
     "@vue/test-utils": "^2.4.6",
     "eslint": "^9.19.0",
     "eslint-config-prettier": "^10.0.1",

+ 25 - 14
app/pages/index.vue

@@ -1,17 +1,31 @@
 <template>
   <div>
-    <h1 class="text-center mb-6">Authors</h1>
+    <h1 class="text-center mb-6">
+      Authors
+    </h1>
+
     <v-card v-if="pending" class="text-center pa-4">
-      <v-progress-circular :indeterminate="true" />
-      <div class="mt-2">Loading authors...</div>
+      <v-progress-circular
+        :indeterminate="true"
+      />
+      <div class="mt-2">
+        Loading authors...
+      </div>
     </v-card>
+
     <v-card v-else-if="error" class="text-center pa-4 error--text">
-      <v-icon color="error" large>mdi-alert-circle</v-icon>
-      <div class="mt-2">Error loading authors: {{ error }}</div>
+      <v-icon color="error" large>
+        mdi-alert-circle
+      </v-icon>
+      <div class="mt-2">
+        Error loading authors: {{ error }}
+      </div>
     </v-card>
+
     <v-card v-else-if="authors.length === 0" class="text-center pa-4">
       <div>No authors found</div>
     </v-card>
+
     <v-card v-else class="pa-4">
       <v-expansion-panels class="w-80 flex-column" multiple>
         <AuthorPanel
@@ -27,31 +41,28 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue'
 import AuthorPanel from '~/components/AuthorPanel.vue'
+import type Author from '~/types/interfaces'
 
-// Set page title
 useHead({
   title: 'Authors'
 })
 
-// Define types
-interface Author {
-  id: number
-  name: string
-}
+const runtimeConfig = useRuntimeConfig()
+
+const apiBaseUrl = runtimeConfig.apiBaseUrl ?? runtimeConfig.public.apiBaseUrl
 
-// Fetch authors from API
 const authors = ref<Author[]>([])
 const error = ref<string | null>(null)
 const pending = ref(true)
 
 onMounted(async () => {
   try {
-    const response = await fetch('https://local.api.snc-demo.fr/api/authors')
+    const response = await fetch(apiBaseUrl + '/api/authors')
     if (!response.ok) {
       throw new Error(`HTTP error. Status: ${response.status}`)
     }
     const data = await response.json()
-    authors.value = data['member'] || []
+    authors.value = data.member || []
   } catch (err) {
     error.value = err instanceof Error ? err.message : 'Unknown error'
     console.error('Error fetching authors:', err)

+ 10 - 0
app/types/interfaces.d.ts

@@ -0,0 +1,10 @@
+interface Author {
+  id: number
+  name: string
+}
+
+interface Song {
+  id: number
+  title: string
+  author: Author|string
+}

Деякі файли не було показано, через те що забагато файлів було змінено