Init du front
This commit is contained in:
174
src/views/Home.vue
Normal file
174
src/views/Home.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-white">
|
||||
<!-- Section héro -->
|
||||
<section class="py-20 px-4">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<h1 class="text-5xl font-bold text-black mb-6">
|
||||
Patois Franco-Provençal
|
||||
</h1>
|
||||
<p class="text-xl mb-8 leading-relaxed" style="color: #6b7280">
|
||||
Découvrez, apprenez et préservez notre patrimoine linguistique régional.
|
||||
Une collection de textes traditionnels traduits et annotés pour transmettre
|
||||
la richesse de notre langue ancestrale.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<router-link to="/textes" class="btn-primary">
|
||||
Explorer les textes
|
||||
</router-link>
|
||||
<router-link to="/au-hasard" class="btn-secondary">
|
||||
Texte au hasard
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section présentation -->
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
<div class="text-center">
|
||||
<div class="bg-white rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4 shadow-sm">
|
||||
<svg class="w-8 h-8 text-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C20.832 18.477 19.246 18 17.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">Textes Authentiques</h3>
|
||||
<p style="color: #6b7280">
|
||||
Collection de textes traditionnels et contemporains en patois franco-provençal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="bg-white rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4 shadow-sm">
|
||||
<svg class="w-8 h-8 text-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">Audio Intégré</h3>
|
||||
<p style="color: #6b7280">
|
||||
Écoutez la prononciation authentique avec nos enregistrements audio
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="bg-white rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4 shadow-sm">
|
||||
<svg class="w-8 h-8 text-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">Recherche Avancée</h3>
|
||||
<p style="color: #6b7280">
|
||||
Trouvez facilement des textes par mot-clé, auteur ou thématique
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section statistiques -->
|
||||
<section class="py-16">
|
||||
<div class="max-w-4xl mx-auto px-4 text-center">
|
||||
<h2 class="text-3xl font-bold mb-12">Notre Collection</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
<div>
|
||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.totalTexts }}</div>
|
||||
<div style="color: #6b7280">Textes</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.withAudio }}</div>
|
||||
<div style="color: #6b7280">Avec Audio</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.authors }}</div>
|
||||
<div style="color: #6b7280">Auteurs</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.categories }}</div>
|
||||
<div style="color: #6b7280">Catégories</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section récents -->
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold mb-4">Textes Récents</h2>
|
||||
<p style="color: #6b7280">Découvrez nos derniers ajouts à la collection</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="text in recentTexts"
|
||||
:key="text.id"
|
||||
class="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||||
@click="$router.push(`/texte/${text.id}`)"
|
||||
>
|
||||
<h3 class="font-semibold mb-2">{{ text.titre_fr }}</h3>
|
||||
<p class="text-sm mb-3" style="color: #6b7280">{{ text.titre_pt }}</p>
|
||||
<div class="flex justify-between items-center text-xs text-gray-500">
|
||||
<span>{{ text.auteur }}</span>
|
||||
<span v-if="text.hasAudio" class="flex items-center">
|
||||
<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 class="text-center mt-8">
|
||||
<router-link to="/textes" class="btn-secondary">
|
||||
Voir tous les textes
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { textService } from '../services/textService.js'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
setup() {
|
||||
const stats = ref({
|
||||
totalTexts: 0,
|
||||
withAudio: 0,
|
||||
authors: 0,
|
||||
categories: 0
|
||||
})
|
||||
|
||||
const recentTexts = ref([])
|
||||
|
||||
// Fonction pour charger les statistiques et textes récents
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// Charger les statistiques
|
||||
const statsData = await textService.getStats()
|
||||
stats.value = statsData
|
||||
|
||||
// Charger les textes récents (tous les textes pour l'instant)
|
||||
const allTexts = await textService.loadAllTexts()
|
||||
recentTexts.value = allTexts.slice(0, 3) // Limiter à 3 textes récents
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des données:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
return {
|
||||
stats,
|
||||
recentTexts
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
135
src/views/Random.vue
Normal file
135
src/views/Random.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- En-tête -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-black mb-4">Texte au Hasard</h1>
|
||||
<p class="mb-6" style="color: #6b7280">
|
||||
Découvrez un texte choisi aléatoirement dans notre collection
|
||||
</p>
|
||||
<button
|
||||
@click="loadRandomText"
|
||||
:disabled="loading"
|
||||
class="btn-primary"
|
||||
>
|
||||
<span v-if="loading" class="flex items-center">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Chargement...
|
||||
</span>
|
||||
<span v-else>
|
||||
Nouveau texte au hasard
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Contenu du texte -->
|
||||
<div v-if="randomText && !loading">
|
||||
<!-- Navigation vers le texte complet -->
|
||||
<div class="text-center mb-6">
|
||||
<router-link
|
||||
:to="`/texte/${randomText.id}`"
|
||||
class="btn-secondary"
|
||||
>
|
||||
Voir le texte complet
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Affichage du texte -->
|
||||
<TextDisplay
|
||||
:patois-text="randomText.patoisText"
|
||||
:french-text="randomText.frenchText"
|
||||
:metadata="randomText.metadata"
|
||||
/>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="mt-8 flex justify-center space-x-4">
|
||||
<button
|
||||
@click="loadRandomText"
|
||||
class="btn-primary"
|
||||
>
|
||||
Autre texte au hasard
|
||||
</button>
|
||||
<router-link
|
||||
:to="`/texte/${randomText.id}`"
|
||||
class="btn-secondary"
|
||||
>
|
||||
Lire avec audio
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chargement -->
|
||||
<div v-if="loading" class="flex justify-center items-center py-20">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-black"></div>
|
||||
</div>
|
||||
|
||||
<!-- Erreur -->
|
||||
<div v-if="error" class="text-center py-20">
|
||||
<svg class="mx-auto h-12 w-12 text-red-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Erreur</h3>
|
||||
<p class="text-gray-500 mb-4">{{ error }}</p>
|
||||
<button
|
||||
@click="loadRandomText"
|
||||
class="btn-primary"
|
||||
>
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Message si aucun texte -->
|
||||
<div v-if="!randomText && !loading && !error" class="text-center py-20">
|
||||
<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 disponible</h3>
|
||||
<p class="text-gray-500">La collection est vide pour le moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { textService } from '../services/textService.js'
|
||||
import TextDisplay from '../components/TextDisplay.vue'
|
||||
|
||||
export default {
|
||||
name: 'Random',
|
||||
components: {
|
||||
TextDisplay
|
||||
},
|
||||
setup() {
|
||||
const randomText = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const loadRandomText = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const text = await textService.getRandomText()
|
||||
randomText.value = text
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
console.error('Erreur lors du chargement du texte aléatoire:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRandomText()
|
||||
})
|
||||
|
||||
return {
|
||||
randomText,
|
||||
loading,
|
||||
error,
|
||||
loadRandomText
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
186
src/views/TextReader.vue
Normal file
186
src/views/TextReader.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<!-- Navigation de retour -->
|
||||
<div class="mb-6">
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="flex items-center hover:text-black transition-colors" style="color: #6b7280"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Retour à la liste
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Titre du texte -->
|
||||
<div v-if="textData" class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-black mb-2">
|
||||
{{ textData.metadata.titre_fr }}
|
||||
</h1>
|
||||
<h2 class="text-2xl" style="color: #6b7280">
|
||||
{{ textData.metadata.titre_pt }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Lecteur audio -->
|
||||
<AudioPlayer
|
||||
v-if="textData && textData.hasAudio"
|
||||
:audio-src="audioSrc"
|
||||
class="mb-8"
|
||||
/>
|
||||
|
||||
<!-- Affichage du texte -->
|
||||
<TextDisplay
|
||||
v-if="textData"
|
||||
:patois-text="textData.patoisText"
|
||||
:french-text="textData.frenchText"
|
||||
:metadata="textData.metadata"
|
||||
/>
|
||||
|
||||
<!-- Actions supplémentaires -->
|
||||
<div v-if="textData" class="mt-8 flex justify-center space-x-4">
|
||||
<button
|
||||
@click="goToRandomText"
|
||||
class="btn-secondary"
|
||||
>
|
||||
Texte au hasard
|
||||
</button>
|
||||
<button
|
||||
@click="shareText"
|
||||
class="btn-secondary"
|
||||
>
|
||||
Partager
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Chargement -->
|
||||
<div v-if="loading" class="flex justify-center items-center py-20">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-black"></div>
|
||||
</div>
|
||||
|
||||
<!-- Erreur -->
|
||||
<div v-if="error" class="text-center py-20">
|
||||
<svg class="mx-auto h-12 w-12 text-red-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Erreur de chargement</h3>
|
||||
<p class="text-gray-500 mb-4">{{ error }}</p>
|
||||
<button
|
||||
@click="loadText"
|
||||
class="btn-primary"
|
||||
>
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { textService } from '../services/textService.js'
|
||||
import AudioPlayer from '../components/AudioPlayer.vue'
|
||||
import TextDisplay from '../components/TextDisplay.vue'
|
||||
|
||||
export default {
|
||||
name: 'TextReader',
|
||||
components: {
|
||||
AudioPlayer,
|
||||
TextDisplay
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const textData = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
|
||||
const audioSrc = computed(() => {
|
||||
if (!textData.value || !textData.value.hasAudio) return null
|
||||
// Le chemin vers le fichier audio sera construit dynamiquement
|
||||
return `/texts/${props.id}/audio.mp3`
|
||||
})
|
||||
|
||||
const loadText = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const text = await textService.loadText(props.id)
|
||||
textData.value = text
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
console.error('Erreur lors du chargement du texte:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goToRandomText = async () => {
|
||||
try {
|
||||
const randomText = await textService.getRandomText()
|
||||
router.push(`/texte/${randomText.id}`)
|
||||
} catch (err) {
|
||||
router.push('/au-hasard')
|
||||
}
|
||||
}
|
||||
|
||||
const shareText = async () => {
|
||||
const url = window.location.href
|
||||
const title = textData.value?.metadata?.titre_fr || 'Texte en patois'
|
||||
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: title,
|
||||
url: url
|
||||
})
|
||||
} catch (err) {
|
||||
// Fallback pour la copie dans le presse-papier
|
||||
copyToClipboard(url)
|
||||
}
|
||||
} else {
|
||||
copyToClipboard(url)
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
// Ici vous pourriez ajouter une notification de succès
|
||||
} catch (err) {
|
||||
console.error('Erreur lors de la copie:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Recharger le texte quand l'ID change
|
||||
watch(() => props.id, () => {
|
||||
loadText()
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
loadText()
|
||||
})
|
||||
|
||||
return {
|
||||
textData,
|
||||
loading,
|
||||
error,
|
||||
audioSrc,
|
||||
loadText,
|
||||
goToRandomText,
|
||||
shareText
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
325
src/views/Texts.vue
Normal file
325
src/views/Texts.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<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(() => {
|
||||
let filtered = allTexts.value
|
||||
|
||||
// Recherche textuelle
|
||||
if (searchQuery.value.trim()) {
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
filtered = filtered.filter(text => {
|
||||
return (
|
||||
text.metadata.titre_fr?.toLowerCase().includes(query) ||
|
||||
text.metadata.titre_pt?.toLowerCase().includes(query) ||
|
||||
text.metadata.auteur?.toLowerCase().includes(query) ||
|
||||
text.frenchText?.toLowerCase().includes(query) ||
|
||||
text.patoisText?.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Filtre par catégorie
|
||||
if (selectedCategory.value) {
|
||||
filtered = filtered.filter(text => text.metadata.categorie === selectedCategory.value)
|
||||
}
|
||||
|
||||
// Filtre par difficulté
|
||||
if (selectedDifficulty.value) {
|
||||
filtered = filtered.filter(text => text.metadata.difficulte === selectedDifficulty.value)
|
||||
}
|
||||
|
||||
// Filtre audio seulement
|
||||
if (onlyWithAudio.value) {
|
||||
filtered = filtered.filter(text => text.hasAudio)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
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 {
|
||||
const texts = await textService.loadAllTexts()
|
||||
allTexts.value = texts
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des textes:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const performSearch = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
searchQuery.value = ''
|
||||
selectedCategory.value = ''
|
||||
selectedDifficulty.value = ''
|
||||
onlyWithAudio.value = false
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user