Compare commits

...

3 Commits

Author SHA1 Message Date
Do-raa
967b6114e1 changed some colors
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-12-23 20:04:26 +01:00
Do-raa
1b35e137d3 ui enhancement
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-12-23 19:23:36 +01:00
Do-raa
e24b3e1955 add interactions to comments
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-12-23 15:44:36 +01:00
22 changed files with 3964 additions and 3411 deletions

View File

@@ -1,7 +0,0 @@
{
"cSpell.words": [
"Charte",
"Nuxt",
"supabase"
]
}

13
components/Loader.vue Normal file
View 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-xl text-center text-bold text-uppercase font-sans " cols="12">
Soyez le bienvenue sur DAV
</v-col>
<v-col cols="6">
<v-progress-linear color="primary" height="6" indeterminate
rounded></v-progress-linear>
</v-col>
</v-row>
</v-container>
</template>

73
lib/fetchers.js Normal file
View 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
View 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
View 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,
};
};

View File

@@ -1,29 +1,31 @@
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import { defineNuxtConfig } from 'nuxt/config';
export default defineNuxtConfig({
ssr: false,
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
app: {
head: {
title: 'DAV',
meta: [
{
name: 'description',
content: `Droits de l'ame et du vivant`,
},
],
},
},
css: [
'~/assets/css/main.css',
'~/assets/css/main.css',
],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
build: {
transpile: ['vuetify'],
},
modules: [
(_options, nuxt) => {
nuxt.hooks.hook('vite:extendConfig', (config) => {
// @ts-expect-error
config.plugins.push(vuetify({ autoImport: true }))
})
},
'vuetify-nuxt-module',
'@nuxt/content',
'@nuxtjs/supabase',
"@prisma/nuxt"
@@ -34,12 +36,36 @@ export default defineNuxtConfig({
key: process.env.SUPABASE_KEY,
redirect: false,
},
vite: {
vue: {
template: {
transformAssetUrls,
vuetify: {
vuetifyOptions: {
components: 'VBtn',
theme: {
themes: {
light: {
colors: {
primary: '#A7D129',
secondary: '#FFD93D',
tertiary: '#FFB400',
accent: '#646464',
},
},
},
},
aliases: {
VBtnValid: 'VBtn',
},
defaults: {
VBtn: {
color: 'accent',
class: 'custom-btn',
},
VBtnValid: {
color: 'primary',
class: 'custom-btn',
},
},
},
},
})
vite: {
},
})

6140
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,22 +10,20 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@nuxt/content": "^2.13.4",
"@nuxtjs/supabase": "^1.4.4",
"@prisma/nuxt": "^0.1.3",
"nuxt": "^3.14.1592",
"uuid": "^11.0.3",
"vue": "latest",
"vue-router": "latest"
"vue-router": "latest",
"vuetify": "^3.7.6",
"vuetify-nuxt-module": "^0.18.3"
},
"devDependencies": {
"@prisma/client": "^5.22.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"prisma": "^6.0.1",
"tailwindcss": "^3.4.16",
"vite-plugin-vuetify": "^2.0.4",
"vuetify": "^3.7.5"
"prisma": "^6.1.0",
"tailwindcss": "^3.4.16"
}
}

View File

@@ -1,197 +1,245 @@
<template>
<v-container fluid class="lg:mx-12 mx-2">
<v-row class="video-section">
<video autoplay loop muted playsinline class="video-background">
<source src="/videos/video.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<v-container fluid>
<!-- Loading Section -->
<v-row v-if="isLoading" class="justify-center">
<Loader />
</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>
<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 deux 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>
<!-- Main Content Section -->
<v-row v-else class="mx-5">
<v-row class="video-section">
<video autoplay loop muted playsinline class="video-background">
<source src="/videos/video.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</v-row>
<!-- Introduction section -->
<v-row v-if="introduction" class="d-flex justify-center align-center text-center mx-4">
<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 deux 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>
<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 deux 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 deux 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">
<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-xs" size="18" color="secondary"
@click.stop="likeArticle(article.id)">
mdi-thumb-up
</v-icon>
<p class="text-gray-400 text-sm">{{ article.likes }}</p>
<v-icon class="mr-1 ml-2 text-xs" size="18" color="tertiary"
@click.stop="dislikeArticle(article.id)">
mdi-thumb-down
</v-icon>
<p class="text-gray-400 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-item v-if="article.comments.length"
v-for="(comment, cIndex) in article.comments" :key="cIndex" dot-color="primary"
size="x-small">
<div class="mb-0">
<p class="text-sm text-black">{{ comment }}</p>
v-for="(comment, cIndex) in article.comments" :key="cIndex"
dot-color="secondary" size="x-small">
<div class="flex items-center mb-0">
<!-- Like/Dislike Section -->
<div class="mr-3 flex items-center">
<v-icon color="secondary" size="14"
@click="likeComment(article.id, comment.id)">mdi-thumb-up</v-icon>
<span class="text-xs text-gray-400 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-gray-400 ml-1">{{ comment.dislikes }}</span>
</div>
<!-- Comment Content -->
<p class="text-sm text-black">{{ comment.content }}</p>
</div>
</v-timeline-item>
<v-timeline-item v-else dot-color="secondary" size="x-small">
<!-- No Comments Placeholder -->
<v-timeline-item v-else dot-color="accent" size="x-small">
<p class="text-sm text-black">Aucune suggestion pour cet article</p>
</v-timeline-item>
</v-timeline>
</v-expand-transition>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-expansion-panel-text>
<!-- Signature Section -->
<v-row class="justify-center my-8">
<v-col cols="12" md="8" lg="6">
<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 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-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-card-text>
<!-- Signature Section -->
<v-row class="justify-center w-full">
<v-col cols="12" md="6" lg="8">
<v-card elevation="3" class="pa-6 bg-gray-100" color="accent">
<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="justify-center">
<v-col cols="12" md="6" lg="8">
<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="accent" 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-col>
</v-overlay>
</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>
</template>
@@ -199,6 +247,11 @@
import { ref, onMounted } from 'vue';
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({
layout: 'default',
});
@@ -210,7 +263,6 @@ const rightColumnArticles = ref([]);
const showStates = ref({});
const introduction = ref(null);
const summary = ref(null);
const signatures = ref([]);
// Form fields
const name = ref('');
@@ -218,12 +270,15 @@ const email = ref('');
const comment = ref('');
const isFormValid = computed(() => name.value && email.value);
const isLoading = ref(true);
const overlay = ref(false);
const overlayIcon = ref('mdi-check-circle'); // Default icon
const overlayMessage = ref('Votre suggestion à été ajoutée avec succès'); // Default message
const overlayButtonText = ref('Continuer'); // Default button text
const overlayIconColor = ref('success');
const overlay = ref({
icon: '',
message: '',
buttonText: '',
iconColor: '',
visible: false,
})
const rules = {
required: (value) => !!value || "Ce champ est obligatoire.",
@@ -235,178 +290,194 @@ const toggleShow = (articleId) => {
};
// Fetch articles data
const { data: contentData, error } = await useAsyncData("content", () =>
queryContent().find()
);
const fetchContent = async () => {
try {
const { data: contentData, error } = await useAsyncData('content', () =>
queryContent().find()
);
// Process fetched content
if (!error.value && contentData.value) {
for (const file of contentData.value) {
if (file.type === "intro") {
introduction.value = file;
} else if (file.type === "article") {
try {
// Add default values (likes, dislikes, comments) if missing
const article = {
id: file.id,
title: file.title,
description: file.description,
likes: file.likes || 0,
dislikes: file.dislikes || 0,
comments: file.comments || [],
};
// Process fetched content
if (!error.value && contentData.value) {
for (const file of contentData.value) {
if (file.type === "intro") {
introduction.value = file;
} else if (file.type === "article") {
const article = {
id: file.id,
title: file.title,
description: file.description,
likes: file.likes || 0,
dislikes: file.dislikes || 0,
comments: file.comments || [],
};
// Add the article locally
articles.value.push(article);
// Add the article locally
articles.value.push(article);
await $fetch('/api/articles', {
method: 'POST',
body: {
id: article.id,
title: article.title,
description: article.description,
likes: article.likes,
dislikes: article.dislikes,
comments: article.comments,
},
});
} catch (err) {
console.error('Error creating article:', err);
await $fetch('/api/articles', {
method: 'POST',
body: {
id: article.id,
title: article.title,
description: article.description,
likes: article.likes,
dislikes: article.dislikes
},
});
} else if (file.type === "summary") {
summary.value = file;
}
}
} 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);
// 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);
// Initialize newComments object based on article IDs
articles.value.forEach(article => {
newComments.value[article.id] = '';
});
}
// Initialize newComments object based on article IDs
articles.value.forEach(article => {
newComments.value[article.id] = '';
});
// 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);
}
catch (err) {
console.error('Error fetching content:', err);
} finally {
isLoading.value = false;
}
};
onMounted(async () => {
try {
const response = await $fetch(`/api/articles`, {
method: 'GET',
});
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);
}
await fetchContent()
await fetchArticlesData(articles);
await fetchCommentsData(articles);
await fetchSignatures(signatures);
});
const likeArticle = (articleId) => {
updateLikeDislike({
type: 'article',
action: 'like',
id: articleId,
articles,
});
};
const likeArticle = async (articleId) => {
const article = articles.value.find((a) => a.id === articleId);
if (!article) return;
article.likes += 1;
const dislikeArticle = (articleId) => {
updateLikeDislike({
type: 'article',
action: 'dislike',
id: articleId,
articles,
});
};
try {
await $fetch(`/api/articles/${articleId}`, {
method: 'PUT',
body: { likes: article.likes },
});
const likeComment = (articleId, commentId) => {
updateLikeDislike({
type: 'comment',
action: 'like',
id: commentId,
articleId,
articles,
});
};
console.log('Article updated successfully');
} catch (error) {
console.error('Error in likeArticle:', error);
}
};;
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 dislikeComment = (articleId, commentId) => {
updateLikeDislike({
type: 'comment',
action: 'dislike',
id: commentId,
articleId,
articles,
});
};
const addComment = async (articleId) => {
const commentText = newComments.value[articleId]?.trim();
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 {
await $fetch(`/api/articles/${article.id}`, {
method: 'PUT',
body: { comments: article.comments },
const response = await $fetch('/api/comments', {
method: 'POST',
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) {
console.error('Error in addComment:', error);
}
};
const submitSignature = async () => {
const signatureData = {
name: name.value,
email: email.value,
comment: comment.value,
};
signatures.value.push({ ...signatureData });
try {
await $fetch('/api/charte', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(signatureData)
})
console.log('Charte is submitted successfully');
} catch (error) {
console.error('Error in submit signature:', error);
}
overlayIcon.value = 'mdi-check-circle';
overlayMessage.value = `Merci pour votre signature ${name.value}`
overlayButtonText.value = 'Continuer';
overlayIconColor.value = 'success';
overlay.value = true;
name.value = '';
email.value = '';
comment.value = '';
if (isFormValid.value) {
const signatureData = {
name: name.value,
email: email.value,
comment: comment.value,
};
signatures.value.push({ ...signatureData });
try {
await $fetch('/api/charte', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(signatureData)
})
// Display success overlay using the reusable function
showOverlay(
{
icon: 'mdi-check-circle',
message: `Merci pour votre signature ${name.value}`,
buttonText: 'Continuer',
iconColor: 'success',
},
overlay
)
name.value = '';
email.value = '';
comment.value = '';
console.log('Charte is submitted successfully');
} catch (error) {
console.error('Error in submit signature:', error);
}
} return;
};
</script>
<style scoped>
.video-section {
position: relative;
@@ -451,4 +522,8 @@ p {
min-width: 40px;
line-height: 1.2;
}
.v-icon:hover {
opacity: 0.5;
}
</style>

View File

@@ -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)
})

View 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;

View File

@@ -8,14 +8,25 @@ datasource db {
}
model Article {
id String @id @default(uuid())
id String @id @default(uuid())
title String
description String
likes Int @default(0)
dislikes Int @default(0)
comments String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
likes Int @default(0)
dislikes Int @default(0)
comments Comment[]
createdAt DateTime @default(now())
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 {

View File

@@ -1 +0,0 @@

View File

@@ -2,14 +2,18 @@ import prisma from '~/lib/prisma';
export default defineEventHandler(async (event) => {
const id = event.context.params?.id;
console.log('prisma' + event.context.params?.id)
try {
const article = await prisma.article.findUnique({ where: { id } });
const article = await prisma.article.findUnique({
where: { id },
include: { comments: true },
});
if (!article) {
return {
success: false,
data: null,
data: null,
message: 'Article not found.',
};
}

View File

@@ -4,7 +4,7 @@ export default defineEventHandler(async (event) => {
const id = event.context.params?.id;
const body = await readBody(event);
if (!id ) {
if (!id) {
return {
success: false,
message: 'Invalid article ID.',
@@ -15,19 +15,19 @@ export default defineEventHandler(async (event) => {
const updatedArticle = await prisma.article.update({
where: { id },
data: {
id: body.id,
title: body.title,
description: body.description,
likes: body.likes,
dislikes: body.dislikes,
comments: body.comments,
},
});
return {
success: true,
data: updatedArticle,
};
} catch (error) {
console.error('Error updating article:', error);
return {
success: false,
error: error.message,

View File

@@ -2,12 +2,14 @@ import prisma from '~/lib/prisma';
export default defineEventHandler(async (event) => {
try {
const articles = await prisma.article.findMany();
const articles = await prisma.article.findMany({
include: { comments: true },
});
if (!articles || articles.length === 0) {
return {
success: false,
data: null,
data: null,
message: 'No articles found.',
};
}

View File

@@ -3,16 +3,14 @@ import prisma from '~/lib/prisma';
export default defineEventHandler(async (event) => {
try {
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({
data: {
id,
title,
description,
likes: likes || 0,
dislikes: dislikes || 0,
comments: comments || [],
},
});

View 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,
};
}
});

View 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,
};
}
});

View 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,
};
}
});

View 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,
};
}
});

View 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,
};
}
});