Files
mes-budgets-participatifs/src/components/MarkdownEditor.tsx
2025-08-27 12:21:09 +02:00

192 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useEffect } from 'react';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Eye, Edit3, AlertCircle, HelpCircle } from 'lucide-react';
import { previewMarkdown, validateMarkdown } from '@/lib/markdown';
interface MarkdownEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
label?: string;
maxLength?: number;
className?: string;
}
export function MarkdownEditor({
value,
onChange,
placeholder = "Écrivez votre description...",
label = "Description",
maxLength = 5000,
className = ""
}: MarkdownEditorProps) {
const [activeTab, setActiveTab] = useState<'edit' | 'preview'>('edit');
const [validation, setValidation] = useState<{ isValid: boolean; errors: string[] }>({ isValid: true, errors: [] });
const [showHelp, setShowHelp] = useState(false);
// Validation en temps réel
useEffect(() => {
const validationResult = validateMarkdown(value);
setValidation(validationResult);
}, [value]);
const handleChange = (newValue: string) => {
if (newValue.length <= maxLength) {
onChange(newValue);
}
};
const previewContent = previewMarkdown(value);
return (
<div className={`space-y-4 ${className}`}>
<div className="flex items-center justify-between">
<Label htmlFor="markdown-editor" className="text-sm font-medium">
{label}
</Label>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setShowHelp(!showHelp)}
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground rounded border-0 bg-transparent cursor-pointer flex items-center gap-1"
tabIndex={-1}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setShowHelp(!showHelp);
}
}}
>
<HelpCircle className="h-3 w-3" />
Aide Markdown
</button>
<span className="text-sm text-muted-foreground">{value.length}/{maxLength}</span>
</div>
</div>
<div className="flex items-center justify-center mb-4">
<div className="flex rounded-lg border bg-muted p-1">
<button
type="button"
onClick={() => setActiveTab('edit')}
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors flex items-center gap-2 ${
activeTab === 'edit'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
tabIndex={-1}
>
<Edit3 className="h-4 w-4" />
Éditer
</button>
<button
type="button"
onClick={() => setActiveTab('preview')}
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors flex items-center gap-2 ${
activeTab === 'preview'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
tabIndex={-1}
>
<Eye className="h-4 w-4" />
Prévisualiser
</button>
</div>
</div>
{activeTab === 'edit' && (
<div className="space-y-4">
<Textarea
id="markdown-editor"
value={value}
onChange={(e) => handleChange(e.target.value)}
placeholder={placeholder}
className="min-h-[200px] max-h-[300px] font-mono text-sm overflow-y-auto"
tabIndex={0}
/>
{/* Aide markdown (affichée conditionnellement) */}
{showHelp && (
<div className="rounded-lg border bg-muted/50 p-4 animate-in fade-in duration-200">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium">Syntaxe Markdown supportée</h4>
<button
type="button"
onClick={() => setShowHelp(false)}
className="h-6 px-2 text-xs rounded border-0 bg-transparent cursor-pointer hover:bg-muted"
tabIndex={-1}
>
×
</button>
</div>
<div className="grid grid-cols-2 gap-4 text-xs text-muted-foreground">
<div>
<p><strong>**gras**</strong> <strong>gras</strong></p>
<p><em>*italique*</em> <em>italique</em></p>
<p><u>__souligné__</u> <u>souligné</u></p>
<p><del>~~barré~~</del> <del>barré</del></p>
</div>
<div>
<p># Titre 1</p>
<p>## Titre 2</p>
<p>- Liste à puces</p>
<p>[Lien](https://exemple.com)</p>
</div>
</div>
</div>
)}
</div>
)}
{activeTab === 'preview' && (
<div className="space-y-4">
<div className="min-h-[200px] max-h-[300px] rounded-lg border bg-background p-4 overflow-y-auto">
{value ? (
<div
className="prose prose-sm max-w-none [&_ul]:space-y-1 [&_ol]:space-y-1 [&_li]:my-0"
dangerouslySetInnerHTML={{ __html: previewContent }}
/>
) : (
<p className="text-muted-foreground italic">
Aucun contenu à prévisualiser
</p>
)}
</div>
</div>
)}
{/* Messages d'erreur */}
{!validation.isValid && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<ul className="list-disc list-inside space-y-1">
{validation.errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
{/* Avertissement de longueur */}
{value.length > maxLength * 0.9 && (
<Alert variant={value.length > maxLength ? "destructive" : "default"}>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{value.length > maxLength
? `Le contenu dépasse la limite de ${maxLength} caractères`
: `Le contenu approche de la limite de ${maxLength} caractères`
}
</AlertDescription>
</Alert>
)}
</div>
);
}