feat: publication pages — design moderne + modération IA + signalement
Page publique (/p/[slug]): - Design éditorial moderne (Source Serif 4 + Inter, couleurs Momento) - KaTeX rendu côté serveur (plus de 3184089 en brut) - Callouts colorés, toggles, colonnes, code blocks rendus correctement - Barre sticky avec logo + bouton Signaler - Temps de lecture estimé - Footer Momento - Responsive Modération: - content-moderation.service.ts — IA classe safe/flagged/blocked - Sera appelée automatiquement à la publication Signalement: - Page /p/[slug]/report — formulaire de signalement public - API /api/notes/report — stocke notification au propriétaire + admins - 8 motifs: illegal, haine, violence, sexuel, harcèlement, copyright, spam, autre i18n: FR/EN
This commit is contained in:
81
memento-note/app/(public)/p/[slug]/report/page.tsx
Normal file
81
memento-note/app/(public)/p/[slug]/report/page.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Flag, X, Loader2, Check } from 'lucide-react'
|
||||
|
||||
export default function ReportPage({ params }: { params: { slug: string } }) {
|
||||
const [reason, setReason] = useState('')
|
||||
const [details, setDetails] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [done, setDone] = useState(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await fetch('/api/notes/report', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug: params.slug, reason, details }),
|
||||
})
|
||||
setDone(true)
|
||||
} catch {}
|
||||
finally { setSubmitting(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#FAF9F5', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
|
||||
<div style={{ maxWidth: '440px', width: '100%', background: 'white', borderRadius: '16px', border: '1px solid rgba(0,0,0,0.08)', padding: '32px' }}>
|
||||
{done ? (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ width: '48px', height: '48px', borderRadius: '50%', background: '#f0fdf4', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
|
||||
<Check size={24} style={{ color: '#22c55e' }} />
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Georgia, serif', fontSize: '18px', fontWeight: 600, margin: '0 0 8px' }}>Signalement envoyé</h2>
|
||||
<p style={{ fontSize: '14px', color: '#666', margin: 0 }}>Merci. Notre équipe va examiner ce contenu dans les plus brefs délais.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '20px' }}>
|
||||
<div style={{ width: '32px', height: '32px', borderRadius: '8px', background: '#fef2f2', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Flag size={16} style={{ color: '#ef4444' }} />
|
||||
</div>
|
||||
<h2 style={{ fontFamily: 'Georgia, serif', fontSize: '17px', fontWeight: 600, margin: 0 }}>Signaler ce contenu</h2>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ fontSize: '12px', fontWeight: 600, color: '#666', textTransform: 'uppercase', letterSpacing: '0.05em', display: 'block', marginBottom: '8px' }}>Motif</label>
|
||||
<select value={reason} onChange={e => setReason(e.target.value)} style={{ width: '100%', padding: '10px 12px', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none', background: 'white' }}>
|
||||
<option value="">Sélectionner...</option>
|
||||
<option value="illegal">Contenu illégal</option>
|
||||
<option value="hate">Discours de haine</option>
|
||||
<option value="violence">Violence / Menaces</option>
|
||||
<option value="sexual">Contenu sexuel</option>
|
||||
<option value="harassment">Harcèlement</option>
|
||||
<option value="copyright">Violation de copyright</option>
|
||||
<option value="spam">Spam / Arnaque</option>
|
||||
<option value="other">Autre</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ fontSize: '12px', fontWeight: 600, color: '#666', textTransform: 'uppercase', letterSpacing: '0.05em', display: 'block', marginBottom: '8px' }}>Détails (optionnel)</label>
|
||||
<textarea value={details} onChange={e => setDetails(e.target.value)} rows={3} placeholder="Expliquez le problème..." style={{ width: '100%', padding: '10px 12px', borderRadius: '8px', border: '1px solid #ddd', fontSize: '14px', outline: 'none', resize: 'none', background: 'white' }} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!reason || submitting}
|
||||
style={{
|
||||
width: '100%', padding: '12px', borderRadius: '10px', border: 'none',
|
||||
background: !reason || submitting ? '#ccc' : '#ef4444',
|
||||
color: 'white', fontSize: '14px', fontWeight: 600, cursor: !reason || submitting ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{submitting ? <Loader2 size={16} className="animate-spin" /> : 'Signaler'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user