Ajout paramètre message bas de page personnalisable

This commit is contained in:
Yannick Le Duc
2025-08-27 12:21:09 +02:00
parent 28df167fee
commit aa859a1e44
15 changed files with 580 additions and 207 deletions

View File

@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import { Campaign } from '@/types';
import { campaignService, propositionService } from '@/lib/services';
import { campaignService, propositionService, settingsService } from '@/lib/services';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -13,6 +13,7 @@ import { ArrowLeft, FileText, User, Mail, CheckCircle, AlertCircle } from 'lucid
import { MarkdownContent } from '@/components/MarkdownContent';
import { MarkdownEditor } from '@/components/MarkdownEditor';
import { PROJECT_CONFIG } from '@/lib/project.config';
import Footer from '@/components/Footer';
export const dynamic = 'force-dynamic';
@@ -25,13 +26,16 @@ export default function PublicProposePage() {
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [startTime] = useState(Date.now()); // Validation temporelle
const [proposePageMessage, setProposePageMessage] = useState('');
const [formData, setFormData] = useState({
title: '',
description: '',
author_first_name: '',
author_last_name: '',
author_email: ''
author_email: '',
website: '' // Honeypot field
});
useEffect(() => {
@@ -43,8 +47,12 @@ export default function PublicProposePage() {
const loadCampaign = async () => {
try {
setLoading(true);
const campaigns = await campaignService.getAll();
const campaignData = campaigns.find(c => c.id === campaignId);
const [campaigns, messageValue] = await Promise.all([
campaignService.getAll(),
settingsService.getStringValue('propose_page_message', 'Partagez votre vision et proposez des projets qui feront la différence dans votre collectif. Votre voix compte pour façonner l\'avenir de votre communauté.')
]);
const campaignData = campaigns.find((c: Campaign) => c.id === campaignId);
if (!campaignData) {
setError('Campagne non trouvée');
@@ -57,6 +65,7 @@ export default function PublicProposePage() {
}
setCampaign(campaignData);
setProposePageMessage(messageValue);
} catch (error) {
console.error('Erreur lors du chargement de la campagne:', error);
setError('Erreur lors du chargement de la campagne');
@@ -70,6 +79,21 @@ export default function PublicProposePage() {
setSubmitting(true);
setError('');
// Validation temporelle - détecte les soumissions trop rapides
const timeSpent = Date.now() - startTime;
if (timeSpent < 5000) { // Moins de 5 secondes
setError('Veuillez prendre le temps de bien rédiger votre proposition');
setSubmitting(false);
return;
}
// Validation honeypot - détecte les bots
if (formData.website) {
setError('Soumission invalide');
setSubmitting(false);
return;
}
try {
await propositionService.create({
campaign_id: campaignId,
@@ -86,7 +110,8 @@ export default function PublicProposePage() {
description: '',
author_first_name: '',
author_last_name: '',
author_email: ''
author_email: '',
website: ''
});
} catch (err: any) {
const errorMessage = err?.message || err?.details || 'Erreur lors de la soumission de la proposition';
@@ -172,44 +197,28 @@ export default function PublicProposePage() {
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">Déposer une proposition</h1>
<p className="text-slate-600 dark:text-slate-300 mt-2">
Campagne : <span className="font-medium">{campaign?.title}</span>
</p>
</div>
<div className="text-center">
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-4">
{campaign?.title}
</h1>
<p className="text-lg text-slate-600 dark:text-slate-300 max-w-2xl mx-auto">
{proposePageMessage}
</p>
</div>
</div>
{/* Campaign Info */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Informations sur la campagne
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-slate-600 dark:text-slate-300 mb-2">Description</h3>
<MarkdownContent
content={campaign?.description || ''}
className="text-slate-900 dark:text-slate-100"
/>
</div>
</div>
</CardContent>
</Card>
{/* Campaign Description */}
<div className="mb-8 max-w-3xl mx-auto">
<MarkdownContent
content={campaign?.description || ''}
className="text-slate-700 dark:text-slate-300 text-base leading-relaxed"
/>
</div>
{/* Form */}
<Card>
<CardHeader>
<CardTitle>Votre proposition</CardTitle>
<CardDescription>
Remplissez le formulaire ci-dessous pour soumettre votre proposition.
</CardDescription>
<Card className="max-w-3xl mx-auto">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Votre proposition</CardTitle>
</CardHeader>
<CardContent>
@@ -220,6 +229,25 @@ export default function PublicProposePage() {
</div>
)}
{/* Honeypot field - caché pour détecter les bots */}
<input
type="text"
name="website"
value={formData.website}
onChange={handleChange}
style={{
position: 'absolute',
left: '-9999px',
width: '1px',
height: '1px',
opacity: 0,
pointerEvents: 'none'
}}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
/>
<div className="space-y-2">
<label htmlFor="title" className="text-sm font-medium text-slate-700 dark:text-slate-300">
Titre de la proposition *
@@ -305,20 +333,7 @@ export default function PublicProposePage() {
</Card>
{/* Footer discret */}
<div className="text-center mt-16 pb-8">
<p className="text-slate-400 dark:text-slate-500 text-sm">
Développé avec pour faciliter la démocratie participative -{' '}
<a
href={PROJECT_CONFIG.repository.url}
target="_blank"
rel="noopener noreferrer"
className="text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 underline"
>
Logiciel libre
</a>{' '}
et transparent pour tous
</p>
</div>
<Footer />
</div>
</div>
);

View File

@@ -7,6 +7,7 @@ import { Campaign, Proposition, Participant, Vote, PropositionWithVote } from '@
import { campaignService, participantService, propositionService, voteService, settingsService } from '@/lib/services';
import { MarkdownContent } from '@/components/MarkdownContent';
import { PROJECT_CONFIG } from '@/lib/project.config';
import Footer from '@/components/Footer';
// Force dynamic rendering to avoid SSR issues with Supabase
export const dynamic = 'force-dynamic';
@@ -539,20 +540,7 @@ export default function PublicVotePage() {
)}
{/* Footer discret */}
<div className="text-center mt-16 pb-20">
<p className="text-gray-400 text-sm">
Développé avec pour faciliter la démocratie participative -{' '}
<a
href={PROJECT_CONFIG.repository.url}
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 hover:text-gray-700 underline"
>
Logiciel libre
</a>{' '}
et transparent pour tous
</p>
</div>
<Footer />
</div>
{/* Barre fixe en bas */}