V1 Fonctionnelle (pas de secu de l'api, ni front quali)

This commit is contained in:
ElPoyo
2025-09-27 11:08:24 +02:00
parent 65846640cf
commit b4a468c14b
14 changed files with 2132 additions and 353 deletions

View File

@@ -105,10 +105,10 @@
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>
<h3 class="font-semibold mb-2">{{ text.metadata.titre_fr }}</h3>
<p class="text-sm mb-3" style="color: #6b7280">{{ text.metadata.titre_pt }}</p>
<div class="flex justify-between items-center text-xs text-gray-500">
<span>{{ text.auteur }}</span>
<span>{{ text.metadata.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"/>

View File

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

View File

@@ -4,7 +4,7 @@
<!-- Navigation de retour -->
<div class="mb-6">
<button
@click="$router.go(-1)"
@click="$router.push('/textes')"
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">
@@ -17,10 +17,10 @@
<!-- 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 }}
{{ textData.metadata.titre_pt }}
</h1>
<h2 class="text-2xl" style="color: #6b7280">
{{ textData.metadata.titre_pt }}
{{ textData.metadata.titre_fr }}
</h2>
</div>
@@ -107,8 +107,8 @@ export default {
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`
// Utiliser l'URL de l'API backend pour le fichier audio
return textService.getAudioUrl(props.id)
})
const loadText = async () => {

View File

@@ -200,38 +200,8 @@ export default {
})
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
// Ne pas filtrer localement - l'API backend s'en charge
return allTexts.value
})
const totalPages = computed(() => {
@@ -246,23 +216,39 @@ export default {
const loadTexts = async () => {
try {
const texts = await textService.loadAllTexts()
// 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 = () => {
const performSearch = async () => {
currentPage.value = 1
await loadTexts()
}
const clearFilters = () => {
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) => {