Init du front
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>patois</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2239
package-lock.json
generated
Normal file
2239
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "patois",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"vue": "^3.5.21",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
|
"vite": "^7.1.7"
|
||||||
|
},
|
||||||
|
"description": "This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.",
|
||||||
|
"main": "postcss.config.js",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
38
src/App.vue
Normal file
38
src/App.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app" class="min-h-screen flex flex-col">
|
||||||
|
<NavigationBar />
|
||||||
|
|
||||||
|
<main class="flex-1">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<FooterComponent />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import NavigationBar from './components/NavigationBar.vue'
|
||||||
|
import FooterComponent from './components/FooterComponent.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
275
src/components/AudioPlayer.vue
Normal file
275
src/components/AudioPlayer.vue
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-gray-50 rounded-lg p-6 mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Lecture Audio</h3>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<!-- Contrôle du volume -->
|
||||||
|
<svg class="h-4 w-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.617.78L4.137 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.137l4.246-3.78z" clip-rule="evenodd"/>
|
||||||
|
<path d="M11.738 9.222a4.002 4.002 0 000-0.444c0.779-0.771 1.278-1.834 1.278-3.001s-0.499-2.23-1.278-3.001A1 1 0 0111.738 4.222a2 2 0 010 3.556A1 1 0 0011.738 9.222z"/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
v-model="volume"
|
||||||
|
@input="updateVolume"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
class="w-20 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-500 w-8">{{ volume }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contrôles de lecture -->
|
||||||
|
<div class="flex items-center justify-center space-x-4 mb-4">
|
||||||
|
<button
|
||||||
|
@click="rewind"
|
||||||
|
class="p-2 rounded-full hover:bg-gray-200 transition-colors"
|
||||||
|
:disabled="!hasAudio"
|
||||||
|
>
|
||||||
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="togglePlay"
|
||||||
|
class="btn-primary p-4 rounded-full"
|
||||||
|
:disabled="!hasAudio"
|
||||||
|
>
|
||||||
|
<svg v-if="!isPlaying" class="h-8 w-8" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="h-8 w-8" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 002 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="fastForward"
|
||||||
|
class="p-2 rounded-full hover:bg-gray-200 transition-colors"
|
||||||
|
:disabled="!hasAudio"
|
||||||
|
>
|
||||||
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de progression -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex items-center justify-between text-sm text-gray-500 mb-2">
|
||||||
|
<span>{{ formatTime(currentTime) }}</span>
|
||||||
|
<span>{{ formatTime(duration) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full h-2 bg-gray-200 rounded-full cursor-pointer relative"
|
||||||
|
@click="seekTo"
|
||||||
|
ref="progressBar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full bg-black rounded-full transition-all duration-100"
|
||||||
|
:style="{ width: progress + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vitesse de lecture -->
|
||||||
|
<div class="flex items-center justify-center space-x-2">
|
||||||
|
<span class="text-sm text-gray-500">Vitesse:</span>
|
||||||
|
<select
|
||||||
|
v-model="playbackRate"
|
||||||
|
@change="updatePlaybackRate"
|
||||||
|
class="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:border-black"
|
||||||
|
:disabled="!hasAudio"
|
||||||
|
>
|
||||||
|
<option value="0.5">0.5x</option>
|
||||||
|
<option value="0.75">0.75x</option>
|
||||||
|
<option value="1">1x</option>
|
||||||
|
<option value="1.25">1.25x</option>
|
||||||
|
<option value="1.5">1.5x</option>
|
||||||
|
<option value="2">2x</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message si pas d'audio -->
|
||||||
|
<div v-if="!hasAudio" class="text-center text-gray-500 mt-4">
|
||||||
|
<p class="text-sm">Aucun fichier audio disponible pour ce texte</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio element (caché) -->
|
||||||
|
<audio
|
||||||
|
ref="audioPlayer"
|
||||||
|
@loadedmetadata="onLoadedMetadata"
|
||||||
|
@timeupdate="onTimeUpdate"
|
||||||
|
@ended="onEnded"
|
||||||
|
preload="metadata"
|
||||||
|
>
|
||||||
|
<source :src="audioSrc" type="audio/mpeg" v-if="audioSrc">
|
||||||
|
Votre navigateur ne supporte pas l'audio HTML5.
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AudioPlayer',
|
||||||
|
props: {
|
||||||
|
audioSrc: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const audioPlayer = ref(null)
|
||||||
|
const progressBar = ref(null)
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const volume = ref(70)
|
||||||
|
const playbackRate = ref(1)
|
||||||
|
|
||||||
|
const hasAudio = computed(() => !!props.audioSrc)
|
||||||
|
|
||||||
|
const progress = computed(() => {
|
||||||
|
if (duration.value === 0) return 0
|
||||||
|
return (currentTime.value / duration.value) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
if (isNaN(seconds)) return '00:00'
|
||||||
|
const mins = Math.floor(seconds / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (!audioPlayer.value || !hasAudio.value) return
|
||||||
|
|
||||||
|
if (isPlaying.value) {
|
||||||
|
audioPlayer.value.pause()
|
||||||
|
isPlaying.value = false
|
||||||
|
} else {
|
||||||
|
audioPlayer.value.play()
|
||||||
|
isPlaying.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewind = () => {
|
||||||
|
if (!audioPlayer.value) return
|
||||||
|
audioPlayer.value.currentTime = Math.max(0, audioPlayer.value.currentTime - 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fastForward = () => {
|
||||||
|
if (!audioPlayer.value) return
|
||||||
|
audioPlayer.value.currentTime = Math.min(duration.value, audioPlayer.value.currentTime + 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekTo = (event) => {
|
||||||
|
if (!audioPlayer.value || !progressBar.value) return
|
||||||
|
|
||||||
|
const rect = progressBar.value.getBoundingClientRect()
|
||||||
|
const clickX = event.clientX - rect.left
|
||||||
|
const percentage = clickX / rect.width
|
||||||
|
const newTime = percentage * duration.value
|
||||||
|
|
||||||
|
audioPlayer.value.currentTime = newTime
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVolume = () => {
|
||||||
|
if (!audioPlayer.value) return
|
||||||
|
audioPlayer.value.volume = volume.value / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePlaybackRate = () => {
|
||||||
|
if (!audioPlayer.value) return
|
||||||
|
audioPlayer.value.playbackRate = playbackRate.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoadedMetadata = () => {
|
||||||
|
if (audioPlayer.value) {
|
||||||
|
duration.value = audioPlayer.value.duration
|
||||||
|
updateVolume()
|
||||||
|
updatePlaybackRate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTimeUpdate = () => {
|
||||||
|
if (audioPlayer.value) {
|
||||||
|
currentTime.value = audioPlayer.value.currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEnded = () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
currentTime.value = 0
|
||||||
|
if (audioPlayer.value) {
|
||||||
|
audioPlayer.value.currentTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watcher pour recharger l'audio quand l'src change
|
||||||
|
watch(() => props.audioSrc, () => {
|
||||||
|
if (audioPlayer.value) {
|
||||||
|
isPlaying.value = false
|
||||||
|
currentTime.value = 0
|
||||||
|
duration.value = 0
|
||||||
|
if (props.audioSrc) {
|
||||||
|
audioPlayer.value.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (audioPlayer.value && props.audioSrc) {
|
||||||
|
audioPlayer.value.load()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
audioPlayer,
|
||||||
|
progressBar,
|
||||||
|
isPlaying,
|
||||||
|
currentTime,
|
||||||
|
duration,
|
||||||
|
volume,
|
||||||
|
playbackRate,
|
||||||
|
hasAudio,
|
||||||
|
progress,
|
||||||
|
formatTime,
|
||||||
|
togglePlay,
|
||||||
|
rewind,
|
||||||
|
fastForward,
|
||||||
|
seekTo,
|
||||||
|
updateVolume,
|
||||||
|
updatePlaybackRate,
|
||||||
|
onLoadedMetadata,
|
||||||
|
onTimeUpdate,
|
||||||
|
onEnded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
23
src/components/FooterComponent.vue
Normal file
23
src/components/FooterComponent.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<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="text-center">
|
||||||
|
<p class="text-gray-600 mb-2">
|
||||||
|
Site développé pour la préservation du patrimoine linguistique
|
||||||
|
</p>
|
||||||
|
<p class="text-black font-medium">
|
||||||
|
© 2025 Association du Patois Franco-Provençal
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">
|
||||||
|
Tous droits réservés - Conservation et transmission de notre langue régionale
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FooterComponent'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
43
src/components/HelloWorld.vue
Normal file
43
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
msg: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Check out
|
||||||
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||||
|
>create-vue</a
|
||||||
|
>, the official Vue + Vite starter
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Learn more about IDE Support for Vue in the
|
||||||
|
<a
|
||||||
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||||
|
target="_blank"
|
||||||
|
>Vue Docs Scaling up Guide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
139
src/components/NavigationBar.vue
Normal file
139
src/components/NavigationBar.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between items-center h-16">
|
||||||
|
<!-- Logo/Titre -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<router-link to="/" class="text-2xl font-bold text-black transition-colors" style="color: black; --hover-color: #6b7280;" onmouseover="this.style.color='#6b7280'" onmouseout="this.style.color='black'">
|
||||||
|
Patois Franco-Provençal
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation principale -->
|
||||||
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
|
<router-link
|
||||||
|
to="/"
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ 'active': $route.name === 'Home' }"
|
||||||
|
>
|
||||||
|
Accueil
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
to="/textes"
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ 'active': $route.name === 'Texts' }"
|
||||||
|
>
|
||||||
|
Textes
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
to="/au-hasard"
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ 'active': $route.name === 'Random' }"
|
||||||
|
>
|
||||||
|
Au Hasard
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<!-- Barre de recherche -->
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
@keyup.enter="performSearch"
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
class="w-64 px-4 py-2 pl-10 pr-4 text-sm border border-gray-300 rounded-full focus:outline-none focus:border-black focus:ring-1 focus:ring-black"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||||
|
<svg class="h-4 w-4 text-gray-400" 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"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu mobile -->
|
||||||
|
<div class="md:hidden">
|
||||||
|
<button
|
||||||
|
@click="mobileMenuOpen = !mobileMenuOpen"
|
||||||
|
class="text-black focus:outline-none" style="color: black; --hover-color: #6b7280;" onmouseover="this.style.color='#6b7280'" onmouseout="this.style.color='black'"
|
||||||
|
>
|
||||||
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu mobile déplié -->
|
||||||
|
<div v-if="mobileMenuOpen" class="md:hidden border-t border-gray-200 pt-4 pb-4">
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<router-link to="/" class="nav-link-mobile">Accueil</router-link>
|
||||||
|
<router-link to="/textes" class="nav-link-mobile">Textes</router-link>
|
||||||
|
<router-link to="/au-hasard" class="nav-link-mobile">Au Hasard</router-link>
|
||||||
|
<div class="pt-2">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
@keyup.enter="performSearch"
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
class="w-full px-4 py-2 text-sm border border-gray-300 rounded-full focus:outline-none focus:border-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NavigationBar',
|
||||||
|
setup() {
|
||||||
|
const router = useRouter()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const mobileMenuOpen = ref(false)
|
||||||
|
|
||||||
|
const performSearch = () => {
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
router.push({
|
||||||
|
name: 'Texts',
|
||||||
|
query: { search: searchQuery.value.trim() }
|
||||||
|
})
|
||||||
|
mobileMenuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
mobileMenuOpen,
|
||||||
|
performSearch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-link {
|
||||||
|
@apply font-medium transition-colors duration-200 pb-1;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
@apply text-black border-b-2 border-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-mobile {
|
||||||
|
@apply font-medium transition-colors duration-200 py-2;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-mobile:hover {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
195
src/components/TextDisplay.vue
Normal file
195
src/components/TextDisplay.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<!-- En-tête avec les titres -->
|
||||||
|
<div class="grid grid-cols-2 gap-8 mb-6 pb-4 border-b border-gray-200">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">Patois Franco-Provençal</h3>
|
||||||
|
<p class="text-sm mt-1" style="color: #6b7280">{{ metadata?.titre_pt || 'Texte en patois' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">Français</h3>
|
||||||
|
<p class="text-sm mt-1" style="color: #6b7280">{{ metadata?.titre_fr || 'Texte en français' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenu des textes alignés -->
|
||||||
|
<div class="grid grid-cols-2 gap-8">
|
||||||
|
<!-- Colonne Patois -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in patoisLines"
|
||||||
|
:key="`patois-${index}`"
|
||||||
|
class="text-patois py-2 px-3 rounded hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
:class="{ 'bg-yellow-50 border-l-4 border-yellow-400': highlightedLine === index }"
|
||||||
|
@click="highlightLine(index)"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonne Français -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in frenchLines"
|
||||||
|
:key="`french-${index}`"
|
||||||
|
class="text-french py-2 px-3 rounded hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
:class="{ 'bg-yellow-50 border-l-4 border-yellow-400': highlightedLine === index }"
|
||||||
|
@click="highlightLine(index)"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations supplémentaires -->
|
||||||
|
<div v-if="metadata" class="mt-8 pt-6 border-t border-gray-200">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div v-if="metadata.auteur" class="flex items-center space-x-2">
|
||||||
|
<span class="font-medium" style="color: #6b7280">Auteur:</span>
|
||||||
|
<span style="color: #6b7280">{{ metadata.auteur }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="metadata.traducteur" class="flex items-center space-x-2">
|
||||||
|
<span class="font-medium" style="color: #6b7280">Traducteur:</span>
|
||||||
|
<span style="color: #6b7280">{{ metadata.traducteur }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="metadata.categorie" class="flex items-center space-x-2">
|
||||||
|
<span class="font-medium" style="color: #6b7280">Catégorie:</span>
|
||||||
|
<span style="color: #6b7280">{{ metadata.categorie }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="metadata.difficulte" class="flex items-center space-x-2">
|
||||||
|
<span class="font-medium" style="color: #6b7280">Difficulté:</span>
|
||||||
|
<span class="px-2 py-1 rounded-full text-xs" :class="getDifficultyClass(metadata.difficulte)">
|
||||||
|
{{ metadata.difficulte }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="metadata.date_creation" class="flex items-center space-x-2">
|
||||||
|
<span class="font-medium" style="color: #6b7280">Date:</span>
|
||||||
|
<span style="color: #6b7280">{{ formatDate(metadata.date_creation) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="mt-6 flex justify-center space-x-4">
|
||||||
|
<button
|
||||||
|
@click="copyPatoisText"
|
||||||
|
class="btn-secondary text-sm"
|
||||||
|
>
|
||||||
|
Copier le texte patois
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="copyFrenchText"
|
||||||
|
class="btn-secondary text-sm"
|
||||||
|
>
|
||||||
|
Copier le texte français
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextDisplay',
|
||||||
|
props: {
|
||||||
|
patoisText: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
frenchText: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const highlightedLine = ref(null)
|
||||||
|
|
||||||
|
// Diviser les textes en lignes
|
||||||
|
const patoisLines = computed(() => {
|
||||||
|
return props.patoisText
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const frenchLines = computed(() => {
|
||||||
|
return props.frenchText
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fonctions utilitaires
|
||||||
|
const highlightLine = (index) => {
|
||||||
|
highlightedLine.value = highlightedLine.value === index ? null : index
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('fr-FR')
|
||||||
|
} catch {
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyPatoisText = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.patoisText)
|
||||||
|
// Ici vous pourriez ajouter une notification de succès
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur lors de la copie:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyFrenchText = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.frenchText)
|
||||||
|
// Ici vous pourriez ajouter une notification de succès
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur lors de la copie:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
highlightedLine,
|
||||||
|
patoisLines,
|
||||||
|
frenchLines,
|
||||||
|
highlightLine,
|
||||||
|
getDifficultyClass,
|
||||||
|
formatDate,
|
||||||
|
copyPatoisText,
|
||||||
|
copyFrenchText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Assurer que les lignes correspondent visuellement */
|
||||||
|
.text-patois,
|
||||||
|
.text-french {
|
||||||
|
min-height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
src/main.js
Normal file
6
src/main.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
createApp(App).use(router).mount('#app')
|
||||||
44
src/router/index.js
Normal file
44
src/router/index.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
import Texts from '../views/Texts.vue'
|
||||||
|
import TextReader from '../views/TextReader.vue'
|
||||||
|
import { textService } from '../services/textService.js'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/textes',
|
||||||
|
name: 'Texts',
|
||||||
|
component: Texts
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/texte/:id',
|
||||||
|
name: 'TextReader',
|
||||||
|
component: TextReader,
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/au-hasard',
|
||||||
|
name: 'Random',
|
||||||
|
beforeEnter: async (to, from, next) => {
|
||||||
|
try {
|
||||||
|
const randomText = await textService.getRandomText()
|
||||||
|
next(`/texte/${randomText.id}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la redirection vers un texte aléatoire:', error)
|
||||||
|
next('/textes')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
148
src/services/textService.js
Normal file
148
src/services/textService.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Service pour communiquer avec l'API backend des textes en patois
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://localhost:3001/api'
|
||||||
|
|
||||||
|
export class TextService {
|
||||||
|
constructor() {
|
||||||
|
this.cache = new Map()
|
||||||
|
this.CACHE_DURATION = 5 * 60 * 1000 // 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effectue une requête HTTP vers l'API
|
||||||
|
*/
|
||||||
|
async fetchAPI(endpoint, options = {}) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(errorData.error || `Erreur HTTP: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur API ${endpoint}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge un texte spécifique par son ID
|
||||||
|
*/
|
||||||
|
async loadText(textId) {
|
||||||
|
const cacheKey = `text-${textId}`
|
||||||
|
|
||||||
|
// Vérifier le cache
|
||||||
|
if (this.cache.has(cacheKey)) {
|
||||||
|
const cached = this.cache.get(cacheKey)
|
||||||
|
if (Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
||||||
|
return cached.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const textData = await this.fetchAPI(`/texts/${textId}`)
|
||||||
|
|
||||||
|
// Mettre en cache
|
||||||
|
this.cache.set(cacheKey, {
|
||||||
|
data: textData,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
return textData
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Impossible de charger le texte "${textId}": ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ni
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche dans les textes
|
||||||
|
const cacheKey = 'all-texts'
|
||||||
|
|
||||||
|
// Vérifier le cache
|
||||||
|
if (this.cache.has(cacheKey)) {
|
||||||
|
const cached = this.cache.get(cacheKey)
|
||||||
|
if (Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
||||||
|
return cached.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const texts = await this.fetchAPI('/texts')
|
||||||
|
|
||||||
|
// Mettre en cache
|
||||||
|
this.cache.set(cacheKey, {
|
||||||
|
data: texts,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
text.metadata.titre_pt?.toLowerCase().includes(searchTerm) ||
|
||||||
|
text.frenchText?.toLowerCase().includes(searchTerm) ||
|
||||||
|
)
|
||||||
|
throw new Error(`Impossible de charger la liste des textes: ${error.message}`)
|
||||||
|
|
||||||
|
// Filtres
|
||||||
|
if (filters.category) {
|
||||||
|
results = results.filter(text => text.metadata.categorie === filters.category)
|
||||||
|
* Recherche dans les textes avec filtres
|
||||||
|
|
||||||
|
if (filters.difficulty) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
if (query && query.trim()) {
|
||||||
|
params.append('search', query.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.category) {
|
||||||
|
params.append('category', filters.category)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.difficulty) {
|
||||||
|
params.append('difficulty', filters.difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.onlyWithAudio) {
|
||||||
|
params.append('onlyWithAudio', 'true')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const endpoint = params.toString() ? `/texts?${params.toString()}` : '/texts'
|
||||||
|
return await this.fetchAPI(endpoint)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Erreur lors de la recherche: ${error.message}`)
|
||||||
|
}
|
||||||
|
const authors = new Set()
|
||||||
|
const categories = new Set()
|
||||||
|
let withAudio = 0
|
||||||
|
|
||||||
|
for (const text of this.textsList) {
|
||||||
|
if (text.metadata.auteur) authors.add(text.metadata.auteur)
|
||||||
|
try {
|
||||||
|
return await this.fetchAPI('/random')
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Impossible d'obtenir un texte aléatoire: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton
|
||||||
|
export const textService = new TextService()
|
||||||
|
|
||||||
|
const cacheKey = 'stats'
|
||||||
|
|
||||||
|
// Vérifier le cache
|
||||||
|
if (this.cache.has(cacheKey)) {
|
||||||
|
const cached = this.cache.get(cacheKey)
|
||||||
|
if (Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
||||||
|
return cached.data
|
||||||
|
}
|
||||||
33
src/style.css
Normal file
33
src/style.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-gray-custom: #6b7280;
|
||||||
|
--color-gray-custom-light: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
@apply bg-white text-black leading-relaxed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-black text-white px-6 py-3 rounded-full hover:bg-gray-800 transition-colors duration-200 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply border-2 border-black text-black px-6 py-3 rounded-full hover:bg-black hover:text-white transition-all duration-200 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-patois {
|
||||||
|
@apply font-medium text-lg leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-french {
|
||||||
|
@apply font-normal text-lg leading-relaxed;
|
||||||
|
color: var(--color-gray-custom);
|
||||||
|
}
|
||||||
|
}
|
||||||
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>
|
||||||
15
tailwind.config.js
Normal file
15
tailwind.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
'sans': ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
11
texts/exemple-conte/fr.txt
Normal file
11
texts/exemple-conte/fr.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Le Renard et le Corbeau
|
||||||
|
Il était une fois un corbeau qui avait trouvé un morceau de fromage.
|
||||||
|
Il se percha sur une branche d'arbre pour le manger tranquillement.
|
||||||
|
Un renard rusé passa par là et sentit l'odeur du fromage.
|
||||||
|
Il leva les yeux et vit le corbeau avec son trésor.
|
||||||
|
Le renard commença à flatter le corbeau sur sa beauté.
|
||||||
|
Il lui dit que sa voix devait être aussi belle que son plumage.
|
||||||
|
Le corbeau, flatté, ouvrit le bec pour chanter.
|
||||||
|
Le fromage tomba directement dans la gueule du renard.
|
||||||
|
Le renard s'enfuit en remerciant le corbeau pour ce repas.
|
||||||
|
|
||||||
8
texts/exemple-conte/metadata.txt
Normal file
8
texts/exemple-conte/metadata.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
titre_fr=Le Renard et le Corbeau
|
||||||
|
titre_pt=Le Renat et le Corbeau
|
||||||
|
auteur=Jean de La Fontaine (adaptation)
|
||||||
|
traducteur=Exemple de traduction
|
||||||
|
categorie=Conte
|
||||||
|
difficulte=Facile
|
||||||
|
date_creation=2025-09-26
|
||||||
|
|
||||||
11
texts/exemple-conte/pt.txt
Normal file
11
texts/exemple-conte/pt.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Le Renat et le Corbeau
|
||||||
|
Y avait una fés un corbeau qu'avait trovâ un morciau de fromâjo.
|
||||||
|
Se perchit sur una brancha d'âbro por le mangiér tranquillament.
|
||||||
|
Un renat rusâ passit per lâ et sentit l'odour du fromâjo.
|
||||||
|
Levit los uelhs et vit le corbeau avouéc son trésôr.
|
||||||
|
Le renat comencit a flatar le corbeau sur sa biôtât.
|
||||||
|
Li dit que sa vouéx deviat étre aussi bèla que son plumâjo.
|
||||||
|
Le corbeau, flatâ, durbrit le bec por chantar.
|
||||||
|
Le fromâjo tombît directament dens la goula du renat.
|
||||||
|
Le renat s'enfugit en remerciant le corbeau por cél repas.
|
||||||
|
|
||||||
BIN
texts/la-vatse-a-metcha/audio.mp3
Normal file
BIN
texts/la-vatse-a-metcha/audio.mp3
Normal file
Binary file not shown.
7
texts/la-vatse-a-metcha/fr.txt
Normal file
7
texts/la-vatse-a-metcha/fr.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
l était une fois deux frères, Pierino et Maurice, qui habitaient le même village, dans deux maisons proches l'une de l'autre. Un jour Pierino propose : « Qu’en penses-tu si on rassemblait nos épargnes et que l’on achetait une vache ? ». Maurice accepte et le lendemain ils se mettent en route pour aller à la foire du village voisin.
|
||||||
|
En marchant Pierino, qui se croyait bien plus malin que Maurice, pense : « Je te mettrais au pas ».
|
||||||
|
A la foire, après avoir bien examiné toutes les vaches, des pie noirs à la tête blanche jusqu’aux châtaines, des reines jusqu’aux vaches à lait, des vaches aux cornes bouclées, jusqu’à celles aux cornes retournées vers le haut, des vaches aux épaules abaissées jusqu’à celles à l’échine courbée, ils en choisissent une, Lenetta, une pie-rouge bien grasse et la payent avec l'argent qu'ils avaient mis en commun.
|
||||||
|
Quand la foire est finie, les deux frères retournent à la maison : « On l’amènera dans mon étable, elle est plus grande et sèche », dit Pierino. Mais ils commencent tout de suite à discuter pour savoir dans quelle étable mettre la vache.
|
||||||
|
Après s’être bien disputé tout le long du chemin : « Voilà la solution, on va attacher la vache au milieu d'un pré situé entre nos maisons ».
|
||||||
|
Après cela, les deux frères décident aussi de se partager la vache. Pierino, le plus intelligent, veut le devant de la bête : « Je ne me salirai pas les mains et j’aurai moins de travail ».
|
||||||
|
Donc il reste le derrière pour Maurice. « Tu croyais être le plus rusé - pense Maurice - tu as voulu la partie de la tête, maintenant il faut que tu ailles chercher du foin et de l'eau pour lui donner à manger et à boire…alors que tu n’auras aucun gain; tandis que moi j’irai chercher un seau pour traire la vache et vendre mon bon lait ».
|
||||||
8
texts/la-vatse-a-metcha/metadata.txt
Normal file
8
texts/la-vatse-a-metcha/metadata.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
titre_fr=La vache partagée
|
||||||
|
titre_pt=La vatse a métchà
|
||||||
|
auteur=Rita Decime
|
||||||
|
traducteur= N/A
|
||||||
|
categorie=Conte
|
||||||
|
difficulte=Difficile
|
||||||
|
date_creation=1984
|
||||||
|
|
||||||
7
texts/la-vatse-a-metcha/pt.txt
Normal file
7
texts/la-vatse-a-metcha/pt.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Y ave én queu do frae, Piérino é Morise, qu’ittévon ou mémo veladzo, dedeun do métcho én protcho dé l'otro. Én dzor Piérino propeuze : « Qué te nen di si no betissan énsembio lé seu é n’atsétissan eunna vatse ? ». Morise asette é lo dzor apré i parton a la féa dou veladzo vezén.
|
||||||
|
Én tsemenèn Piérino, qu’i ché crèyéve bièn peu fén qué Morise, pense : « Té fézo poué vére mè éa ! ».
|
||||||
|
A la féa, apré avé bièn avété totte lé vatse, di boutchanoye i tsatagnaye, di réne di corne i réne dou lasé, di vatse di corne boquie a selle di corne rébécoye, di vatse épaloye, i vatse couerbe, i nen cherdon eunna, Lenetta, na biantse é rodze bièn grâsa é la payon avoué lé seu qué y avon betó énsembio.
|
||||||
|
A la fén dé la féa, lé do frae tornon i métcho : « No la portèn poué a l’éteu dé mè, y é peu lardzo é peu sec qué lo tén », i di Piérino ; ma i coménson to sebeu a ché ruzà pé désidà ioù ivernà la vatse.
|
||||||
|
Apré avé bièn désquetó to lo lon dou tsemén : « Vouèlà la solechón, gropèn poué la vatse ou verzé éntrémé di do métcho ».
|
||||||
|
Dé sen, lé do frae désidon fénque dé ché partadzé én do la vatse. Piérino, lo peu savèn, i vou lo moro : « Paé, m’émpouertso po poué lé man é n’é poué mouèn dé travai ».
|
||||||
|
A Morise, adón, i reste lo déré : « Te crèyéve d’étre lo peu fén, t’o voulù la téta, éa i té fo alà tsertsé dé fen é d’éve pé lle bayé rodzé é bée é... sensa gnen lle gagné ! Mè, i contréo, veu poué mé tsertsé én sezelén pé biètsé la vatse é dze pouì poué vendre dé bon lasé ».
|
||||||
7
vite.config.js
Normal file
7
vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user