312 lines
10 KiB
Vue
312 lines
10 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-white py-8">
|
|
<div class="max-w-6xl mx-auto px-4">
|
|
<!-- En-tête avec recherche -->
|
|
<div class="mb-8">
|
|
<h1 class="text-4xl font-bold text-black mb-4">Collection de Textes</h1>
|
|
<p class="text-gray-600 mb-6">
|
|
Explorez notre collection de textes en patois franco-provençal
|
|
</p>
|
|
|
|
<!-- Barre de recherche avancée -->
|
|
<div class="bg-gray-50 rounded-lg p-6 mb-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<input
|
|
v-model="searchQuery"
|
|
@input="performSearch"
|
|
type="text"
|
|
placeholder="Rechercher dans les textes..."
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-black"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<select
|
|
v-model="selectedCategory"
|
|
@change="performSearch"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-black"
|
|
>
|
|
<option value="">Toutes les catégories</option>
|
|
<option v-for="category in categories" :key="category" :value="category">
|
|
{{ category }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<select
|
|
v-model="selectedDifficulty"
|
|
@change="performSearch"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-black"
|
|
>
|
|
<option value="">Toutes les difficultés</option>
|
|
<option value="Facile">Facile</option>
|
|
<option value="Moyen">Moyen</option>
|
|
<option value="Difficile">Difficile</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtres supplémentaires -->
|
|
<div class="flex flex-wrap gap-4 mt-4">
|
|
<label class="flex items-center">
|
|
<input
|
|
v-model="onlyWithAudio"
|
|
@change="performSearch"
|
|
type="checkbox"
|
|
class="rounded mr-2"
|
|
/>
|
|
<span class="text-sm">Seulement avec audio</span>
|
|
</label>
|
|
<button
|
|
@click="clearFilters"
|
|
class="text-sm text-gray-600 hover:text-black"
|
|
>
|
|
Effacer les filtres
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Résultats -->
|
|
<div class="mb-6">
|
|
<p style="color: #6b7280" class="mb-6">
|
|
{{ filteredTexts.length }} texte(s) trouvé(s)
|
|
<span v-if="searchQuery || selectedCategory || selectedDifficulty || onlyWithAudio">
|
|
pour vos critères de recherche
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Liste des textes -->
|
|
<div class="grid gap-6">
|
|
<div
|
|
v-for="text in paginatedTexts"
|
|
:key="text.id"
|
|
class="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer"
|
|
@click="$router.push(`/texte/${text.id}`)"
|
|
>
|
|
<div class="flex justify-between items-start">
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-black mb-2">
|
|
{{ text.metadata.titre_fr }}
|
|
</h2>
|
|
<p class="text-lg mb-3" style="color: #6b7280">
|
|
{{ text.metadata.titre_pt }}
|
|
</p>
|
|
|
|
<div class="flex flex-wrap gap-4 text-sm mb-4" style="color: #6b7280">
|
|
<span v-if="text.metadata.auteur">
|
|
<strong>Auteur:</strong> {{ text.metadata.auteur }}
|
|
</span>
|
|
<span v-if="text.metadata.traducteur">
|
|
<strong>Traducteur:</strong> {{ text.metadata.traducteur }}
|
|
</span>
|
|
<span v-if="text.metadata.categorie">
|
|
<strong>Catégorie:</strong> {{ text.metadata.categorie }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Aperçu du texte -->
|
|
<p class="text-sm line-clamp-3" style="color: #6b7280">
|
|
{{ getTextPreview(text.frenchText) }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-col items-end ml-4">
|
|
<!-- Badges -->
|
|
<div class="flex flex-col gap-2">
|
|
<span
|
|
v-if="text.metadata.difficulte"
|
|
class="px-2 py-1 rounded-full text-xs"
|
|
:class="getDifficultyClass(text.metadata.difficulte)"
|
|
>
|
|
{{ text.metadata.difficulte }}
|
|
</span>
|
|
<span v-if="text.hasAudio" class="flex items-center text-xs" style="color: #6b7280">
|
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v6.114A4.369 4.369 0 005 11c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.369 4.369 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z"/>
|
|
</svg>
|
|
Audio
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div v-if="totalPages > 1" class="mt-8 flex justify-center">
|
|
<div class="flex space-x-2">
|
|
<button
|
|
@click="currentPage = Math.max(1, currentPage - 1)"
|
|
:disabled="currentPage === 1"
|
|
class="px-4 py-2 border border-gray-300 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
|
>
|
|
Précédent
|
|
</button>
|
|
|
|
<span class="px-4 py-2 bg-black text-white rounded-lg">
|
|
{{ currentPage }} / {{ totalPages }}
|
|
</span>
|
|
|
|
<button
|
|
@click="currentPage = Math.min(totalPages, currentPage + 1)"
|
|
:disabled="currentPage === totalPages"
|
|
class="px-4 py-2 border border-gray-300 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
|
>
|
|
Suivant
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Message si aucun résultat -->
|
|
<div v-if="filteredTexts.length === 0" class="text-center py-12">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucun texte trouvé</h3>
|
|
<p class="text-gray-500">Essayez de modifier vos critères de recherche.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { textService } from '../services/textService.js'
|
|
|
|
export default {
|
|
name: 'Texts',
|
|
setup() {
|
|
const route = useRoute()
|
|
|
|
const allTexts = ref([])
|
|
const searchQuery = ref('')
|
|
const selectedCategory = ref('')
|
|
const selectedDifficulty = ref('')
|
|
const onlyWithAudio = ref(false)
|
|
const currentPage = ref(1)
|
|
const textsPerPage = 10
|
|
|
|
const categories = computed(() => {
|
|
const cats = new Set()
|
|
allTexts.value.forEach(text => {
|
|
if (text.metadata.categorie) {
|
|
cats.add(text.metadata.categorie)
|
|
}
|
|
})
|
|
return Array.from(cats).sort()
|
|
})
|
|
|
|
const filteredTexts = computed(() => {
|
|
// Ne pas filtrer localement - l'API backend s'en charge
|
|
return allTexts.value
|
|
})
|
|
|
|
const totalPages = computed(() => {
|
|
return Math.ceil(filteredTexts.value.length / textsPerPage)
|
|
})
|
|
|
|
const paginatedTexts = computed(() => {
|
|
const start = (currentPage.value - 1) * textsPerPage
|
|
const end = start + textsPerPage
|
|
return filteredTexts.value.slice(start, end)
|
|
})
|
|
|
|
const loadTexts = async () => {
|
|
try {
|
|
// Utiliser l'API de recherche avec les filtres actuels
|
|
const texts = await textService.searchTexts(searchQuery.value, {
|
|
category: selectedCategory.value,
|
|
difficulty: selectedDifficulty.value,
|
|
onlyWithAudio: onlyWithAudio.value
|
|
})
|
|
allTexts.value = texts
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des textes:', error)
|
|
// Fallback : charger tous les textes si la recherche échoue
|
|
try {
|
|
const allTextsData = await textService.loadAllTexts()
|
|
allTexts.value = allTextsData
|
|
} catch (fallbackError) {
|
|
console.error('Erreur fallback:', fallbackError)
|
|
allTexts.value = []
|
|
}
|
|
}
|
|
}
|
|
|
|
const performSearch = async () => {
|
|
currentPage.value = 1
|
|
await loadTexts()
|
|
}
|
|
|
|
const clearFilters = async () => {
|
|
searchQuery.value = ''
|
|
selectedCategory.value = ''
|
|
selectedDifficulty.value = ''
|
|
onlyWithAudio.value = false
|
|
currentPage.value = 1
|
|
// IMPORTANT: Recharger via l'API après avoir vidé les filtres
|
|
await loadTexts()
|
|
}
|
|
|
|
const getTextPreview = (text) => {
|
|
if (!text) return ''
|
|
return text.split('\n').slice(0, 3).join(' ').substring(0, 200) + '...'
|
|
}
|
|
|
|
const getDifficultyClass = (difficulty) => {
|
|
switch (difficulty?.toLowerCase()) {
|
|
case 'facile':
|
|
return 'bg-green-100 text-green-800'
|
|
case 'moyen':
|
|
case 'moyenne':
|
|
return 'bg-yellow-100 text-yellow-800'
|
|
case 'difficile':
|
|
return 'bg-red-100 text-red-800'
|
|
default:
|
|
return 'bg-gray-100 text-gray-800'
|
|
}
|
|
}
|
|
|
|
// Initialiser la recherche depuis l'URL
|
|
watch(() => route.query.search, (newSearch) => {
|
|
if (newSearch) {
|
|
searchQuery.value = newSearch
|
|
}
|
|
}, { immediate: true })
|
|
|
|
onMounted(() => {
|
|
loadTexts()
|
|
})
|
|
|
|
return {
|
|
allTexts,
|
|
searchQuery,
|
|
selectedCategory,
|
|
selectedDifficulty,
|
|
onlyWithAudio,
|
|
currentPage,
|
|
categories,
|
|
filteredTexts,
|
|
totalPages,
|
|
paginatedTexts,
|
|
performSearch,
|
|
clearFilters,
|
|
getTextPreview,
|
|
getDifficultyClass
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.line-clamp-3 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|