add interactions to comments
This commit is contained in:
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"cSpell.words": [
|
|
||||||
"Charte",
|
|
||||||
"Nuxt",
|
|
||||||
"supabase"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
13
components/Loader.vue
Normal file
13
components/Loader.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<v-container style="height: 600px;">
|
||||||
|
<v-row align-content="center" class="fill-height" justify="center">
|
||||||
|
<v-col class="text-l text-center text-semibold text-uppercase " cols="12">
|
||||||
|
Soyez le bienvenue sur DAV
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-progress-linear color="deep-purple-accent-4" height="6" indeterminate
|
||||||
|
rounded></v-progress-linear>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
73
lib/fetchers.js
Normal file
73
lib/fetchers.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and update articles data with likes and dislikes.
|
||||||
|
* @param {Ref<Array>} articles - Reactive array of articles.
|
||||||
|
*/
|
||||||
|
export const fetchArticlesData = async (articles) => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/api/articles`, { method: 'GET' });
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
articles.value = articles.value.map((article) => {
|
||||||
|
const updatedArticle = response.data.find((data) => data.id === article.id);
|
||||||
|
return updatedArticle
|
||||||
|
? { ...article, likes: updatedArticle.likes, dislikes: updatedArticle.dislikes }
|
||||||
|
: article;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch articles:', response.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching articles data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and attach comments to their respective articles.
|
||||||
|
* @param {Ref<Array>} articles - Reactive array of articles.
|
||||||
|
*/
|
||||||
|
export const fetchCommentsData = async (articles) => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/api/comments`, { method: 'GET' });
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
articles.value = articles.value.map((article) => {
|
||||||
|
const relatedComments = response.data.filter((data) => data.articleId === article.id);
|
||||||
|
return { ...article, comments: relatedComments };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch comments:', response.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching comments data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and format signatures data.
|
||||||
|
* @param {Ref<Array>} signatures - Reactive array of signatures.
|
||||||
|
*/
|
||||||
|
export const fetchSignatures = async (signatures) => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/api/charte`, { method: 'GET' });
|
||||||
|
|
||||||
|
if (response.success && Array.isArray(response.data)) {
|
||||||
|
signatures.value = response.data.map((signature) => ({
|
||||||
|
...signature,
|
||||||
|
createdAt: new Intl.DateTimeFormat('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(new Date(signature.createdAt)),
|
||||||
|
}));
|
||||||
|
} else if (response.success) {
|
||||||
|
console.error('Unexpected data format:', response.data);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch signatures:', response.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching signatures data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
54
lib/likes.ts
Normal file
54
lib/likes.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Updates likes or dislikes for an article or comment dynamically.
|
||||||
|
* @param {string} type - 'article' or 'comment'.
|
||||||
|
* @param {string} action - 'like' or 'dislike'.
|
||||||
|
* @param {string} id - ID of the article or comment.
|
||||||
|
* @param {string} articleId - (Optional) ID of the parent article if updating a comment.
|
||||||
|
* @param {Array} articles - The articles array.
|
||||||
|
*/
|
||||||
|
export const updateLikeDislike = async ({ type, action, id, articleId = null, articles }) => {
|
||||||
|
try {
|
||||||
|
// Find the target item
|
||||||
|
let target;
|
||||||
|
if (type === 'article') {
|
||||||
|
target = articles.value.find((a) => a.id === id);
|
||||||
|
} else if (type === 'comment') {
|
||||||
|
const article = articles.value.find((a) => a.id === articleId);
|
||||||
|
if (article) {
|
||||||
|
target = article.comments.find((c) => c.id === id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
console.error(`Target ${type} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the likes or dislikes
|
||||||
|
if (action === 'like') {
|
||||||
|
target.likes += 1;
|
||||||
|
} else if (action === 'dislike') {
|
||||||
|
target.dislikes += 1;
|
||||||
|
} else {
|
||||||
|
console.error('Invalid action specified');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine API endpoint
|
||||||
|
const endpoint = type === 'article' ? `/api/articles/${id}` : `/api/comments/${id}`;
|
||||||
|
|
||||||
|
// Make the API call
|
||||||
|
await $fetch(endpoint, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: {
|
||||||
|
[action === 'like' ? 'likes' : 'dislikes']: action === 'like' ? target.likes : target.dislikes,
|
||||||
|
...(type === 'comment' && { articleId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${type} ${action} updated successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating ${type} ${action}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
21
lib/overlayHandler.js
Normal file
21
lib/overlayHandler.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Display overlay with dynamic parameters.
|
||||||
|
* @param {Object} options - Overlay configuration.
|
||||||
|
* @param {string} options.icon - Icon to display in the overlay.
|
||||||
|
* @param {string} options.message - Message to display in the overlay.
|
||||||
|
* @param {string} options.buttonText - Button text to display in the overlay.
|
||||||
|
* @param {string} options.iconColor - Icon color to display in the overlay.
|
||||||
|
* @param {Ref<boolean>} overlay - Reactive overlay state.
|
||||||
|
*/
|
||||||
|
export const showOverlay = (options, overlay) => {
|
||||||
|
const { icon, message, buttonText, iconColor } = options;
|
||||||
|
|
||||||
|
overlay.value = {
|
||||||
|
icon,
|
||||||
|
message,
|
||||||
|
buttonText,
|
||||||
|
iconColor,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,29 +1,34 @@
|
|||||||
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
import { defineNuxtConfig } from 'nuxt/config';
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
ssr: false,
|
||||||
compatibilityDate: '2024-11-01',
|
compatibilityDate: '2024-11-01',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
title: 'DAV',
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
content: `Droits de l'ame et du vivant`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
css: [
|
css: [
|
||||||
'~/assets/css/main.css',
|
'~/assets/css/main.css',
|
||||||
],
|
],
|
||||||
|
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
// build: {
|
||||||
transpile: ['vuetify'],
|
// transpile: ['vuetify'],
|
||||||
},
|
// },
|
||||||
modules: [
|
modules: [
|
||||||
(_options, nuxt) => {
|
'vuetify-nuxt-module',
|
||||||
nuxt.hooks.hook('vite:extendConfig', (config) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
config.plugins.push(vuetify({ autoImport: true }))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
'@nuxt/content',
|
'@nuxt/content',
|
||||||
'@nuxtjs/supabase',
|
'@nuxtjs/supabase',
|
||||||
"@prisma/nuxt"
|
"@prisma/nuxt"
|
||||||
@@ -34,12 +39,37 @@ export default defineNuxtConfig({
|
|||||||
key: process.env.SUPABASE_KEY,
|
key: process.env.SUPABASE_KEY,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
},
|
},
|
||||||
|
vuetify: {
|
||||||
vite: {
|
vuetifyOptions: {
|
||||||
vue: {
|
components: 'VBtn',
|
||||||
template: {
|
theme: {
|
||||||
transformAssetUrls,
|
themes: {
|
||||||
|
light: {
|
||||||
|
colors: {
|
||||||
|
primary: '#A7D129',
|
||||||
|
secondary: '#686D76',
|
||||||
|
tertiary: '#BE3737',
|
||||||
|
tangerine: '#EC8F67ff',
|
||||||
|
accent: '#000000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
VBtnValid: 'VBtn',
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
VBtn: {
|
||||||
|
color: 'accent',
|
||||||
|
class: 'custom-btn',
|
||||||
|
},
|
||||||
|
VBtnValid: {
|
||||||
|
color: 'primary',
|
||||||
|
class: 'custom-btn',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
vite: {
|
||||||
|
},
|
||||||
|
})
|
||||||
6140
package-lock.json
generated
6140
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -10,22 +10,20 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "^7.4.47",
|
|
||||||
"@nuxt/content": "^2.13.4",
|
"@nuxt/content": "^2.13.4",
|
||||||
"@nuxtjs/supabase": "^1.4.4",
|
"@nuxtjs/supabase": "^1.4.4",
|
||||||
"@prisma/nuxt": "^0.1.3",
|
"@prisma/nuxt": "^0.1.3",
|
||||||
"nuxt": "^3.14.1592",
|
"nuxt": "^3.14.1592",
|
||||||
"uuid": "^11.0.3",
|
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest"
|
"vue-router": "latest",
|
||||||
|
"vuetify": "^3.7.6",
|
||||||
|
"vuetify-nuxt-module": "^0.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prisma": "^6.0.1",
|
"prisma": "^6.1.0",
|
||||||
"tailwindcss": "^3.4.16",
|
"tailwindcss": "^3.4.16"
|
||||||
"vite-plugin-vuetify": "^2.0.4",
|
|
||||||
"vuetify": "^3.7.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
719
pages/index.vue
719
pages/index.vue
@@ -1,197 +1,245 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid class="lg:mx-12 mx-2">
|
<v-container fluid>
|
||||||
<v-row class="video-section">
|
<!-- Loading Section -->
|
||||||
<video autoplay loop muted playsinline class="video-background">
|
<v-row v-if="isLoading" class="justify-center">
|
||||||
<source src="/videos/video.mp4" type="video/mp4" />
|
<Loader />
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
<!-- Introduction section -->
|
|
||||||
<v-row v-if="introduction" class="d-flex justify-center align-center text-center">
|
|
||||||
<h1 class="my-8 lg:text-xl text-l text-black">{{ introduction.title }}</h1>
|
|
||||||
<p class="text-xs text-black mb-8 mr-2 lg:mr-8 text-justify">{{ introduction.description }}</p>
|
|
||||||
</v-row>
|
|
||||||
<!-- Articles Section -->
|
|
||||||
<v-row>
|
|
||||||
<!-- Left Column -->
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<div v-for="(article, index) in leftColumnArticles" :key="article.id || index" class="article">
|
|
||||||
<v-card class="mb-12" elevation="0">
|
|
||||||
<v-card-title>
|
|
||||||
<h2 class="lg:text-sm text-xs text-wrap text-black font-bold">{{ article.title }}</h2>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<p class="text-xs text-black">{{ article.description }}</p>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn icon="mdi-play" @click="toggleShow(article.id)">
|
|
||||||
</v-btn>
|
|
||||||
<p class="text-gray-700 lg:text-xs text-xs">Suggérer une modification de texte</p>
|
|
||||||
</v-card-actions>
|
|
||||||
|
|
||||||
<v-expand-transition>
|
<!-- Main Content Section -->
|
||||||
<div v-show="showStates[article.id]">
|
<v-row v-else class="mx-5">
|
||||||
<v-divider></v-divider>
|
<v-row class="video-section">
|
||||||
<p class="text-gray-500 text-xs mx-5"> La constitution des DAV ne se fera pas
|
<video autoplay loop muted playsinline class="video-background">
|
||||||
seule. Chacun et
|
<source src="/videos/video.mp4" type="video/mp4" />
|
||||||
chacune sont
|
Your browser does not support the video tag.
|
||||||
invité(e)s à donner le meilleur d’eux même pour assurer sa constitution,
|
</video>
|
||||||
son respect, ainsi que sa mise en application.
|
</v-row>
|
||||||
Soyez courtois . Tout commentaire désobligeant irrespectueux ou agressif sera
|
<!-- Introduction section -->
|
||||||
systématiquement supprimé. </p>
|
<v-row v-if="introduction" class="d-flex justify-center align-center text-center">
|
||||||
|
<h1 class="my-8 lg:text-xl text-l text-black">{{ introduction.title }}</h1>
|
||||||
|
<p class="text-xs text-black sm:mb-24 mr-2 lg:mr-8 text-justify">{{ introduction.description }}</p>
|
||||||
|
</v-row>
|
||||||
|
<!-- Articles Section -->
|
||||||
|
<v-row>
|
||||||
|
<!-- Left Column -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div v-for="(article, index) in leftColumnArticles" :key="article.id || index" class="article">
|
||||||
|
<v-card class="mb-12" elevation="0">
|
||||||
|
<v-card-title>
|
||||||
|
<h2 class="lg:text-sm text-xs text-wrap text-black font-bold">{{ article.title }}</h2>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p class="text-xs text-black">{{ article.description }}</p>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn icon="mdi-play" @click="toggleShow(article.id)">
|
||||||
|
</v-btn>
|
||||||
|
<p class="text-gray-700 lg:text-xs text-xs">Suggérer une modification de texte</p>
|
||||||
|
</v-card-actions>
|
||||||
|
|
||||||
<!-- Comments Section -->
|
|
||||||
<v-card-text>
|
|
||||||
<v-textarea v-model="newComments[article.id]" outlined rows="2"
|
|
||||||
auto-grow></v-textarea>
|
|
||||||
<VBtnValid @click="addComment(article.id)">
|
|
||||||
Valider
|
|
||||||
</VBtnValid>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</v-expand-transition>
|
|
||||||
</v-card>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- Right Column -->
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<div v-for="(article, index) in rightColumnArticles" :key="article.id || index" class="article">
|
|
||||||
<v-card class="mb-12" elevation="0">
|
|
||||||
<v-card-title>
|
|
||||||
<h2 class="lg:text-sm text-xs text-wrap text-black font-bold">{{ article.title }}</h2>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<p class="text-xs text-black">{{ article.description }}</p>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn icon="mdi-play" @click="toggleShow(article.id)">
|
|
||||||
</v-btn>
|
|
||||||
<p class="text-gray-700 lg:text-xs text-xs">Suggérer une modification de texte</p>
|
|
||||||
</v-card-actions>
|
|
||||||
|
|
||||||
<v-expand-transition>
|
|
||||||
<div v-show="showStates[article.id]">
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<p class="text-gray-500 text-xs mx-5"> La constitution des DAV ne se fera pas
|
|
||||||
seule. Chacun et
|
|
||||||
chacune sont
|
|
||||||
invité(e)s à donner le meilleur d’eux même pour assurer sa constitution,
|
|
||||||
son respect, ainsi que sa mise en application.
|
|
||||||
Soyez courtois . Tout commentaire désobligeant irrespectueux ou agressif sera
|
|
||||||
systématiquement supprimé. </p>
|
|
||||||
|
|
||||||
<!-- Comments Section -->
|
|
||||||
<v-card-text>
|
|
||||||
<v-textarea v-model="newComments[article.id]" outlined rows="2"
|
|
||||||
auto-grow></v-textarea>
|
|
||||||
<VBtnValid @click="addComment(article.id)">
|
|
||||||
Valider
|
|
||||||
</VBtnValid>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</v-expand-transition>
|
|
||||||
</v-card>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-row v-if="summary" class="d-flex justify-center align-center text-center">
|
|
||||||
<p class="text-xs text-black mb-8 px-1 text-center">{{ summary.description }}</p>
|
|
||||||
</v-row>
|
|
||||||
<v-row class="justify-center">
|
|
||||||
<v-col cols="12" md="6" lg="8"> <!-- Make it take more space on large screens -->
|
|
||||||
<v-expansion-panels variant="accordion">
|
|
||||||
<v-expansion-panel v-for="(article, index) in articles" :key="article.id" class="article">
|
|
||||||
<v-expansion-panel-title class="d-flex flex-column align-start" expand-icon="" collapse-icon="">
|
|
||||||
<div class="d-flex align-center mb-2">
|
|
||||||
<span class="mr-2">
|
|
||||||
<v-icon class="m-1 text-primary hover:text-green-500 text-xs"
|
|
||||||
@click.stop="likeArticle(article.id)">
|
|
||||||
mdi-thumb-up
|
|
||||||
</v-icon>
|
|
||||||
<p class="ml-2 lg:ml-0 text-primary">{{ article.likes }}</p>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="mr-3">
|
|
||||||
<v-icon class="m-1 text-tertiary hover:text-red-500 text-xs"
|
|
||||||
@click.stop="dislikeArticle(article.id)">
|
|
||||||
mdi-thumb-down
|
|
||||||
</v-icon>
|
|
||||||
<p class="ml-2 lg:ml-0 text-tertiary">{{ article.dislikes }}</p>
|
|
||||||
</span>
|
|
||||||
<h2 class="text-sm text-wrap text-black font-bold m-0">
|
|
||||||
{{ article.title }}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-center mt-2">
|
|
||||||
<!-- Control the panel opening -->
|
|
||||||
<v-icon class="mr-1">
|
|
||||||
mdi-play
|
|
||||||
</v-icon>
|
|
||||||
<p class="text-gray-700 text-xs ml-2">Voir les suggestions</p>
|
|
||||||
</div>
|
|
||||||
</v-expansion-panel-title>
|
|
||||||
<v-expansion-panel-text>
|
|
||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
|
<div v-show="showStates[article.id]">
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<p class="text-gray-500 text-xs mx-5"> La constitution des DAV ne se fera pas
|
||||||
|
seule. Chacun et
|
||||||
|
chacune sont
|
||||||
|
invité(e)s à donner le meilleur d’eux même pour assurer sa constitution,
|
||||||
|
son respect, ainsi que sa mise en application.
|
||||||
|
Soyez courtois . Tout commentaire désobligeant irrespectueux ou agressif sera
|
||||||
|
systématiquement supprimé. </p>
|
||||||
|
|
||||||
|
<!-- Comments Section -->
|
||||||
|
<v-card-text>
|
||||||
|
<v-textarea v-model="newComments[article.id]" outlined rows="2"
|
||||||
|
auto-grow></v-textarea>
|
||||||
|
<VBtnValid @click="addComment(article.id)">
|
||||||
|
Valider
|
||||||
|
</VBtnValid>
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
</v-expand-transition>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div v-for="(article, index) in rightColumnArticles" :key="article.id || index" class="article">
|
||||||
|
<v-card class="mb-12" elevation="0">
|
||||||
|
<v-card-title>
|
||||||
|
<h2 class="lg:text-sm text-xs text-wrap text-black font-bold">{{ article.title }}</h2>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p class="text-xs text-black">{{ article.description }}</p>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn icon="mdi-play" @click="toggleShow(article.id)">
|
||||||
|
</v-btn>
|
||||||
|
<p class="text-gray-700 lg:text-xs text-xs">Suggérer une modification de texte</p>
|
||||||
|
</v-card-actions>
|
||||||
|
|
||||||
|
<v-expand-transition>
|
||||||
|
<div v-show="showStates[article.id]">
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<p class="text-gray-500 text-xs mx-5"> La constitution des DAV ne se fera pas
|
||||||
|
seule. Chacun et
|
||||||
|
chacune sont
|
||||||
|
invité(e)s à donner le meilleur d’eux même pour assurer sa constitution,
|
||||||
|
son respect, ainsi que sa mise en application.
|
||||||
|
Soyez courtois . Tout commentaire désobligeant irrespectueux ou agressif sera
|
||||||
|
systématiquement supprimé. </p>
|
||||||
|
|
||||||
|
<!-- Comments Section -->
|
||||||
|
<v-card-text>
|
||||||
|
<v-textarea v-model="newComments[article.id]" outlined rows="2"
|
||||||
|
auto-grow></v-textarea>
|
||||||
|
<VBtnValid @click="addComment(article.id)">
|
||||||
|
Valider
|
||||||
|
</VBtnValid>
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
</v-expand-transition>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="summary" class="d-flex justify-center align-center text-center">
|
||||||
|
<p class="text-xs text-black mb-8 px-1 text-center">{{ summary.description }}</p>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="justify-center">
|
||||||
|
<v-col cols="12" md="6" lg="8"> <!-- Make it take more space on large screens -->
|
||||||
|
<v-expansion-panels variant="accordion">
|
||||||
|
<v-expansion-panel v-for="(article, index) in articles" :key="article.id" class="article">
|
||||||
|
<v-expansion-panel-title class="d-flex flex-column align-start" expand-icon=""
|
||||||
|
collapse-icon="">
|
||||||
|
<div class="flex items-center mb-0">
|
||||||
|
<!-- Like/Dislike Section -->
|
||||||
|
<div class="mr-3 flex items-center">
|
||||||
|
<v-icon class="mr-1 text-primary text-xs" size="18"
|
||||||
|
@click.stop="likeArticle(article.id)">
|
||||||
|
mdi-thumb-up
|
||||||
|
</v-icon>
|
||||||
|
<p class="text-primary text-sm">{{ article.likes }}</p>
|
||||||
|
|
||||||
|
<v-icon class="mr-1 ml-2 text-tertiary text-xs" size="18"
|
||||||
|
@click.stop="dislikeArticle(article.id)">
|
||||||
|
mdi-thumb-down
|
||||||
|
</v-icon>
|
||||||
|
<p class="text-tertiary text-sm">{{ article.dislikes }}</p>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-sm text-wrap text-black font-bold m-0">
|
||||||
|
{{ article.title }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center mt-2">
|
||||||
|
<!-- Control the panel opening -->
|
||||||
|
<v-icon class="mr-1">
|
||||||
|
mdi-play
|
||||||
|
</v-icon>
|
||||||
|
<p class="text-gray-700 text-xs ml-2">Voir les suggestions</p>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
<v-timeline align="start" density="compact" class="ml-3">
|
<v-timeline align="start" density="compact" class="ml-3">
|
||||||
<v-timeline-item v-if="article.comments.length"
|
<v-timeline-item v-if="article.comments.length"
|
||||||
v-for="(comment, cIndex) in article.comments" :key="cIndex" dot-color="primary"
|
v-for="(comment, cIndex) in article.comments" :key="cIndex"
|
||||||
size="x-small">
|
dot-color="secondary" size="x-small">
|
||||||
<div class="mb-0">
|
<div class="flex items-center mb-0">
|
||||||
<p class="text-sm text-black">{{ comment }}</p>
|
<!-- Like/Dislike Section -->
|
||||||
|
<div class="mr-3 flex items-center">
|
||||||
|
<v-icon color="primary" size="14"
|
||||||
|
@click="likeComment(article.id, comment.id)">mdi-thumb-up</v-icon>
|
||||||
|
<span class="text-xs text-primary ml-1 mr-2">{{ comment.likes }}</span>
|
||||||
|
<v-icon color="tertiary" size="14"
|
||||||
|
@click="dislikeComment(article.id, comment.id)">mdi-thumb-down</v-icon>
|
||||||
|
<span class="text-xs text-tertiary ml-1">{{ comment.dislikes }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Comment Content -->
|
||||||
|
<p class="text-sm text-black">{{ comment.content }}</p>
|
||||||
</div>
|
</div>
|
||||||
</v-timeline-item>
|
</v-timeline-item>
|
||||||
|
|
||||||
|
<!-- No Comments Placeholder -->
|
||||||
<v-timeline-item v-else dot-color="secondary" size="x-small">
|
<v-timeline-item v-else dot-color="secondary" size="x-small">
|
||||||
<p class="text-sm text-black">Aucune suggestion pour cet article</p>
|
<p class="text-sm text-black">Aucune suggestion pour cet article</p>
|
||||||
</v-timeline-item>
|
</v-timeline-item>
|
||||||
</v-timeline>
|
</v-timeline>
|
||||||
</v-expand-transition>
|
</v-expansion-panel-text>
|
||||||
</v-expansion-panel-text>
|
|
||||||
</v-expansion-panel>
|
|
||||||
</v-expansion-panels>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Signature Section -->
|
</v-expansion-panel>
|
||||||
<v-row class="justify-center my-8">
|
</v-expansion-panels>
|
||||||
<v-col cols="12" md="8" lg="6">
|
</v-col>
|
||||||
<v-card elevation="3" class="pa-6 bg-gray-100" color="secondary">
|
</v-row>
|
||||||
<v-card-title class="text-center">
|
|
||||||
<h2 class="text-uppercase text-md font-semibold font-sans">Signer la Charte</h2>
|
|
||||||
</v-card-title>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-text>
|
|
||||||
<v-form @submit.prevent="submitSignature" lazy-validation class="text-xs font-sans">
|
|
||||||
<v-text-field v-model="name" label="Nom complet" outlined dense hide-details
|
|
||||||
:rules="[rules.required]" class="mb-2" prepend-inner-icon="mdi-account"></v-text-field>
|
|
||||||
<v-text-field v-model="email" type="email" label="Email" placeholder="Entrez votre email"
|
|
||||||
outlined dense hide-details :rules="[rules.required, rules.email]" class="mb-2"
|
|
||||||
prepend-inner-icon="mdi-email"></v-text-field>
|
|
||||||
<v-textarea v-model="comment" label="Ajouter un commentaire"
|
|
||||||
placeholder="Partagez vos réflexions ou suggestions" outlined dense hide-details
|
|
||||||
rows="2" class="mb-2" prepend-inner-icon="mdi-message-text"></v-textarea>
|
|
||||||
<VBtnValid :disabled="!isFormValid" type="submit" block>
|
|
||||||
<v-icon size="16" left>mdi-check-circle</v-icon>
|
|
||||||
Signer
|
|
||||||
</VBtnValid>
|
|
||||||
</v-form>
|
|
||||||
|
|
||||||
</v-card-text>
|
<!-- Signature Section -->
|
||||||
|
<v-row class="mr-2">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="3" class="pa-6 bg-gray-100" color="secondary">
|
||||||
|
<v-card-title class="text-center">
|
||||||
|
<h2 class="text-uppercase text-md font-semibold font-sans">Signer la Charte</h2>
|
||||||
|
</v-card-title>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="submitSignature" lazy-validation class="text-xs font-sans">
|
||||||
|
<v-text-field v-model="name" label="Nom / prénom / pseudonyme" outlined dense
|
||||||
|
hide-details :rules="[rules.required]" class="mb-2"
|
||||||
|
prepend-inner-icon="mdi-account"></v-text-field>
|
||||||
|
<v-text-field v-model="email" type="email" label="Email"
|
||||||
|
placeholder="Entrez votre email" outlined dense hide-details
|
||||||
|
:rules="[rules.required, rules.email]" class="mb-2"
|
||||||
|
prepend-inner-icon="mdi-email"></v-text-field>
|
||||||
|
<v-textarea v-model="comment" label="Ajouter un commentaire"
|
||||||
|
placeholder="Partagez vos réflexions ou suggestions" outlined dense hide-details
|
||||||
|
rows="2" class="mb-2" prepend-inner-icon="mdi-message-text"></v-textarea>
|
||||||
|
<VBtnValid type="submit" block>
|
||||||
|
<v-icon size="16" left>mdi-check-circle</v-icon>
|
||||||
|
Signer
|
||||||
|
</VBtnValid>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Signature Display Section -->
|
||||||
|
<v-row class=" sm:my-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-expansion-panels variant="accordion">
|
||||||
|
<v-expansion-panel>
|
||||||
|
<v-expansion-panel-title class="d-flex flex-column align-start" expand-icon=""
|
||||||
|
color="primary" collapse-icon="">
|
||||||
|
<div class="text-uppercase text-l font-semibold font-sans">voir les signatures</div>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<v-timeline align="start" density="compact">
|
||||||
|
<v-timeline-item v-for="signature in signatures" :key="signature.id"
|
||||||
|
dot-color="secondary" size="x-small">
|
||||||
|
<div>
|
||||||
|
<div class="font-weight-normal font-sans text-sm">
|
||||||
|
<strong>{{ signature.name }}</strong> @{{ signature.createdAt }}
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-normal font-sans text-sm">{{ signature.comment }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-timeline-item>
|
||||||
|
</v-timeline>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Success Overlay (Dynamic Content) -->
|
||||||
|
<v-overlay v-model="overlay.visible" class="d-flex align-center justify-center"
|
||||||
|
:style="{ background: 'rgba(0, 0, 0, 0.5)', backdropFilter: 'blur(4px)' }">
|
||||||
|
<v-card class="py-10 px-8 text-center" elevation="4" rounded="lg" style="max-width: 400px;">
|
||||||
|
<v-icon :color="overlay.iconColor" size="48" class="mb-4">{{ overlay.icon }}</v-icon>
|
||||||
|
<p class="overlay-msg text-sm font-semibold text-black text-uppercase">{{ overlay.message }}</p>
|
||||||
|
<VBtnValid class="mt-6" @click="overlay.visible = false">
|
||||||
|
{{ overlay.buttonText }}
|
||||||
|
</VBtnValid>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-overlay>
|
||||||
</v-row>
|
</v-row>
|
||||||
<!-- Success Overlay (Dynamic Content) -->
|
|
||||||
<v-overlay v-model="overlay" class="d-flex align-center justify-center"
|
|
||||||
:style="{ background: 'rgba(0, 0, 0, 0.5)', backdropFilter: 'blur(4px)' }">
|
|
||||||
<v-card class="py-10 px-8 text-center" elevation="4" rounded="lg" style="max-width: 400px;">
|
|
||||||
<v-icon :color="overlayIconColor" size="48" class="mb-4">{{ overlayIcon }}</v-icon>
|
|
||||||
<p class="overlay-msg text-sm font-semibold text-black text-uppercase">{{ overlayMessage }}</p>
|
|
||||||
<VBtnValid class="mt-6" @click="overlay = false">
|
|
||||||
{{ overlayButtonText }}
|
|
||||||
</VBtnValid>
|
|
||||||
</v-card>
|
|
||||||
</v-overlay>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -199,6 +247,11 @@
|
|||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useAsyncData } from 'nuxt/app';
|
import { useAsyncData } from 'nuxt/app';
|
||||||
|
|
||||||
|
import Loader from '~/components/Loader.vue';
|
||||||
|
import { updateLikeDislike } from '~/lib/likes.ts';
|
||||||
|
import { fetchArticlesData, fetchCommentsData, fetchSignatures } from '~/lib/fetchers';
|
||||||
|
import { showOverlay } from '~/lib/overlayHandler';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'default',
|
layout: 'default',
|
||||||
});
|
});
|
||||||
@@ -210,7 +263,6 @@ const rightColumnArticles = ref([]);
|
|||||||
const showStates = ref({});
|
const showStates = ref({});
|
||||||
const introduction = ref(null);
|
const introduction = ref(null);
|
||||||
const summary = ref(null);
|
const summary = ref(null);
|
||||||
|
|
||||||
const signatures = ref([]);
|
const signatures = ref([]);
|
||||||
// Form fields
|
// Form fields
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
@@ -218,12 +270,15 @@ const email = ref('');
|
|||||||
const comment = ref('');
|
const comment = ref('');
|
||||||
|
|
||||||
const isFormValid = computed(() => name.value && email.value);
|
const isFormValid = computed(() => name.value && email.value);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
|
||||||
const overlay = ref(false);
|
const overlay = ref({
|
||||||
const overlayIcon = ref('mdi-check-circle'); // Default icon
|
icon: '',
|
||||||
const overlayMessage = ref('Votre suggestion à été ajoutée avec succès'); // Default message
|
message: '',
|
||||||
const overlayButtonText = ref('Continuer'); // Default button text
|
buttonText: '',
|
||||||
const overlayIconColor = ref('success');
|
iconColor: '',
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
required: (value) => !!value || "Ce champ est obligatoire.",
|
required: (value) => !!value || "Ce champ est obligatoire.",
|
||||||
@@ -235,178 +290,190 @@ const toggleShow = (articleId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fetch articles data
|
// Fetch articles data
|
||||||
const { data: contentData, error } = await useAsyncData("content", () =>
|
const fetchContent = async () => {
|
||||||
queryContent().find()
|
try {
|
||||||
);
|
const { data: contentData, error } = await useAsyncData('content', () =>
|
||||||
|
queryContent().find()
|
||||||
|
);
|
||||||
|
|
||||||
// Process fetched content
|
// Process fetched content
|
||||||
if (!error.value && contentData.value) {
|
if (!error.value && contentData.value) {
|
||||||
for (const file of contentData.value) {
|
for (const file of contentData.value) {
|
||||||
if (file.type === "intro") {
|
if (file.type === "intro") {
|
||||||
introduction.value = file;
|
introduction.value = file;
|
||||||
} else if (file.type === "article") {
|
} else if (file.type === "article") {
|
||||||
try {
|
const article = {
|
||||||
// Add default values (likes, dislikes, comments) if missing
|
id: file.id,
|
||||||
const article = {
|
title: file.title,
|
||||||
id: file.id,
|
description: file.description,
|
||||||
title: file.title,
|
likes: file.likes || 0,
|
||||||
description: file.description,
|
dislikes: file.dislikes || 0,
|
||||||
likes: file.likes || 0,
|
comments: file.comments || [],
|
||||||
dislikes: file.dislikes || 0,
|
};
|
||||||
comments: file.comments || [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the article locally
|
// Add the article locally
|
||||||
articles.value.push(article);
|
articles.value.push(article);
|
||||||
|
|
||||||
await $fetch('/api/articles', {
|
await $fetch('/api/articles', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
id: article.id,
|
id: article.id,
|
||||||
title: article.title,
|
title: article.title,
|
||||||
description: article.description,
|
description: article.description,
|
||||||
likes: article.likes,
|
likes: article.likes,
|
||||||
dislikes: article.dislikes,
|
dislikes: article.dislikes
|
||||||
comments: article.comments,
|
},
|
||||||
},
|
});
|
||||||
});
|
} else if (file.type === "summary") {
|
||||||
} catch (err) {
|
summary.value = file;
|
||||||
console.error('Error creating article:', err);
|
}
|
||||||
}
|
}
|
||||||
} else if (file.type === "summary") {
|
|
||||||
summary.value = file;
|
|
||||||
}
|
}
|
||||||
|
// Split articles into left and right columns
|
||||||
|
leftColumnArticles.value = articles.value.filter((_, index) => index % 2 === 0);
|
||||||
|
rightColumnArticles.value = articles.value.filter((_, index) => index % 2 !== 0);
|
||||||
|
// Initialize newComments object based on article IDs
|
||||||
|
articles.value.forEach(article => {
|
||||||
|
newComments.value[article.id] = '';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
// Initialize newComments object based on article IDs
|
console.error('Error fetching content:', err);
|
||||||
articles.value.forEach(article => {
|
} finally {
|
||||||
newComments.value[article.id] = '';
|
isLoading.value = false;
|
||||||
});
|
}
|
||||||
|
};
|
||||||
// Split articles into two columns (assuming a 50% split)
|
|
||||||
const midIndex = Math.ceil(articles.value.length / 2);
|
|
||||||
leftColumnArticles.value = articles.value.slice(0, midIndex);
|
|
||||||
rightColumnArticles.value = articles.value.slice(midIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
await fetchContent()
|
||||||
const response = await $fetch(`/api/articles`, {
|
await fetchArticlesData(articles);
|
||||||
method: 'GET',
|
await fetchCommentsData(articles);
|
||||||
});
|
await fetchSignatures(signatures);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
|
||||||
// Update the articles array by merging data from the response
|
|
||||||
articles.value = articles.value.map((article) => {
|
|
||||||
const updatedArticle = response.data.find((data) => data.id === article.id);
|
|
||||||
return updatedArticle
|
|
||||||
? {
|
|
||||||
...article,
|
|
||||||
likes: updatedArticle.likes,
|
|
||||||
dislikes: updatedArticle.dislikes,
|
|
||||||
comments: updatedArticle.comments,
|
|
||||||
}
|
|
||||||
: article;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Failed to fetch articles:', response.message || 'Unknown error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching articles:', error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const likeArticle = (articleId) => {
|
||||||
|
updateLikeDislike({
|
||||||
|
type: 'article',
|
||||||
|
action: 'like',
|
||||||
|
id: articleId,
|
||||||
|
articles,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const likeArticle = async (articleId) => {
|
const dislikeArticle = (articleId) => {
|
||||||
const article = articles.value.find((a) => a.id === articleId);
|
updateLikeDislike({
|
||||||
if (!article) return;
|
type: 'article',
|
||||||
article.likes += 1;
|
action: 'dislike',
|
||||||
|
id: articleId,
|
||||||
|
articles,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
const likeComment = (articleId, commentId) => {
|
||||||
await $fetch(`/api/articles/${articleId}`, {
|
updateLikeDislike({
|
||||||
method: 'PUT',
|
type: 'comment',
|
||||||
body: { likes: article.likes },
|
action: 'like',
|
||||||
});
|
id: commentId,
|
||||||
|
articleId,
|
||||||
|
articles,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
console.log('Article updated successfully');
|
const dislikeComment = (articleId, commentId) => {
|
||||||
} catch (error) {
|
updateLikeDislike({
|
||||||
console.error('Error in likeArticle:', error);
|
type: 'comment',
|
||||||
}
|
action: 'dislike',
|
||||||
};;
|
id: commentId,
|
||||||
|
articleId,
|
||||||
|
articles,
|
||||||
const dislikeArticle = async (articleId) => {
|
});
|
||||||
const article = articles.value.find((a) => a.id === articleId);
|
|
||||||
if (!article) return;
|
|
||||||
article.dislikes += 1;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await $fetch(`/api/articles/${articleId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: { dislikes: article.dislikes },
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Article updated successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in dislikeArticle:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addComment = async (articleId) => {
|
const addComment = async (articleId) => {
|
||||||
const commentText = newComments.value[articleId]?.trim();
|
const commentText = newComments.value[articleId]?.trim();
|
||||||
if (!commentText) return;
|
if (!commentText) return;
|
||||||
|
|
||||||
const article = articles.value.find((a) => a.id === articleId);
|
|
||||||
article.comments.push(commentText);
|
|
||||||
newComments.value[articleId] = '';
|
|
||||||
|
|
||||||
overlayIcon.value = 'mdi-check-circle';
|
|
||||||
overlayMessage.value = `Merci pour votre suggestion. Veuillez la consulter tout en descendant vers le bas de la page`;
|
|
||||||
overlayButtonText.value = 'Continuer';
|
|
||||||
overlayIconColor.value = 'success';
|
|
||||||
overlay.value = true;
|
|
||||||
try {
|
try {
|
||||||
await $fetch(`/api/articles/${article.id}`, {
|
const response = await $fetch('/api/comments', {
|
||||||
method: 'PUT',
|
method: 'POST',
|
||||||
body: { comments: article.comments },
|
body: {
|
||||||
|
content: commentText,
|
||||||
|
articleId,
|
||||||
|
likes: 0,
|
||||||
|
dislikes: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Comment added successfully');
|
if (response.success) {
|
||||||
|
const article = articles.value.find((a) => a.id === articleId);
|
||||||
|
if (article) {
|
||||||
|
article.comments.push({
|
||||||
|
id: response.data.id,
|
||||||
|
content: commentText,
|
||||||
|
likes: 0,
|
||||||
|
dislikes: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the input field for the current article
|
||||||
|
newComments.value[articleId] = '';
|
||||||
|
|
||||||
|
// Display success overlay
|
||||||
|
showOverlay(
|
||||||
|
{
|
||||||
|
icon: 'mdi-check-circle',
|
||||||
|
message: `Merci pour votre suggestion. Veuillez la consulter tout en descendant vers le bas de la page.`,
|
||||||
|
buttonText: 'Continuer',
|
||||||
|
iconColor: 'success',
|
||||||
|
},
|
||||||
|
overlay
|
||||||
|
);
|
||||||
|
console.log('Comment added successfully');
|
||||||
|
} else {
|
||||||
|
console.error('Failed to add comment:', response.message);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in addComment:', error);
|
console.error('Error in addComment:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitSignature = async () => {
|
const submitSignature = async () => {
|
||||||
const signatureData = {
|
if (isFormValid.value) {
|
||||||
name: name.value,
|
const signatureData = {
|
||||||
email: email.value,
|
name: name.value,
|
||||||
comment: comment.value,
|
email: email.value,
|
||||||
};
|
comment: comment.value,
|
||||||
signatures.value.push({ ...signatureData });
|
};
|
||||||
try {
|
signatures.value.push({ ...signatureData });
|
||||||
await $fetch('/api/charte', {
|
try {
|
||||||
method: 'POST',
|
await $fetch('/api/charte', {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json'
|
headers: {
|
||||||
},
|
'Content-Type': 'application/json'
|
||||||
body: JSON.stringify(signatureData)
|
},
|
||||||
})
|
body: JSON.stringify(signatureData)
|
||||||
console.log('Charte is submitted successfully');
|
})
|
||||||
} catch (error) {
|
// Display success overlay using the reusable function
|
||||||
console.error('Error in submit signature:', error);
|
showOverlay(
|
||||||
}
|
{
|
||||||
overlayIcon.value = 'mdi-check-circle';
|
icon: 'mdi-check-circle',
|
||||||
overlayMessage.value = `Merci pour votre signature ${name.value}`
|
message: `Merci pour votre signature ${name.value}`,
|
||||||
overlayButtonText.value = 'Continuer';
|
buttonText: 'Continuer',
|
||||||
overlayIconColor.value = 'success';
|
iconColor: 'success',
|
||||||
overlay.value = true;
|
},
|
||||||
name.value = '';
|
overlay
|
||||||
email.value = '';
|
)
|
||||||
comment.value = '';
|
name.value = '';
|
||||||
|
email.value = '';
|
||||||
|
comment.value = '';
|
||||||
|
console.log('Charte is submitted successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in submit signature:', error);
|
||||||
|
}
|
||||||
|
} return;
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.video-section {
|
.video-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -451,4 +518,8 @@ p {
|
|||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-icon:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
// import this after install `@mdi/font` package
|
|
||||||
import '@mdi/font/css/materialdesignicons.css'
|
|
||||||
|
|
||||||
import 'vuetify/styles'
|
|
||||||
import { createVuetify } from 'vuetify'
|
|
||||||
import { VBtn } from 'vuetify/components';
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((app) => {
|
|
||||||
const vuetify = createVuetify({
|
|
||||||
theme: {
|
|
||||||
themes: {
|
|
||||||
light: {
|
|
||||||
colors: {
|
|
||||||
primary: '#A7D129',
|
|
||||||
secondary: '#686D76',
|
|
||||||
tertiary: '#BE3737',
|
|
||||||
tangerine: '#EC8F67ff',
|
|
||||||
accent: '#000000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
VBtnValid: VBtn,
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
VBtn: {
|
|
||||||
color: 'accent',
|
|
||||||
class: 'custom-btn',
|
|
||||||
},
|
|
||||||
VBtnValid: {
|
|
||||||
color: 'primary',
|
|
||||||
class: 'custom-btn',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
app.vueApp.use(vuetify)
|
|
||||||
})
|
|
||||||
24
prisma/migrations/20241222124646_dav/migration.sql
Normal file
24
prisma/migrations/20241222124646_dav/migration.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `comments` on the `Article` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Article" DROP COLUMN "comments";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Comment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"likes" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"dislikes" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"articleId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -8,14 +8,25 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Article {
|
model Article {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
title String
|
title String
|
||||||
description String
|
description String
|
||||||
likes Int @default(0)
|
likes Int @default(0)
|
||||||
dislikes Int @default(0)
|
dislikes Int @default(0)
|
||||||
comments String[] @default([])
|
comments Comment[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Comment {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
content String
|
||||||
|
likes Int @default(0)
|
||||||
|
dislikes Int @default(0)
|
||||||
|
articleId String // Foreign key for the related article
|
||||||
|
article Article @relation(fields: [articleId], references: [id]) // Define relationship
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Charte {
|
model Charte {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -2,14 +2,18 @@ import prisma from '~/lib/prisma';
|
|||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const id = event.context.params?.id;
|
const id = event.context.params?.id;
|
||||||
console.log('prisma' + event.context.params?.id)
|
|
||||||
try {
|
try {
|
||||||
const article = await prisma.article.findUnique({ where: { id } });
|
const article = await prisma.article.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { comments: true },
|
||||||
|
});
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
data: null,
|
data: null,
|
||||||
|
message: 'Article not found.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const id = event.context.params?.id;
|
const id = event.context.params?.id;
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
if (!id ) {
|
if (!id) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Invalid article ID.',
|
message: 'Invalid article ID.',
|
||||||
@@ -15,19 +15,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
const updatedArticle = await prisma.article.update({
|
const updatedArticle = await prisma.article.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
id: body.id,
|
|
||||||
title: body.title,
|
title: body.title,
|
||||||
description: body.description,
|
description: body.description,
|
||||||
likes: body.likes,
|
likes: body.likes,
|
||||||
dislikes: body.dislikes,
|
dislikes: body.dislikes,
|
||||||
comments: body.comments,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedArticle,
|
data: updatedArticle,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error updating article:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import prisma from '~/lib/prisma';
|
|||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const articles = await prisma.article.findMany();
|
const articles = await prisma.article.findMany({
|
||||||
|
include: { comments: true },
|
||||||
|
});
|
||||||
|
|
||||||
if (!articles || articles.length === 0) {
|
if (!articles || articles.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
data: null,
|
data: null,
|
||||||
message: 'No articles found.',
|
message: 'No articles found.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ import prisma from '~/lib/prisma';
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const { id, title, description, likes, dislikes, comments } = body;
|
const { title, description, likes, dislikes } = body;
|
||||||
|
|
||||||
const newArticle = await prisma.article.create({
|
const newArticle = await prisma.article.create({
|
||||||
data: {
|
data: {
|
||||||
id,
|
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
likes: likes || 0,
|
likes: likes || 0,
|
||||||
dislikes: dislikes || 0,
|
dislikes: dislikes || 0,
|
||||||
comments: comments || [],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
26
server/api/charte/index.get.ts
Normal file
26
server/api/charte/index.get.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import prisma from '~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const chartes = await prisma.charte.findMany();
|
||||||
|
|
||||||
|
if (!chartes || chartes.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: null,
|
||||||
|
message: 'No chartes found.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: chartes,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching chartes:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
30
server/api/comments/[id].delete.ts
Normal file
30
server/api/comments/[id].delete.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import prisma from '~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const id = event.context.params?.id;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid comment ID.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.comment.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Comment deleted successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting comment:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'An error occurred while deleting the comment',
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
37
server/api/comments/[id].put.ts
Normal file
37
server/api/comments/[id].put.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import prisma from '~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const id = event.context.params?.id;
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid comment ID.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedComment = await prisma.comment.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
content: body.content,
|
||||||
|
likes: body.likes,
|
||||||
|
dislikes: body.dislikes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Comment updated successfully',
|
||||||
|
data: updatedComment,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating comment:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'An error occurred while updating the comment',
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
24
server/api/comments/index.get.ts
Normal file
24
server/api/comments/index.get.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import prisma from '~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const query = getQuery(event);
|
||||||
|
const { articleId } = query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const comments = await prisma.comment.findMany({
|
||||||
|
where: articleId ? { articleId: String(articleId) } : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: comments,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching comments:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'An error occurred while fetching comments',
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
37
server/api/comments/index.post.ts
Normal file
37
server/api/comments/index.post.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import prisma from '~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event);
|
||||||
|
const { content, articleId, likes, dislikes } = body;
|
||||||
|
|
||||||
|
if (!content || !articleId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Content and articleId are required to create a comment.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newComment = await prisma.comment.create({
|
||||||
|
data: {
|
||||||
|
content,
|
||||||
|
articleId,
|
||||||
|
likes: likes || 0,
|
||||||
|
dislikes: dislikes || 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Comment created successfully',
|
||||||
|
data: newComment,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating comment:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'An error occurred while creating the comment',
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user