Compare commits
5 Commits
0e23d7f455
...
feature/ui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
967b6114e1 | ||
|
|
1b35e137d3 | ||
|
|
e24b3e1955 | ||
|
|
6d6d70295d | ||
|
|
b9719e0c98 |
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-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
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,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',
|
||||
],
|
||||
|
||||
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,
|
||||
},
|
||||
|
||||
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: {
|
||||
vue: {
|
||||
template: {
|
||||
transformAssetUrls,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
343
pages/index.vue
343
pages/index.vue
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<v-container fluid class="lg:mx-12 mx-2">
|
||||
<v-container fluid>
|
||||
<!-- Loading Section -->
|
||||
<v-row v-if="isLoading" class="justify-center">
|
||||
<Loader />
|
||||
</v-row>
|
||||
|
||||
<!-- 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" />
|
||||
@@ -7,9 +14,9 @@
|
||||
</video>
|
||||
</v-row>
|
||||
<!-- Introduction section -->
|
||||
<v-row v-if="introduction" class="d-flex justify-center align-center text-center">
|
||||
<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 mb-8 mr-2 lg:mr-8 text-justify">{{ introduction.description }}</p>
|
||||
<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>
|
||||
@@ -99,26 +106,26 @@
|
||||
<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-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="d-flex align-center mb-2">
|
||||
<span class="mr-2">
|
||||
<v-icon class="m-1 text-primary hover:text-green-500 text-xs"
|
||||
<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="ml-2 lg:ml-0 text-primary">{{ article.likes }}</p>
|
||||
</span>
|
||||
<p class="text-gray-400 text-sm">{{ article.likes }}</p>
|
||||
|
||||
<span class="mr-3">
|
||||
<v-icon class="m-1 text-tertiary hover:text-red-500 text-xs"
|
||||
<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="ml-2 lg:ml-0 text-tertiary">{{ article.dislikes }}</p>
|
||||
</span>
|
||||
<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>
|
||||
@@ -132,66 +139,107 @@
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-expand-transition>
|
||||
<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>
|
||||
|
||||
<!-- 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-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 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"
|
||||
<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 :disabled="!isFormValid" type="submit" block>
|
||||
<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" class="d-flex align-center justify-center"
|
||||
<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="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 }}
|
||||
<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-overlay>
|
||||
</v-row>
|
||||
</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,18 +290,18 @@ const toggleShow = (articleId) => {
|
||||
};
|
||||
|
||||
// Fetch articles data
|
||||
const { data: contentData, error } = await useAsyncData("content", () =>
|
||||
const fetchContent = async () => {
|
||||
try {
|
||||
const { data: contentData, error } = await useAsyncData('content', () =>
|
||||
queryContent().find()
|
||||
);
|
||||
);
|
||||
|
||||
// Process fetched content
|
||||
if (!error.value && contentData.value) {
|
||||
// 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,
|
||||
@@ -266,118 +321,128 @@ if (!error.value && contentData.value) {
|
||||
title: article.title,
|
||||
description: article.description,
|
||||
likes: article.likes,
|
||||
dislikes: article.dislikes,
|
||||
comments: article.comments,
|
||||
dislikes: article.dislikes
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error creating article:', err);
|
||||
}
|
||||
} else if (file.type === "summary") {
|
||||
summary.value = file;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize newComments object based on article IDs
|
||||
articles.value.forEach(article => {
|
||||
newComments.value[article.id] = '';
|
||||
});
|
||||
|
||||
}
|
||||
// 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] = '';
|
||||
});
|
||||
}
|
||||
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 = async (articleId) => {
|
||||
const article = articles.value.find((a) => a.id === articleId);
|
||||
if (!article) return;
|
||||
article.likes += 1;
|
||||
|
||||
try {
|
||||
await $fetch(`/api/articles/${articleId}`, {
|
||||
method: 'PUT',
|
||||
body: { likes: article.likes },
|
||||
const likeArticle = (articleId) => {
|
||||
updateLikeDislike({
|
||||
type: 'article',
|
||||
action: 'like',
|
||||
id: 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 },
|
||||
const dislikeArticle = (articleId) => {
|
||||
updateLikeDislike({
|
||||
type: 'article',
|
||||
action: 'dislike',
|
||||
id: articleId,
|
||||
articles,
|
||||
});
|
||||
};
|
||||
|
||||
console.log('Article updated successfully');
|
||||
} catch (error) {
|
||||
console.error('Error in dislikeArticle:', error);
|
||||
}
|
||||
const likeComment = (articleId, commentId) => {
|
||||
updateLikeDislike({
|
||||
type: 'comment',
|
||||
action: 'like',
|
||||
id: commentId,
|
||||
articleId,
|
||||
articles,
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
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 () => {
|
||||
if (isFormValid.value) {
|
||||
const signatureData = {
|
||||
name: name.value,
|
||||
email: email.value,
|
||||
@@ -392,21 +457,27 @@ const submitSignature = async () => {
|
||||
},
|
||||
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);
|
||||
}
|
||||
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 = '';
|
||||
} 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>
|
||||
|
||||
@@ -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;
|
||||
@@ -13,7 +13,18 @@ model Article {
|
||||
description String
|
||||
likes Int @default(0)
|
||||
dislikes Int @default(0)
|
||||
comments String[] @default([])
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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,
|
||||
message: 'Article not found.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,9 @@ 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 {
|
||||
|
||||
@@ -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 || [],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
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