소스 검색

add author panels

Olivier Massot 6 달 전
부모
커밋
85421b6680
5개의 변경된 파일233개의 추가작업 그리고 24개의 파일을 삭제
  1. 3 0
      api/src/Entity/Song.php
  2. 88 0
      app/components/AuthorPanel.vue
  3. 130 0
      app/components/AuthorSongsList.vue
  4. 12 9
      app/pages/index.vue
  5. 0 15
      app/pages/songs.vue

+ 3 - 0
api/src/Entity/Song.php

@@ -4,11 +4,14 @@ declare(strict_types=1);
 namespace App\Entity;
 
 use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiFilter;
+use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Serializer\Annotation\Groups;
 
 #[ORM\Entity]
 #[ApiResource]
+#[ApiFilter(SearchFilter::class, properties: ['author' => 'exact'])]
 class Song
 {
     #[ORM\Id]

+ 88 - 0
app/components/AuthorPanel.vue

@@ -0,0 +1,88 @@
+<template>
+  <div>
+    <v-expansion-panel
+      class="my-1"
+      @update:model-value="fetchSongs"
+    >
+      <v-expansion-panel-title>
+        {{ 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>
+        </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>
+        </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>
+          <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>
+        </v-list>
+      </v-expansion-panel-text>
+    </v-expansion-panel>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+
+// Define props
+const props = defineProps<{
+  author: {
+    id: number;
+    name: string;
+  };
+}>();
+
+// 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 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);
+
+  // Skip if already loaded
+  if (songs.value.length > 0) {
+    return;
+  }
+
+  loadingSongs.value = true;
+
+  try {
+    const response = await fetch(`https://local.api.snc-demo.fr/api/songs?author=${props.author.id}`);
+    if (!response.ok) {
+      throw new Error(`HTTP error. Status: ${response.status}`);
+    }
+    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);
+  } finally {
+    loadingSongs.value = false;
+  }
+};
+
+// Expose methods to parent component
+defineExpose({
+  fetchSongs
+});
+</script>

+ 130 - 0
app/components/AuthorSongsList.vue

@@ -0,0 +1,130 @@
+<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>

+ 12 - 9
app/pages/index.vue

@@ -9,21 +9,24 @@
       <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>
-      <v-list>
-        <v-list-item v-for="author in authors" :key="author.id">
-          <v-list-item-title>{{ author.name }}</v-list-item-title>
-        </v-list-item>
-        <v-list-item v-if="authors.length === 0">
-          <v-list-item-title class="text-center">No authors found</v-list-item-title>
-        </v-list-item>
-      </v-list>
+    <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
+          v-for="author in authors"
+          :key="author.id"
+          :author="author"
+        />
+      </v-expansion-panels>
     </v-card>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue'
+import AuthorPanel from '~/components/AuthorPanel.vue'
 
 // Set page title
 useHead({

+ 0 - 15
app/pages/songs.vue

@@ -1,15 +0,0 @@
-<template>
-  <div>
-    <h1 class="text-center mb-6">Songs</h1>
-    <v-card class="pa-4">
-      <p class="text-center">This page is under construction.</p>
-    </v-card>
-  </div>
-</template>
-
-<script setup lang="ts">
-// Set page title
-useHead({
-  title: 'Songs'
-})
-</script>