rajoute le support de l'utilisation de markdown (sur un sous-ensemble) dans la description des campagnes et des propositions

This commit is contained in:
Yannick Le Duc
2025-08-27 10:47:01 +02:00
parent 228be1b6f2
commit 5c5c5d11e3
14 changed files with 742 additions and 88 deletions

View File

@@ -7,10 +7,11 @@ import { Campaign } from '@/types';
import { campaignService, propositionService } from '@/lib/services';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ArrowLeft, FileText, User, Mail, CheckCircle, AlertCircle } from 'lucide-react';
import { MarkdownContent } from '@/components/MarkdownContent';
import { MarkdownEditor } from '@/components/MarkdownEditor';
export const dynamic = 'force-dynamic';
@@ -192,9 +193,10 @@ export default function PublicProposePage() {
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-slate-600 dark:text-slate-300 mb-2">Description</h3>
<div className="text-slate-900 dark:text-slate-100 whitespace-pre-wrap leading-relaxed">
{campaign?.description}
</div>
<MarkdownContent
content={campaign?.description || ''}
className="text-slate-900 dark:text-slate-100"
/>
</div>
</div>
</CardContent>
@@ -231,20 +233,13 @@ export default function PublicProposePage() {
/>
</div>
<div className="space-y-2">
<label htmlFor="description" className="text-sm font-medium text-slate-700 dark:text-slate-300">
Description *
</label>
<Textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Décrivez votre proposition en détail..."
rows={6}
required
/>
</div>
<MarkdownEditor
value={formData.description}
onChange={(value) => setFormData(prev => ({ ...prev, description: value }))}
placeholder="Décrivez votre proposition en détail..."
label="Description *"
maxLength={2000}
/>
<div className="border-t border-slate-200 dark:border-slate-700 pt-6">
<h3 className="text-lg font-medium text-slate-900 dark:text-slate-100 mb-4 flex items-center gap-2">

View File

@@ -5,6 +5,7 @@ import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { Campaign, Proposition, Participant, Vote, PropositionWithVote } from '@/types';
import { campaignService, participantService, propositionService, voteService, settingsService } from '@/lib/services';
import { MarkdownContent } from '@/components/MarkdownContent';
// Force dynamic rendering to avoid SSR issues with Supabase
export const dynamic = 'force-dynamic';
@@ -344,7 +345,7 @@ export default function PublicVotePage() {
const spendingTiers = getSpendingTiers();
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-50 vote-page">
{/* Header fixe avec le total et le bouton de validation */}
<div className="sticky top-0 z-40 bg-white shadow-sm border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
@@ -401,16 +402,26 @@ export default function PublicVotePage() {
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-1 gap-6">
<div>
<p className="mt-1 text-base font-medium text-gray-900 whitespace-pre-wrap leading-relaxed">{campaign?.description}</p>
{isRandomOrder && (
<div className="mt-4 text-xs text-gray-500 italic">
Les propositions sont affichées dans un ordre aléatoire pour éviter les biais liés à l'ordre de présentation.
</div>
)}
<MarkdownContent
content={campaign?.description || ''}
className="mt-1 text-base font-medium text-gray-900"
/>
</div>
</div>
</div>
{/* Message discret sur l'ordre aléatoire */}
{isRandomOrder && (
<div className="mb-6 text-center">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs text-gray-400 bg-gray-50 border border-gray-100">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
Les propositions sont affichées dans un ordre aléatoire pour éviter les biais liés à l'ordre de présentation
</span>
</div>
)}
{/* Propositions */}
{propositions.length === 0 ? (
<div className="text-center py-12">
@@ -444,9 +455,10 @@ export default function PublicVotePage() {
{proposition.title}
</h3>
{!isCompactView && (
<p className="text-sm text-gray-600 mb-4 whitespace-pre-wrap">
{proposition.description}
</p>
<MarkdownContent
content={proposition.description}
className="text-sm text-gray-600 mb-4"
/>
)}
</div>
</div>
@@ -465,7 +477,7 @@ export default function PublicVotePage() {
min="0"
max={spendingTiers.length}
step="1"
value={localVotes[proposition.id] ? spendingTiers.indexOf(localVotes[proposition.id]) + 1 : 0}
value={localVotes[proposition.id] ? (localVotes[proposition.id] === 0 ? 0 : spendingTiers.indexOf(localVotes[proposition.id]) + 1) : 0}
onChange={(e) => {
const index = parseInt(e.target.value);
const amount = index === 0 ? 0 : spendingTiers[index - 1];
@@ -475,15 +487,16 @@ export default function PublicVotePage() {
/>
{/* Marqueurs des paliers */}
<div className="relative mt-3 mb-16" style={{ marginLeft: '12px', marginRight: '12px' }}>
<div className="relative mt-3 mb-16" style={{ marginLeft: '12px', marginRight: '24px' }}>
{/* Marqueur 0€ */}
<div className="absolute text-center" style={{ left: '0%', transform: 'translateX(-12px)' }}>
<div className="w-3 h-3 bg-gray-400 rounded-full mx-auto mb-2"></div>
<span className="text-xs text-gray-600 font-medium">0€</span>
<span className="text-xs text-gray-600 font-medium whitespace-nowrap">0</span>
</div>
{/* Marqueurs des paliers */}
{spendingTiers.map((tier, index) => {
// Calcul correct de la position pour correspondre au slider
const position = ((index + 1) / spendingTiers.length) * 100;
return (
<div
@@ -495,7 +508,7 @@ export default function PublicVotePage() {
}}
>
<div className="w-3 h-3 bg-indigo-500 rounded-full mx-auto mb-2"></div>
<span className="text-xs text-gray-600 font-medium">{tier}€</span>
<span className="text-xs text-gray-600 font-medium whitespace-nowrap">{tier}</span>
</div>
);
})}