Compare commits
1 Commits
03ffc846bf
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10d5238f3a |
@@ -270,7 +270,10 @@ class TextService {
|
|||||||
|
|
||||||
// Filtres
|
// Filtres
|
||||||
if (filters.category) {
|
if (filters.category) {
|
||||||
results = results.filter(text => text.metadata.categorie === filters.category)
|
const categoryLower = filters.category.toLowerCase()
|
||||||
|
results = results.filter(text =>
|
||||||
|
text.metadata.categorie?.toLowerCase() === categoryLower
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.difficulty) {
|
if (filters.difficulty) {
|
||||||
@@ -285,7 +288,7 @@ class TextService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NOUVEAU : Obtient toutes les catégories disponibles
|
* NOUVEAU : Obtient toutes les catégories disponibles (normalisées en minuscules)
|
||||||
*/
|
*/
|
||||||
async getCategories() {
|
async getCategories() {
|
||||||
const allTexts = await this.scanTexts()
|
const allTexts = await this.scanTexts()
|
||||||
@@ -293,7 +296,8 @@ class TextService {
|
|||||||
|
|
||||||
for (const text of allTexts) {
|
for (const text of allTexts) {
|
||||||
if (text.metadata.categorie) {
|
if (text.metadata.categorie) {
|
||||||
categories.add(text.metadata.categorie)
|
// Normaliser en minuscules
|
||||||
|
categories.add(text.metadata.categorie.toLowerCase())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +408,19 @@ app.get('/api/stats', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/categories - Liste des catégories disponibles
|
||||||
|
*/
|
||||||
|
app.get('/api/categories', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const categories = await textService.getCategories()
|
||||||
|
res.json(categories)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur GET /api/categories:', error)
|
||||||
|
res.status(500).json({ error: 'Erreur lors du chargement des catégories' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/texts/:id/audio - Fichier audio
|
* GET /api/texts/:id/audio - Fichier audio
|
||||||
*/
|
*/
|
||||||
@@ -439,6 +456,7 @@ app.listen(PORT, () => {
|
|||||||
console.log(` GET /api/texts/:id - Détails d'un texte`)
|
console.log(` GET /api/texts/:id - Détails d'un texte`)
|
||||||
console.log(` GET /api/random - Texte aléatoire`)
|
console.log(` GET /api/random - Texte aléatoire`)
|
||||||
console.log(` GET /api/stats - Statistiques`)
|
console.log(` GET /api/stats - Statistiques`)
|
||||||
|
console.log(` GET /api/categories - Liste des catégories`)
|
||||||
console.log(` GET /api/texts/:id/audio - Fichier audio`)
|
console.log(` GET /api/texts/:id/audio - Fichier audio`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<footer class="bg-gray-50 border-t border-gray-200 mt-auto">
|
<footer class="bg-gray-50 border-t border-gray-200 mt-auto">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<!-- Liens de navigation -->
|
||||||
|
<nav class="flex justify-center flex-wrap gap-6 mb-6 text-base">
|
||||||
|
<router-link to="/" class="text-gray-600 hover:text-black transition-colors">
|
||||||
|
Accueil
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/textes" class="text-gray-600 hover:text-black transition-colors">
|
||||||
|
Textes
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/actualites" class="text-gray-600 hover:text-black transition-colors">
|
||||||
|
Actualités
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/a-propos" class="text-gray-600 hover:text-black transition-colors font-medium">
|
||||||
|
À propos
|
||||||
|
</router-link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Informations principales -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-gray-600 mb-2">
|
<p class="text-gray-600 mb-2">
|
||||||
Site développé avec soin par Paul Fournel
|
Site développé avec soin par Paul Fournel
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export default {
|
|||||||
name: 'Texts',
|
name: 'Texts',
|
||||||
query: { search: searchQuery.value.trim() }
|
query: { search: searchQuery.value.trim() }
|
||||||
})
|
})
|
||||||
|
// Vider la barre de recherche après navigation
|
||||||
|
searchQuery.value = ''
|
||||||
mobileMenuOpen.value = false
|
mobileMenuOpen.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Texts from '../views/Texts.vue'
|
|||||||
import TextReader from '../views/TextReader.vue'
|
import TextReader from '../views/TextReader.vue'
|
||||||
import News from '../views/News.vue'
|
import News from '../views/News.vue'
|
||||||
import NewsArticle from '../views/NewsArticle.vue'
|
import NewsArticle from '../views/NewsArticle.vue'
|
||||||
|
import AboutPage from '../views/AboutPage.vue'
|
||||||
import { textService } from '../services/textService.js'
|
import { textService } from '../services/textService.js'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -38,12 +39,21 @@ const routes = [
|
|||||||
name: 'NewsArticle',
|
name: 'NewsArticle',
|
||||||
component: NewsArticle,
|
component: NewsArticle,
|
||||||
props: true
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/a-propos',
|
||||||
|
name: 'AboutPage',
|
||||||
|
component: AboutPage
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
// Toujours scroller en haut lors d'un changement de page
|
||||||
|
return { top: 0, behavior: 'smooth' }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
291
src/services/searchService.js
Normal file
291
src/services/searchService.js
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import { textService } from './textService.js'
|
||||||
|
import { wordpressService } from './wordpressService.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de recherche unifié qui combine les textes et les posts de blog
|
||||||
|
*/
|
||||||
|
export class SearchService {
|
||||||
|
constructor() {
|
||||||
|
this.cache = new Map()
|
||||||
|
this.CACHE_DURATION = 5 * 60 * 1000 // 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effectue une recherche unifiée dans les textes et les posts de blog
|
||||||
|
* @param {string} query - Terme de recherche
|
||||||
|
* @param {Object} filters - Filtres de recherche
|
||||||
|
* @param {string} filters.type - Type de contenu: 'texts', 'blog', 'all'
|
||||||
|
* @param {string} filters.textCategory - Catégorie de texte
|
||||||
|
* @param {Array<number>} filters.blogCategories - IDs des catégories de blog
|
||||||
|
* @param {boolean} filters.onlyWithAudio - Seulement les textes avec audio
|
||||||
|
* @returns {Promise<Object>} - Résultats de recherche
|
||||||
|
*/
|
||||||
|
async search(query = '', filters = {}) {
|
||||||
|
const normalizedQuery = query.trim().toLowerCase()
|
||||||
|
const type = filters.type || 'all'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = {
|
||||||
|
texts: [],
|
||||||
|
posts: [],
|
||||||
|
total: 0,
|
||||||
|
query: normalizedQuery,
|
||||||
|
filters: filters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rechercher dans les textes si nécessaire
|
||||||
|
if (type === 'texts' || type === 'all') {
|
||||||
|
results.texts = await this.searchTexts(normalizedQuery, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rechercher dans les posts de blog si nécessaire
|
||||||
|
if (type === 'blog' || type === 'all') {
|
||||||
|
results.posts = await this.searchPosts(normalizedQuery, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
results.total = results.texts.length + results.posts.length
|
||||||
|
|
||||||
|
return results
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la recherche:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche dans les textes
|
||||||
|
* @param {string} query - Terme de recherche
|
||||||
|
* @param {Object} filters - Filtres
|
||||||
|
* @returns {Promise<Array>} - Textes correspondants
|
||||||
|
*/
|
||||||
|
async searchTexts(query, filters) {
|
||||||
|
try {
|
||||||
|
// Utiliser l'API de recherche des textes
|
||||||
|
const textFilters = {}
|
||||||
|
|
||||||
|
if (filters.textCategory) {
|
||||||
|
textFilters.category = filters.textCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.onlyWithAudio) {
|
||||||
|
textFilters.onlyWithAudio = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let texts = await textService.searchTexts(query, textFilters)
|
||||||
|
|
||||||
|
// Filtrage côté client pour la recherche locale si nécessaire
|
||||||
|
if (query) {
|
||||||
|
texts = texts.filter(text =>
|
||||||
|
this.matchesTextSearch(text, query)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normaliser les catégories (insensible à la casse)
|
||||||
|
texts = texts.map(text => ({
|
||||||
|
...text,
|
||||||
|
type: 'text',
|
||||||
|
metadata: {
|
||||||
|
...text.metadata,
|
||||||
|
categorie: text.metadata.categorie?.toLowerCase()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return texts
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la recherche de textes:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche dans les posts de blog
|
||||||
|
* @param {string} query - Terme de recherche
|
||||||
|
* @param {Object} filters - Filtres
|
||||||
|
* @returns {Promise<Array>} - Posts correspondants
|
||||||
|
*/
|
||||||
|
async searchPosts(query, filters) {
|
||||||
|
try {
|
||||||
|
let allPosts = []
|
||||||
|
|
||||||
|
// Si des catégories spécifiques sont demandées
|
||||||
|
if (filters.blogCategories && filters.blogCategories.length > 0) {
|
||||||
|
// Récupérer les posts pour chaque catégorie
|
||||||
|
for (const categoryId of filters.blogCategories) {
|
||||||
|
try {
|
||||||
|
const { posts } = await wordpressService.getPostsByCategory(categoryId, 100, 1)
|
||||||
|
allPosts = allPosts.concat(posts)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Erreur lors de la récupération des posts de la catégorie ${categoryId}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Récupérer tous les posts de toutes les catégories
|
||||||
|
const categories = await wordpressService.getCategories()
|
||||||
|
for (const category of categories) {
|
||||||
|
try {
|
||||||
|
const { posts } = await wordpressService.getPostsByCategory(category.id, 100, 1)
|
||||||
|
allPosts = allPosts.concat(posts)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Erreur lors de la récupération des posts de la catégorie ${category.name}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrer par recherche textuelle si nécessaire
|
||||||
|
if (query) {
|
||||||
|
allPosts = allPosts.filter(post =>
|
||||||
|
this.matchesPostSearch(post, query)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dédupliquer les posts
|
||||||
|
const uniquePosts = Array.from(
|
||||||
|
new Map(allPosts.map(post => [post.id, post])).values()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ajouter le type pour différenciation
|
||||||
|
return uniquePosts.map(post => ({
|
||||||
|
...post,
|
||||||
|
type: 'post'
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la recherche de posts:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un texte correspond à la recherche
|
||||||
|
* @param {Object} text - Texte à vérifier
|
||||||
|
* @param {string} query - Terme de recherche
|
||||||
|
* @returns {boolean} - true si correspond
|
||||||
|
*/
|
||||||
|
matchesTextSearch(text, query) {
|
||||||
|
const lowerQuery = query.toLowerCase()
|
||||||
|
|
||||||
|
return (
|
||||||
|
text.metadata.titre_fr?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
text.metadata.titre_pt?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
text.metadata.auteur?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
text.metadata.traducteur?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
text.metadata.categorie?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
text.frenchText?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
text.patoisText?.toLowerCase().includes(lowerQuery)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un post correspond à la recherche
|
||||||
|
* @param {Object} post - Post à vérifier
|
||||||
|
* @param {string} query - Terme de recherche
|
||||||
|
* @returns {boolean} - true si correspond
|
||||||
|
*/
|
||||||
|
matchesPostSearch(post, query) {
|
||||||
|
const lowerQuery = query.toLowerCase()
|
||||||
|
|
||||||
|
// Extraire le texte HTML (simplifié)
|
||||||
|
const stripHtml = (html) => {
|
||||||
|
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
post.title?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
stripHtml(post.content || '').toLowerCase().includes(lowerQuery) ||
|
||||||
|
stripHtml(post.excerpt || '').toLowerCase().includes(lowerQuery) ||
|
||||||
|
post.author?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
post.categories?.some(cat => cat.toLowerCase().includes(lowerQuery)) ||
|
||||||
|
post.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les catégories de textes disponibles
|
||||||
|
* @returns {Promise<Array<string>>} - Liste des catégories
|
||||||
|
*/
|
||||||
|
async getTextCategories() {
|
||||||
|
try {
|
||||||
|
// Essayer de récupérer depuis l'API si disponible
|
||||||
|
try {
|
||||||
|
const categories = await textService.fetchAPI('/categories')
|
||||||
|
return categories.sort()
|
||||||
|
} catch (apiError) {
|
||||||
|
console.warn('API categories non disponible, fallback sur scan local')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback : scanner tous les textes
|
||||||
|
const texts = await textService.loadAllTexts()
|
||||||
|
const categories = new Set()
|
||||||
|
|
||||||
|
texts.forEach(text => {
|
||||||
|
if (text.metadata.categorie) {
|
||||||
|
// Normaliser en minuscules
|
||||||
|
categories.add(text.metadata.categorie.toLowerCase())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(categories).sort()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération des catégories de textes:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les catégories de blog disponibles
|
||||||
|
* @returns {Promise<Array>} - Liste des catégories de blog
|
||||||
|
*/
|
||||||
|
async getBlogCategories() {
|
||||||
|
try {
|
||||||
|
const categories = await wordpressService.getCategories()
|
||||||
|
|
||||||
|
// Récupérer également les sous-catégories
|
||||||
|
const allCategories = [...categories]
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
const subcategories = await this.getSubcategories(category.id)
|
||||||
|
allCategories.push(...subcategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allCategories
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération des catégories de blog:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les sous-catégories d'une catégorie
|
||||||
|
* @param {number} parentId - ID de la catégorie parent
|
||||||
|
* @returns {Promise<Array>} - Sous-catégories
|
||||||
|
*/
|
||||||
|
async getSubcategories(parentId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://admin-afpl.federation-ouest-francoprovencal.fr/wp-json/wp/v2/categories?parent=${parentId}&per_page=100`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcategories = await response.json()
|
||||||
|
return subcategories
|
||||||
|
.filter(cat => cat.slug !== 'uncategorized')
|
||||||
|
.map(cat => wordpressService.formatCategory(cat))
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Erreur lors de la récupération des sous-catégories de ${parentId}:`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vide le cache
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.cache.clear()
|
||||||
|
textService.clearCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton
|
||||||
|
export const searchService = new SearchService()
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* Service pour communiquer avec l'API backend des textes
|
* Service pour communiquer avec l'API backend des textes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_TEXTS_API_URL || 'https://patois.lagaudiere.uk/api' || 'http://localhost:3000/api'
|
const API_BASE_URL = import.meta.env.VITE_TEXTS_API_URL || 'http://localhost:3000/api'
|
||||||
|
|
||||||
export class TextService {
|
export class TextService {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -126,6 +126,31 @@ export const wordpressService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une page WordPress par son slug
|
||||||
|
* @param {string} slug - Slug de la page (ex: 'a-propos')
|
||||||
|
* @returns {Promise<Object>} - Page formatée
|
||||||
|
*/
|
||||||
|
async getPageBySlug(slug) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/pages?slug=${slug}&_embed`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = await response.json()
|
||||||
|
if (!pages.length) {
|
||||||
|
throw new Error(`Page "${slug}" non trouvée`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatPage(pages[0])
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de la récupération de la page ${slug}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formate une catégorie WordPress
|
* Formate une catégorie WordPress
|
||||||
* @param {Object} category - Catégorie brute de l'API WordPress
|
* @param {Object} category - Catégorie brute de l'API WordPress
|
||||||
@@ -165,6 +190,27 @@ export const wordpressService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate une page WordPress pour l'utilisation dans l'application
|
||||||
|
* @param {Object} page - Page brute de l'API WordPress
|
||||||
|
* @returns {Object} - Page formatée
|
||||||
|
*/
|
||||||
|
formatPage(page) {
|
||||||
|
return {
|
||||||
|
id: page.id,
|
||||||
|
title: page.title.rendered,
|
||||||
|
content: page.content.rendered,
|
||||||
|
excerpt: page.excerpt.rendered,
|
||||||
|
date: page.date,
|
||||||
|
modified: page.modified,
|
||||||
|
slug: page.slug,
|
||||||
|
link: page.link,
|
||||||
|
author: page._embedded?.author?.[0]?.name || 'Auteur inconnu',
|
||||||
|
featuredImage: page._embedded?.['wp:featuredmedia']?.[0]?.source_url || null,
|
||||||
|
featuredImageAlt: page._embedded?.['wp:featuredmedia']?.[0]?.alt_text || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formate une date pour l'affichage
|
* Formate une date pour l'affichage
|
||||||
* @param {string} dateString - Date au format ISO
|
* @param {string} dateString - Date au format ISO
|
||||||
|
|||||||
159
src/views/AboutPage.vue
Normal file
159
src/views/AboutPage.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<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-6xl font-bold text-black mb-6">
|
||||||
|
À propos de l'association
|
||||||
|
</h1>
|
||||||
|
<p class="text-2xl mb-8 leading-relaxed" style="color: #6b7280">
|
||||||
|
Découvrez l'histoire et les valeurs de notre association dédiée à la préservation
|
||||||
|
du patrimoine linguistique franco-provençal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section avec image -->
|
||||||
|
<section class="py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">
|
||||||
|
<div class="grid md:grid-cols-2 gap-12 items-center mb-20">
|
||||||
|
<!-- Emplacement image -->
|
||||||
|
<div class="bg-gray-200 rounded-xl aspect-[4/3] flex items-center justify-center">
|
||||||
|
<span class="text-gray-400 text-lg">Image à venir</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Texte -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-4xl font-bold text-black mb-6">Notre histoire</h2>
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed mb-4">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
|
||||||
|
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
|
||||||
|
exercitation ullamco laboris.
|
||||||
|
</p>
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed">
|
||||||
|
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
|
||||||
|
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section inversée avec image -->
|
||||||
|
<div class="grid md:grid-cols-2 gap-12 items-center mb-20">
|
||||||
|
<!-- Texte -->
|
||||||
|
<div class="md:order-1 order-2">
|
||||||
|
<h2 class="text-4xl font-bold text-black mb-6">Nos valeurs</h2>
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed mb-4">
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
|
||||||
|
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
|
||||||
|
architecto beatae vitae dicta sunt explicabo.
|
||||||
|
</p>
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed">
|
||||||
|
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia
|
||||||
|
consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Emplacement image -->
|
||||||
|
<div class="bg-gray-200 rounded-xl aspect-[4/3] flex items-center justify-center md:order-2 order-1">
|
||||||
|
<span class="text-gray-400 text-lg">Image à venir</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section équipe -->
|
||||||
|
<section class="py-20 px-4">
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
|
<h2 class="text-4xl font-bold text-black mb-12 text-center">L'équipe</h2>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-3 gap-8">
|
||||||
|
<!-- Membre 1 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
<div class="bg-gray-200 aspect-square flex items-center justify-center">
|
||||||
|
<span class="text-gray-400">Photo</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="font-bold text-xl mb-2 text-black">Prénom Nom</h3>
|
||||||
|
<p class="text-gray-600 mb-2">Président(e)</p>
|
||||||
|
<p class="text-gray-700 text-base">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Membre 2 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
<div class="bg-gray-200 aspect-square flex items-center justify-center">
|
||||||
|
<span class="text-gray-400">Photo</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="font-bold text-xl mb-2 text-black">Prénom Nom</h3>
|
||||||
|
<p class="text-gray-600 mb-2">Vice-président(e)</p>
|
||||||
|
<p class="text-gray-700 text-base">
|
||||||
|
Sed do eiusmod tempor incididunt ut labore et dolore.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Membre 3 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
<div class="bg-gray-200 aspect-square flex items-center justify-center">
|
||||||
|
<span class="text-gray-400">Photo</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="font-bold text-xl mb-2 text-black">Prénom Nom</h3>
|
||||||
|
<p class="text-gray-600 mb-2">Secrétaire</p>
|
||||||
|
<p class="text-gray-700 text-base">
|
||||||
|
Ut enim ad minim veniam, quis nostrud exercitation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section contact -->
|
||||||
|
<section class="py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||||
|
<div class="max-w-4xl mx-auto px-4">
|
||||||
|
<div class="bg-white rounded-xl shadow-lg p-8 md:p-12">
|
||||||
|
<h2 class="text-4xl font-bold text-black mb-8 text-center">Nous contacter</h2>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-xl mb-4 text-black">Coordonnées</h3>
|
||||||
|
<div class="space-y-3 text-lg text-gray-700">
|
||||||
|
<p>
|
||||||
|
<strong>Email :</strong><br>
|
||||||
|
contact@exemple.fr
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Téléphone :</strong><br>
|
||||||
|
04 00 00 00 00
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Adresse :</strong><br>
|
||||||
|
MJC, Place du Plomb<br>
|
||||||
|
69850 Saint-Martin-en-Haut
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="bg-gray-200 rounded-lg aspect-video flex items-center justify-center">
|
||||||
|
<span class="text-gray-400">Carte Google Maps</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.title = 'À propos - Patois Franco-Provençal'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
<!-- Section héro -->
|
<!-- Section héro -->
|
||||||
<section class="py-20 px-4">
|
<section class="py-20 px-4">
|
||||||
<div class="max-w-4xl mx-auto text-center">
|
<div class="max-w-4xl mx-auto text-center">
|
||||||
<h1 class="text-5xl font-bold text-black mb-6">
|
<h1 class="text-6xl font-bold text-black mb-6">
|
||||||
Patois Franco-Provençal
|
Patois Franco-Provençal
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl mb-8 leading-relaxed" style="color: #6b7280">
|
<p class="text-2xl mb-8 leading-relaxed" style="color: #6b7280">
|
||||||
Découvrez, apprenez et préservez notre patrimoine linguistique régional.
|
Découvrez, apprenez et préservez notre patrimoine linguistique régional.
|
||||||
Une collection de textes traditionnels traduits et annotés pour transmettre
|
Une collection de textes traditionnels traduits et annotés pour transmettre
|
||||||
la richesse de notre langue ancestrale.
|
la richesse de notre langue ancestrale.
|
||||||
@@ -19,9 +19,142 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section présentation -->
|
<!-- Section à propos de l'association -->
|
||||||
|
<section class="py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">
|
||||||
|
<div class="text-center mb-16">
|
||||||
|
<h2 class="text-4xl font-bold text-black mb-4">Notre Mission</h2>
|
||||||
|
<p class="text-2xl text-gray-700 max-w-3xl mx-auto leading-relaxed">
|
||||||
|
Faire connaître et perdurer notre langue régionale, le patois franco-provençal en Pays Lyonnais.
|
||||||
|
Nous créons des ponts entre les générations et faisons se rencontrer patoisants et sympathisants.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Histoire de l'association -->
|
||||||
|
<div class="bg-white rounded-xl shadow-lg p-8 md:p-12 mb-12">
|
||||||
|
<div class="flex items-start mb-6">
|
||||||
|
<div class="bg-black text-white rounded-full w-12 h-12 flex items-center justify-center flex-shrink-0 mr-4">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-bold mb-4 text-black">Il était une fois...</h3>
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed mb-4">
|
||||||
|
À la fin du vingtième siècle, en Lyonnais, des gens imaginèrent de se réunir pour agir
|
||||||
|
contre la disparition de leur patois. À Saint-Martin, Yzeron, Coise, Larajasse,
|
||||||
|
Saint-Symphorien et en bien d'autres lieux, on se rencontrait pour le simple plaisir de converser.
|
||||||
|
</p>
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed mb-4">
|
||||||
|
Peu à peu se fit jour une évidence surprenante : le nombre de patoisants était bien plus
|
||||||
|
grand que ce que l'on pensait. Des témoignages émouvants émergèrent, comme celui de cette
|
||||||
|
dame : <em class="text-gray-600">"J'avais oublié que je parlais patois. Je l'ai redécouvert en assistant
|
||||||
|
aux rencontres. Et maintenant, je sais à nouveau parler mon patois"</em>.
|
||||||
|
</p>
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed">
|
||||||
|
C'est ainsi que naquit l'idée de relier toutes ces initiatives locales, souvent méconnues
|
||||||
|
et isolées. Notre association vise à créer un réseau vivant et dynamique de préservation
|
||||||
|
de notre patrimoine linguistique.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Objectifs -->
|
||||||
|
<div class="grid md:grid-cols-3 gap-6 mb-12">
|
||||||
|
<div class="bg-white rounded-lg p-6 shadow-md border-t-4 border-black">
|
||||||
|
<div class="text-black mb-4">
|
||||||
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4 class="font-bold text-xl mb-2 text-black">Sauvegarder</h4>
|
||||||
|
<p class="text-gray-600 text-base">
|
||||||
|
Maintenir et développer l'usage du francoprovençal en Pays Lyonnais pour les générations futures.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg p-6 shadow-md border-t-4 border-black">
|
||||||
|
<div class="text-black mb-4">
|
||||||
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4 class="font-bold text-xl mb-2 text-black">Publier</h4>
|
||||||
|
<p class="text-gray-600 text-base">
|
||||||
|
Créer des contenus : bulletins, recueils, CD-ROM, DVD et spectacles vivants pour faire vivre notre culture.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg p-6 shadow-md border-t-4 border-black">
|
||||||
|
<div class="text-black mb-4">
|
||||||
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4 class="font-bold text-xl mb-2 text-black">Rassembler</h4>
|
||||||
|
<p class="text-gray-600 text-base">
|
||||||
|
Organiser des rencontres locales, régionales et internationales avec l'aire francoprovençale.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Le Francoprovençal -->
|
||||||
|
<div class="bg-gradient-to-r from-gray-900 to-gray-800 rounded-xl shadow-xl p-8 md:p-12 text-white mb-12">
|
||||||
|
<h3 class="text-2xl font-bold mb-6 flex items-center">
|
||||||
|
<svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
Le Francoprovençal : Un Patrimoine Vivant
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-4 text-lg text-gray-200 leading-relaxed">
|
||||||
|
<p>
|
||||||
|
Découvert par le linguiste italien Ascoli dans les années 1870, le francoprovençal forme
|
||||||
|
une zone linguistique distincte entre langue d'oïl et langue d'oc, couvrant la région
|
||||||
|
Rhône-Alpes, la Suisse Romande et les vallées des Alpes italiennes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Né de l'influence de Lugdunum (Lyon), fondée en 49 avant notre ère, cette langue porte
|
||||||
|
en elle plus de deux millénaires d'histoire. Malgré l'absence d'unité politique qui aurait
|
||||||
|
pu en faire une grande langue littéraire, le francoprovençal s'est maintenu dans les cœurs
|
||||||
|
et les pratiques quotidiennes de nos régions.
|
||||||
|
</p>
|
||||||
|
<p class="font-semibold border-l-4 border-white pl-4 italic">
|
||||||
|
Une langue n'est pas un simple code, mais une manière d'appréhender le monde et de sentir
|
||||||
|
les choses. Nos dialectes sont marqués par notre enfance, par des coutumes et des
|
||||||
|
savoir-faire, par des traditions de chansons et d'histoires.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Appel à l'action -->
|
||||||
|
<div class="text-center bg-white rounded-xl shadow-lg p-8 md:p-12">
|
||||||
|
<h3 class="text-3xl font-bold mb-4 text-black">Rejoignez-nous !</h3>
|
||||||
|
<p class="text-lg text-gray-700 mb-6 leading-relaxed max-w-2xl mx-auto">
|
||||||
|
Que vous soyez patoisant confirmé, curieux de vos racines, ou simplement passionné par
|
||||||
|
les langues régionales, nous vous invitons à participer à cette aventure collective.
|
||||||
|
Le patrimoine linguistique est fragile : ensemble, donnons-lui la créativité et la
|
||||||
|
jeunesse qui font l'originalité de notre région.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<router-link to="/a-propos" class="btn-primary">
|
||||||
|
En savoir +
|
||||||
|
</router-link>
|
||||||
|
<a href="mailto:contact@patois-lyon.fr" class="btn-secondary">
|
||||||
|
Nous contacter
|
||||||
|
</a>
|
||||||
|
<router-link to="/textes" class="btn-secondary">
|
||||||
|
Découvrir les textes
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section présentation des fonctionnalités -->
|
||||||
<section class="py-16 bg-gray-50">
|
<section class="py-16 bg-gray-50">
|
||||||
<div class="max-w-6xl mx-auto px-4">
|
<div class="max-w-6xl mx-auto px-4">
|
||||||
|
<h2 class="text-3xl font-bold text-center mb-12">Les outils du site</h2>
|
||||||
<div class="grid md:grid-cols-3 gap-8">
|
<div class="grid md:grid-cols-3 gap-8">
|
||||||
<div class="text-center">
|
<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">
|
<div class="bg-white rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4 shadow-sm">
|
||||||
@@ -29,8 +162,8 @@
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold mb-2">Textes Authentiques</h3>
|
<h3 class="text-xl font-semibold mb-2">Textes Authentiques</h3>
|
||||||
<p style="color: #6b7280">
|
<p class="text-base" style="color: #6b7280">
|
||||||
Collection de textes traditionnels et contemporains en patois franco-provençal
|
Collection de textes traditionnels et contemporains en patois franco-provençal
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,8 +174,8 @@
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold mb-2">Audio Intégré</h3>
|
<h3 class="text-xl font-semibold mb-2">Audio Intégré</h3>
|
||||||
<p style="color: #6b7280">
|
<p class="text-base" style="color: #6b7280">
|
||||||
Écoutez la prononciation authentique avec nos enregistrements audio
|
Écoutez la prononciation authentique avec nos enregistrements audio
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,8 +186,8 @@
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold mb-2">Recherche Avancée</h3>
|
<h3 class="text-xl font-semibold mb-2">Recherche Avancée</h3>
|
||||||
<p style="color: #6b7280">
|
<p class="text-base" style="color: #6b7280">
|
||||||
Trouvez facilement des textes par mot-clé, auteur ou thématique
|
Trouvez facilement des textes par mot-clé, auteur ou thématique
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,20 +201,20 @@
|
|||||||
<h2 class="text-3xl font-bold mb-12">Notre Collection</h2>
|
<h2 class="text-3xl font-bold mb-12">Notre Collection</h2>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.totalTexts }}</div>
|
<div class="text-5xl font-bold text-black mb-2">{{ stats.totalTexts }}</div>
|
||||||
<div style="color: #6b7280">Textes</div>
|
<div class="text-lg" style="color: #6b7280">Textes</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.withAudio }}</div>
|
<div class="text-5xl font-bold text-black mb-2">{{ stats.withAudio }}</div>
|
||||||
<div style="color: #6b7280">Avec Audio</div>
|
<div class="text-lg" style="color: #6b7280">Avec Audio</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.authors }}</div>
|
<div class="text-5xl font-bold text-black mb-2">{{ stats.authors }}</div>
|
||||||
<div style="color: #6b7280">Auteurs</div>
|
<div class="text-lg" style="color: #6b7280">Auteurs</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-4xl font-bold text-black mb-2">{{ stats.categories }}</div>
|
<div class="text-5xl font-bold text-black mb-2">{{ stats.categories }}</div>
|
||||||
<div style="color: #6b7280">Catégories</div>
|
<div class="text-lg" style="color: #6b7280">Catégories</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +225,7 @@
|
|||||||
<div class="max-w-6xl mx-auto px-4">
|
<div class="max-w-6xl mx-auto px-4">
|
||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
<h2 class="text-3xl font-bold mb-4">Textes Récents</h2>
|
<h2 class="text-3xl font-bold mb-4">Textes Récents</h2>
|
||||||
<p style="color: #6b7280">Découvrez nos derniers ajouts à la collection</p>
|
<p class="text-lg" style="color: #6b7280">Découvrez nos derniers ajouts à la collection</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-3 gap-6">
|
<div class="grid md:grid-cols-3 gap-6">
|
||||||
@@ -102,9 +235,9 @@
|
|||||||
class="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
class="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||||||
@click="$router.push(`/texte/${text.id}`)"
|
@click="$router.push(`/texte/${text.id}`)"
|
||||||
>
|
>
|
||||||
<h3 class="font-semibold mb-2">{{ text.metadata.titre_fr }}</h3>
|
<h3 class="text-lg font-semibold mb-2">{{ text.metadata.titre_fr }}</h3>
|
||||||
<p class="text-sm mb-3" style="color: #6b7280">{{ text.metadata.titre_pt }}</p>
|
<p class="text-base mb-3" style="color: #6b7280">{{ text.metadata.titre_pt }}</p>
|
||||||
<div class="flex justify-between items-center text-xs text-gray-500">
|
<div class="flex justify-between items-center text-sm text-gray-500">
|
||||||
<span>{{ text.metadata.auteur }}</span>
|
<span>{{ text.metadata.auteur }}</span>
|
||||||
<span v-if="text.hasAudio" class="flex items-center">
|
<span v-if="text.hasAudio" class="flex items-center">
|
||||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
|||||||
@@ -3,51 +3,87 @@
|
|||||||
<div class="max-w-6xl mx-auto px-4">
|
<div class="max-w-6xl mx-auto px-4">
|
||||||
<!-- En-tête avec recherche -->
|
<!-- En-tête avec recherche -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-4xl font-bold text-black mb-4">Collection de Textes</h1>
|
<h1 class="text-4xl font-bold text-black mb-4">Recherche</h1>
|
||||||
<p class="text-gray-600 mb-6">
|
<p class="text-gray-600 mb-6">
|
||||||
Explorez notre collection de textes en patois franco-provençal
|
Explorez notre collection de textes en patois et nos articles de blog
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Barre de recherche avancée -->
|
<!-- Barre de recherche avancée -->
|
||||||
<div class="bg-gray-50 rounded-lg p-6 mb-6">
|
<div class="bg-gray-50 rounded-lg p-6 mb-6">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<!-- Barre de recherche principale -->
|
||||||
<div>
|
<div class="mb-4">
|
||||||
<input
|
<input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
@input="performSearch"
|
@input="performSearch"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher dans les textes..."
|
placeholder="Rechercher dans les textes et articles..."
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-black"
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-black"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<select
|
<!-- Filtres par type de contenu -->
|
||||||
v-model="selectedCategory"
|
<div class="mb-4">
|
||||||
@change="performSearch"
|
<label class="block text-sm font-medium text-gray-700 mb-2">Type de contenu</label>
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-black"
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
@click="setContentType('all')"
|
||||||
|
:class="contentType === 'all' ? 'bg-black text-white' : 'bg-white text-gray-700 border border-gray-300'"
|
||||||
|
class="px-4 py-2 rounded-lg hover:shadow-sm transition-shadow"
|
||||||
>
|
>
|
||||||
<option value="">Toutes les catégories</option>
|
Tout
|
||||||
<option v-for="category in categories" :key="category" :value="category">
|
</button>
|
||||||
{{ category }}
|
<button
|
||||||
</option>
|
@click="setContentType('texts')"
|
||||||
</select>
|
:class="contentType === 'texts' ? 'bg-black text-white' : 'bg-white text-gray-700 border border-gray-300'"
|
||||||
</div>
|
class="px-4 py-2 rounded-lg hover:shadow-sm transition-shadow"
|
||||||
<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>
|
Textes en patois
|
||||||
<option value="Facile">Facile</option>
|
</button>
|
||||||
<option value="Moyen">Moyen</option>
|
<button
|
||||||
<option value="Difficile">Difficile</option>
|
@click="setContentType('blog')"
|
||||||
</select>
|
:class="contentType === 'blog' ? 'bg-black text-white' : 'bg-white text-gray-700 border border-gray-300'"
|
||||||
|
class="px-4 py-2 rounded-lg hover:shadow-sm transition-shadow"
|
||||||
|
>
|
||||||
|
Articles de blog
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filtres supplémentaires -->
|
<!-- Filtres pour textes -->
|
||||||
<div class="flex flex-wrap gap-4 mt-4">
|
<div v-if="contentType === 'texts' || contentType === 'all'" class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Catégories de textes</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="category in textCategories"
|
||||||
|
:key="category"
|
||||||
|
@click="toggleTextCategory(category)"
|
||||||
|
:class="selectedTextCategories.includes(category) ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 border border-gray-300'"
|
||||||
|
class="px-3 py-1 rounded-full text-sm hover:shadow-sm transition-shadow capitalize"
|
||||||
|
>
|
||||||
|
{{ category }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtres pour blog -->
|
||||||
|
<div v-if="contentType === 'blog' || contentType === 'all'" class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Catégories de blog</label>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="category in blogCategories"
|
||||||
|
:key="category.id"
|
||||||
|
@click="toggleBlogCategory(category.id)"
|
||||||
|
:class="selectedBlogCategories.includes(category.id) ? 'bg-green-600 text-white' : 'bg-white text-gray-700 border border-gray-300'"
|
||||||
|
class="px-3 py-1 rounded-full text-sm hover:shadow-sm transition-shadow"
|
||||||
|
>
|
||||||
|
{{ category.name }}
|
||||||
|
<span v-if="category.count" class="ml-1 opacity-75">({{ category.count }})</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtres supplémentaires pour textes -->
|
||||||
|
<div v-if="contentType === 'texts' || contentType === 'all'" class="flex flex-wrap gap-4">
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="onlyWithAudio"
|
v-model="onlyWithAudio"
|
||||||
@@ -70,59 +106,73 @@
|
|||||||
<!-- Résultats -->
|
<!-- Résultats -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<p style="color: #6b7280" class="mb-6">
|
<p style="color: #6b7280" class="mb-6">
|
||||||
{{ filteredTexts.length }} texte(s) trouvé(s)
|
<span v-if="loading" class="inline-flex items-center">
|
||||||
<span v-if="searchQuery || selectedCategory || selectedDifficulty || onlyWithAudio">
|
<svg class="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Chargement...
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ totalResults }} résultat(s) trouvé(s)
|
||||||
|
<span v-if="hasActiveFilters">
|
||||||
pour vos critères de recherche
|
pour vos critères de recherche
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Liste des textes -->
|
<!-- Liste des résultats -->
|
||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
|
<!-- Résultat texte -->
|
||||||
<div
|
<div
|
||||||
v-for="text in paginatedTexts"
|
v-for="item in paginatedResults"
|
||||||
:key="text.id"
|
:key="`${item.type}-${item.id}`"
|
||||||
class="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer"
|
class="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer"
|
||||||
@click="$router.push(`/texte/${text.id}`)"
|
@click="navigateToItem(item)"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-start">
|
<!-- Badge de type -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<span
|
||||||
|
:class="item.type === 'text' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'"
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-medium"
|
||||||
|
>
|
||||||
|
{{ item.type === 'text' ? 'Texte en patois' : 'Article de blog' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenu pour texte -->
|
||||||
|
<div v-if="item.type === 'text'" class="flex justify-between items-start">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h2 class="text-xl font-semibold text-black mb-2">
|
<h2 class="text-xl font-semibold text-black mb-2">
|
||||||
{{ text.metadata.titre_fr }}
|
{{ item.metadata.titre_fr }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-lg mb-3" style="color: #6b7280">
|
<p class="text-lg mb-3" style="color: #6b7280">
|
||||||
{{ text.metadata.titre_pt }}
|
{{ item.metadata.titre_pt }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 text-sm mb-4" style="color: #6b7280">
|
<div class="flex flex-wrap gap-4 text-sm mb-4" style="color: #6b7280">
|
||||||
<span v-if="text.metadata.auteur">
|
<span v-if="item.metadata.auteur">
|
||||||
<strong>Auteur:</strong> {{ text.metadata.auteur }}
|
<strong>Auteur:</strong> {{ item.metadata.auteur }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="text.metadata.traducteur">
|
<span v-if="item.metadata.traducteur">
|
||||||
<strong>Traducteur:</strong> {{ text.metadata.traducteur }}
|
<strong>Traducteur:</strong> {{ item.metadata.traducteur }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="text.metadata.categorie">
|
<span v-if="item.metadata.categorie" class="capitalize">
|
||||||
<strong>Catégorie:</strong> {{ text.metadata.categorie }}
|
<strong>Catégorie:</strong> {{ item.metadata.categorie }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aperçu du texte -->
|
<!-- Aperçu du texte -->
|
||||||
<p class="text-sm line-clamp-3" style="color: #6b7280">
|
<p class="text-sm line-clamp-3" style="color: #6b7280">
|
||||||
{{ getTextPreview(text.frenchText) }}
|
{{ getTextPreview(item.frenchText) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-end ml-4">
|
<div class="flex flex-col items-end ml-4">
|
||||||
<!-- Badges -->
|
<!-- Badges -->
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span
|
<span v-if="item.hasAudio" class="flex items-center text-xs" style="color: #6b7280">
|
||||||
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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
@@ -131,6 +181,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenu pour post -->
|
||||||
|
<div v-else class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-xl font-semibold text-black mb-2" v-html="item.title"></h2>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm mb-4" style="color: #6b7280">
|
||||||
|
<span v-if="item.author">
|
||||||
|
<strong>Auteur:</strong> {{ item.author }}
|
||||||
|
</span>
|
||||||
|
<span v-if="item.date">
|
||||||
|
<strong>Date:</strong> {{ formatDate(item.date) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="item.categories && item.categories.length">
|
||||||
|
<strong>Catégories:</strong> {{ item.categories.join(', ') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aperçu du post -->
|
||||||
|
<div class="text-sm line-clamp-3" style="color: #6b7280" v-html="item.excerpt"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.featuredImage" class="ml-4">
|
||||||
|
<img :src="item.featuredImage" :alt="item.featuredImageAlt" class="w-32 h-32 object-cover rounded-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -160,11 +236,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Message si aucun résultat -->
|
<!-- Message si aucun résultat -->
|
||||||
<div v-if="filteredTexts.length === 0" class="text-center py-12">
|
<div v-if="!loading && totalResults === 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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucun texte trouvé</h3>
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucun résultat trouvé</h3>
|
||||||
<p class="text-gray-500">Essayez de modifier vos critères de recherche.</p>
|
<p class="text-gray-500">Essayez de modifier vos critères de recherche.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,82 +249,177 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { textService } from '../services/textService.js'
|
import { searchService } from '../services/searchService.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Texts',
|
name: 'Texts',
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const allTexts = ref([])
|
// État de la recherche
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedCategory = ref('')
|
const contentType = ref('texts') // 'all', 'texts', 'blog' - Par défaut sur textes
|
||||||
const selectedDifficulty = ref('')
|
const selectedTextCategories = ref([])
|
||||||
|
const selectedBlogCategories = ref([])
|
||||||
const onlyWithAudio = ref(false)
|
const onlyWithAudio = ref(false)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const textsPerPage = 10
|
const resultsPerPage = 10
|
||||||
|
|
||||||
const categories = computed(() => {
|
// Résultats
|
||||||
const cats = new Set()
|
const searchResults = ref({ texts: [], posts: [], total: 0 })
|
||||||
allTexts.value.forEach(text => {
|
const textCategories = ref([])
|
||||||
if (text.metadata.categorie) {
|
const blogCategories = ref([])
|
||||||
cats.add(text.metadata.categorie)
|
const loading = ref(true) // Commencer en mode chargement
|
||||||
|
let searchVersion = 0 // Pour éviter les race conditions
|
||||||
|
|
||||||
|
// Calculs
|
||||||
|
const hasActiveFilters = computed(() => {
|
||||||
|
return searchQuery.value.trim() !== '' ||
|
||||||
|
selectedTextCategories.value.length > 0 ||
|
||||||
|
selectedBlogCategories.value.length > 0 ||
|
||||||
|
onlyWithAudio.value ||
|
||||||
|
contentType.value !== 'all'
|
||||||
|
})
|
||||||
|
|
||||||
|
const allResults = computed(() => {
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
if (contentType.value === 'all' || contentType.value === 'texts') {
|
||||||
|
results.push(...searchResults.value.texts)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
return Array.from(cats).sort()
|
if (contentType.value === 'all' || contentType.value === 'blog') {
|
||||||
|
results.push(...searchResults.value.posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredTexts = computed(() => {
|
const totalResults = computed(() => allResults.value.length)
|
||||||
// Ne pas filtrer localement - l'API backend s'en charge
|
|
||||||
return allTexts.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalPages = computed(() => {
|
const totalPages = computed(() => {
|
||||||
return Math.ceil(filteredTexts.value.length / textsPerPage)
|
return Math.ceil(totalResults.value / resultsPerPage)
|
||||||
})
|
})
|
||||||
|
|
||||||
const paginatedTexts = computed(() => {
|
const paginatedResults = computed(() => {
|
||||||
const start = (currentPage.value - 1) * textsPerPage
|
const start = (currentPage.value - 1) * resultsPerPage
|
||||||
const end = start + textsPerPage
|
const end = start + resultsPerPage
|
||||||
return filteredTexts.value.slice(start, end)
|
return allResults.value.slice(start, end)
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadTexts = async () => {
|
// Méthodes
|
||||||
|
const loadCategories = async () => {
|
||||||
try {
|
try {
|
||||||
// Utiliser l'API de recherche avec les filtres actuels
|
const [textCats, blogCats] = await Promise.all([
|
||||||
const texts = await textService.searchTexts(searchQuery.value, {
|
searchService.getTextCategories(),
|
||||||
category: selectedCategory.value,
|
searchService.getBlogCategories()
|
||||||
difficulty: selectedDifficulty.value,
|
])
|
||||||
onlyWithAudio: onlyWithAudio.value
|
textCategories.value = textCats
|
||||||
})
|
blogCategories.value = blogCats
|
||||||
allTexts.value = texts
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement des textes:', error)
|
console.error('Erreur lors du chargement des catégories:', 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 () => {
|
const performSearch = async () => {
|
||||||
|
loading.value = true
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
await loadTexts()
|
|
||||||
|
// Incrémenter la version de recherche pour éviter les race conditions
|
||||||
|
searchVersion++
|
||||||
|
const currentVersion = searchVersion
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filters = {
|
||||||
|
type: contentType.value,
|
||||||
|
onlyWithAudio: onlyWithAudio.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearFilters = async () => {
|
// Ajouter les catégories de textes si sélectionnées
|
||||||
|
if (selectedTextCategories.value.length > 0) {
|
||||||
|
// Pour l'instant, on ne peut filtrer que par une catégorie via l'API
|
||||||
|
// On prend la première sélectionnée
|
||||||
|
filters.textCategory = selectedTextCategories.value[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les catégories de blog si sélectionnées
|
||||||
|
if (selectedBlogCategories.value.length > 0) {
|
||||||
|
filters.blogCategories = selectedBlogCategories.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await searchService.search(searchQuery.value, filters)
|
||||||
|
|
||||||
|
// Vérifier que cette recherche est toujours la plus récente
|
||||||
|
if (currentVersion !== searchVersion) {
|
||||||
|
console.log('Recherche obsolète ignorée')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrage côté client pour les catégories multiples de textes
|
||||||
|
if (selectedTextCategories.value.length > 1) {
|
||||||
|
results.texts = results.texts.filter(text =>
|
||||||
|
selectedTextCategories.value.includes(text.metadata.categorie)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults.value = results
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la recherche:', error)
|
||||||
|
// Ne mettre à jour que si c'est toujours la recherche actuelle
|
||||||
|
if (currentVersion === searchVersion) {
|
||||||
|
searchResults.value = { texts: [], posts: [], total: 0 }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Ne mettre loading à false que si c'est toujours la recherche actuelle
|
||||||
|
if (currentVersion === searchVersion) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setContentType = (type) => {
|
||||||
|
contentType.value = type
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTextCategory = (category) => {
|
||||||
|
const index = selectedTextCategories.value.indexOf(category)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedTextCategories.value.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
selectedTextCategories.value.push(category)
|
||||||
|
}
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBlogCategory = (categoryId) => {
|
||||||
|
const index = selectedBlogCategories.value.indexOf(categoryId)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedBlogCategories.value.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
selectedBlogCategories.value.push(categoryId)
|
||||||
|
}
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
selectedCategory.value = ''
|
contentType.value = 'texts' // Remettre sur textes par défaut
|
||||||
selectedDifficulty.value = ''
|
selectedTextCategories.value = []
|
||||||
|
selectedBlogCategories.value = []
|
||||||
onlyWithAudio.value = false
|
onlyWithAudio.value = false
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
// IMPORTANT: Recharger via l'API après avoir vidé les filtres
|
performSearch()
|
||||||
await loadTexts()
|
}
|
||||||
|
|
||||||
|
const navigateToItem = (item) => {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
router.push(`/texte/${item.id}`)
|
||||||
|
} else {
|
||||||
|
router.push(`/actualites/${item.id}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTextPreview = (text) => {
|
const getTextPreview = (text) => {
|
||||||
@@ -256,46 +427,61 @@ export default {
|
|||||||
return text.split('\n').slice(0, 3).join(' ').substring(0, 200) + '...'
|
return text.split('\n').slice(0, 3).join(' ').substring(0, 200) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDifficultyClass = (difficulty) => {
|
const formatDate = (dateString) => {
|
||||||
switch (difficulty?.toLowerCase()) {
|
const date = new Date(dateString)
|
||||||
case 'facile':
|
return date.toLocaleDateString('fr-FR', {
|
||||||
return 'bg-green-100 text-green-800'
|
year: 'numeric',
|
||||||
case 'moyen':
|
month: 'long',
|
||||||
case 'moyenne':
|
day: 'numeric'
|
||||||
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
|
// Initialiser la recherche depuis l'URL
|
||||||
watch(() => route.query.search, (newSearch) => {
|
watch(() => route.query.search, (newSearch) => {
|
||||||
if (newSearch) {
|
if (newSearch) {
|
||||||
searchQuery.value = newSearch
|
searchQuery.value = newSearch
|
||||||
|
// Si recherche depuis la navbar, chercher dans tout
|
||||||
|
contentType.value = 'all'
|
||||||
|
performSearch()
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
loadTexts()
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await loadCategories()
|
||||||
|
if (!route.query.search) {
|
||||||
|
// Si pas de recherche URL, rester sur textes par défaut
|
||||||
|
await performSearch()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allTexts,
|
|
||||||
searchQuery,
|
searchQuery,
|
||||||
selectedCategory,
|
contentType,
|
||||||
selectedDifficulty,
|
selectedTextCategories,
|
||||||
|
selectedBlogCategories,
|
||||||
onlyWithAudio,
|
onlyWithAudio,
|
||||||
currentPage,
|
currentPage,
|
||||||
categories,
|
textCategories,
|
||||||
filteredTexts,
|
blogCategories,
|
||||||
|
searchResults,
|
||||||
|
loading,
|
||||||
|
hasActiveFilters,
|
||||||
|
totalResults,
|
||||||
totalPages,
|
totalPages,
|
||||||
paginatedTexts,
|
paginatedResults,
|
||||||
performSearch,
|
performSearch,
|
||||||
|
setContentType,
|
||||||
|
toggleTextCategory,
|
||||||
|
toggleBlogCategory,
|
||||||
clearFilters,
|
clearFilters,
|
||||||
|
navigateToItem,
|
||||||
getTextPreview,
|
getTextPreview,
|
||||||
getDifficultyClass
|
formatDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user