Ajout paramètre message bas de page personnalisable
This commit is contained in:
@@ -15,6 +15,7 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import Navigation from '@/components/Navigation';
|
||||
import AuthGuard from '@/components/AuthGuard';
|
||||
import { FileText, Calendar, Mail, Upload } from 'lucide-react';
|
||||
import { MarkdownContent } from '@/components/MarkdownContent';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -196,7 +197,7 @@ function CampaignPropositionsPageContent() {
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-xl mb-2">{proposition.title}</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
{proposition.description}
|
||||
<MarkdownContent content={proposition.description} />
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import AuthGuard from '@/components/AuthGuard';
|
||||
import { FolderOpen, Users, FileText, Plus, BarChart3, Settings, Check, Copy } from 'lucide-react';
|
||||
import StatusSwitch from '@/components/StatusSwitch';
|
||||
import { MarkdownContent } from '@/components/MarkdownContent';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -255,7 +256,7 @@ function AdminPageContent() {
|
||||
</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="text-slate-600 dark:text-slate-400 text-sm leading-relaxed mb-4">
|
||||
{campaign.description}
|
||||
<MarkdownContent content={campaign.description} />
|
||||
</CardDescription>
|
||||
|
||||
{/* Status Switch */}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Label } from '@/components/ui/label';
|
||||
import Navigation from '@/components/Navigation';
|
||||
import AuthGuard from '@/components/AuthGuard';
|
||||
import SmtpSettingsForm from '@/components/SmtpSettingsForm';
|
||||
import { Settings, Monitor, Save, CheckCircle, Mail } from 'lucide-react';
|
||||
import { Settings, Monitor, Save, CheckCircle, Mail, FileText } from 'lucide-react';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -19,6 +19,8 @@ function SettingsPageContent() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [randomizePropositions, setRandomizePropositions] = useState(false);
|
||||
const [proposePageMessage, setProposePageMessage] = useState('');
|
||||
const [footerMessage, setFooterMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
@@ -33,6 +35,14 @@ function SettingsPageContent() {
|
||||
// Charger la valeur du paramètre d'ordre aléatoire
|
||||
const randomizeValue = await settingsService.getBooleanValue('randomize_propositions', false);
|
||||
setRandomizePropositions(randomizeValue);
|
||||
|
||||
// Charger le message de la page de dépôt de propositions
|
||||
const messageValue = await 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é.');
|
||||
setProposePageMessage(messageValue);
|
||||
|
||||
// Charger le message du bas de page
|
||||
const footerValue = await settingsService.getStringValue('footer_message', 'Développé avec ❤️ pour faciliter la démocratie participative - [Logiciel libre et open source](GITURL) et transparent pour tous');
|
||||
setFooterMessage(footerValue);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des paramètres:', error);
|
||||
} finally {
|
||||
@@ -48,6 +58,8 @@ function SettingsPageContent() {
|
||||
try {
|
||||
setSaving(true);
|
||||
await settingsService.setBooleanValue('randomize_propositions', randomizePropositions);
|
||||
await settingsService.setStringValue('propose_page_message', proposePageMessage);
|
||||
await settingsService.setStringValue('footer_message', footerMessage);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
} catch (error) {
|
||||
@@ -148,6 +160,62 @@ function SettingsPageContent() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Textes Category */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-green-600 dark:text-green-300" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">Textes</CardTitle>
|
||||
<CardDescription>
|
||||
Personnalisez les textes affichés dans l'application
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Propose Page Message Setting */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="propose-page-message" className="text-base font-medium">
|
||||
Message d'invitation - Page de dépôt de propositions
|
||||
</Label>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mt-1">
|
||||
Ce texte apparaît sous le titre de la campagne pour inviter les utilisateurs à déposer des propositions.
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
id="propose-page-message"
|
||||
value={proposePageMessage}
|
||||
onChange={(e) => setProposePageMessage(e.target.value)}
|
||||
className="w-full min-h-[100px] p-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y"
|
||||
placeholder="Entrez votre message d'invitation..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Message Setting */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="footer-message" className="text-base font-medium">
|
||||
Message du bas de page
|
||||
</Label>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mt-1">
|
||||
Ce texte apparaît en bas des pages publiques. Vous pouvez utiliser <code className="bg-slate-100 dark:bg-slate-700 px-1 rounded text-xs">[texte du lien](GITURL)</code> pour insérer un lien vers le repository Git.
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
id="footer-message"
|
||||
value={footerMessage}
|
||||
onChange={(e) => setFooterMessage(e.target.value)}
|
||||
className="w-full min-h-[80px] p-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y"
|
||||
placeholder="Entrez votre message de bas de page..."
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Email Category */}
|
||||
<SmtpSettingsForm onSave={() => {
|
||||
setSaved(true);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -255,24 +255,24 @@
|
||||
.prose h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.75rem;
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.5rem;
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.375rem;
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
@@ -280,22 +280,22 @@
|
||||
.vote-page .prose h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.vote-page .prose h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.vote-page .prose h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.375rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
@@ -334,7 +334,7 @@
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
@@ -445,3 +445,9 @@
|
||||
border-color: hsl(210 40% 98%);
|
||||
color: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
/* Styles simples pour réduire l'espacement des listes */
|
||||
.prose ul li,
|
||||
.prose ol li {
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { PROJECT_CONFIG } from '@/lib/project.config';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
@@ -154,11 +155,7 @@ export default function HomePage() {
|
||||
</Card>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center mt-16 pb-8">
|
||||
<p className="text-slate-600 dark:text-slate-400 text-lg">
|
||||
Développé avec ❤️ pour faciliter la démocratie participative
|
||||
</p>
|
||||
</div>
|
||||
<Footer variant="home" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user