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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,8 +1,52 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--color-background: 0 0% 100%;
|
||||
--color-foreground: 222.2 84% 4.9%;
|
||||
--color-card: 0 0% 100%;
|
||||
--color-card-foreground: 222.2 84% 4.9%;
|
||||
--color-popover: 0 0% 100%;
|
||||
--color-popover-foreground: 222.2 84% 4.9%;
|
||||
--color-primary: 222.2 47.4% 11.2%;
|
||||
--color-primary-foreground: 210 40% 98%;
|
||||
--color-secondary: 210 40% 96%;
|
||||
--color-secondary-foreground: 222.2 84% 4.9%;
|
||||
--color-muted: 210 40% 96%;
|
||||
--color-muted-foreground: 215.4 16.3% 46.9%;
|
||||
--color-accent: 210 40% 96%;
|
||||
--color-accent-foreground: 222.2 84% 4.9%;
|
||||
--color-destructive: 0 84.2% 60.2%;
|
||||
--color-destructive-foreground: 210 40% 98%;
|
||||
--color-border: 214.3 31.8% 91.4%;
|
||||
--color-input: 214.3 31.8% 91.4%;
|
||||
--color-ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme dark {
|
||||
--color-background: 222.2 84% 4.9%;
|
||||
--color-foreground: 210 40% 98%;
|
||||
--color-card: 222.2 84% 4.9%;
|
||||
--color-card-foreground: 210 40% 98%;
|
||||
--color-popover: 222.2 84% 4.9%;
|
||||
--color-popover-foreground: 210 40% 98%;
|
||||
--color-primary: 210 40% 98%;
|
||||
--color-primary-foreground: 222.2 47.4% 11.2%;
|
||||
--color-secondary: 217.2 32.6% 17.5%;
|
||||
--color-secondary-foreground: 210 40% 98%;
|
||||
--color-muted: 217.2 32.6% 17.5%;
|
||||
--color-muted-foreground: 215 20.2% 65.1%;
|
||||
--color-accent: 217.2 32.6% 17.5%;
|
||||
--color-accent-foreground: 210 40% 98%;
|
||||
--color-destructive: 0 62.8% 30.6%;
|
||||
--color-destructive-foreground: 210 40% 98%;
|
||||
--color-border: 217.2 32.6% 17.5%;
|
||||
--color-input: 217.2 32.6% 17.5%;
|
||||
--color-ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
@@ -181,7 +225,7 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@@ -202,3 +246,202 @@
|
||||
linear-gradient(90deg, rgba(148, 163, 184, 0.05) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Styles pour le support Markdown */
|
||||
.prose {
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
/* Styles spécifiques pour la page de vote - titres markdown plus petits */
|
||||
.vote-page .prose h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.vote-page .prose h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.vote-page .prose h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.375rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose strong {
|
||||
font-weight: 600;
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
.prose em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prose u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.prose del {
|
||||
text-decoration: line-through;
|
||||
color: hsl(215.4 16.3% 46.9%);
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
list-style-type: decimal;
|
||||
list-style-position: inside;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: hsl(222.2 47.4% 11.2%);
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.prose a:hover {
|
||||
color: hsl(222.2 47.4% 11.2% / 0.8);
|
||||
}
|
||||
|
||||
.prose hr {
|
||||
border-color: hsl(214.3 31.8% 91.4%);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.prose br {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Styles pour l'éditeur markdown */
|
||||
.markdown-editor {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.markdown-editor:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px hsl(222.2 84% 4.9%), 0 0 0 4px hsl(222.2 84% 4.9% / 0.1);
|
||||
}
|
||||
|
||||
/* Styles pour la prévisualisation */
|
||||
.markdown-preview {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Styles pour les onglets */
|
||||
.markdown-tabs {
|
||||
border-bottom: 1px solid hsl(214.3 31.8% 91.4%);
|
||||
}
|
||||
|
||||
.markdown-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.markdown-tab:hover {
|
||||
border-color: hsl(214.3 31.8% 91.4%);
|
||||
}
|
||||
|
||||
.markdown-tab.active {
|
||||
border-color: hsl(222.2 47.4% 11.2%);
|
||||
color: hsl(222.2 47.4% 11.2%);
|
||||
}
|
||||
|
||||
/* Styles pour le mode sombre */
|
||||
.dark .prose {
|
||||
color: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
.dark .prose h1,
|
||||
.dark .prose h2,
|
||||
.dark .prose h3,
|
||||
.dark .prose strong {
|
||||
color: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
/* Styles spécifiques pour la page de vote en mode sombre */
|
||||
.dark .vote-page .prose h1,
|
||||
.dark .vote-page .prose h2,
|
||||
.dark .vote-page .prose h3 {
|
||||
color: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
.dark .prose del {
|
||||
color: hsl(215 20.2% 65.1%);
|
||||
}
|
||||
|
||||
.dark .prose a {
|
||||
color: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
.dark .prose a:hover {
|
||||
color: hsl(210 40% 98% / 0.8);
|
||||
}
|
||||
|
||||
.dark .prose hr {
|
||||
border-color: hsl(217.2 32.6% 17.5%);
|
||||
}
|
||||
|
||||
.dark .markdown-editor:focus {
|
||||
box-shadow: 0 0 0 2px hsl(210 40% 98%), 0 0 0 4px hsl(210 40% 98% / 0.1);
|
||||
}
|
||||
|
||||
.dark .markdown-tabs {
|
||||
border-bottom: 1px solid hsl(217.2 32.6% 17.5%);
|
||||
}
|
||||
|
||||
.dark .markdown-tab:hover {
|
||||
border-color: hsl(217.2 32.6% 17.5%);
|
||||
}
|
||||
|
||||
.dark .markdown-tab.active {
|
||||
border-color: hsl(210 40% 98%);
|
||||
color: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user