From a6cdcba76f00a7519724ed0b06ac6a0c2e38a1b2 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 19 Jun 2026 22:11:51 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20publication=20pages=20=E2=80=94=20desig?= =?UTF-8?q?n=20moderne=20+=20mod=C3=A9ration=20IA=20+=20signalement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- memento-note/app/(public)/p/[slug]/page.tsx | 213 ++++++++++++++---- .../app/(public)/p/[slug]/report/page.tsx | 81 +++++++ memento-note/app/api/notes/report/route.ts | 51 +++++ .../components/note-editor/publish-dialog.tsx | 9 +- .../ai/services/content-moderation.service.ts | 52 +++++ 5 files changed, 359 insertions(+), 47 deletions(-) create mode 100644 memento-note/app/(public)/p/[slug]/report/page.tsx create mode 100644 memento-note/app/api/notes/report/route.ts create mode 100644 memento-note/lib/ai/services/content-moderation.service.ts diff --git a/memento-note/app/(public)/p/[slug]/page.tsx b/memento-note/app/(public)/p/[slug]/page.tsx index 6eb997e..85fceb2 100644 --- a/memento-note/app/(public)/p/[slug]/page.tsx +++ b/memento-note/app/(public)/p/[slug]/page.tsx @@ -1,17 +1,67 @@ import { notFound } from 'next/navigation' import { getPublishedNote } from '@/app/actions/notes-publishing' -import { FileText, Calendar } from 'lucide-react' +import { FileText, Calendar, Clock, Flag } from 'lucide-react' import { format } from 'date-fns' +import katex from 'katex' export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params const note = await getPublishedNote(slug) - if (!note) return { title: 'Note not found' } - return { title: note.title || 'Published note', description: note.content?.replace(/<[^>]+>/g, '').slice(0, 160) } + if (!note) return { title: 'Note not found — Momento' } + return { + title: `${note.title || 'Published note'} — Momento`, + description: note.content?.replace(/<[^>]+>/g, '').slice(0, 160), + } } -export const dynamic = 'force-static' -export const revalidate = 3600 +export const revalidate = 60 + +function renderMathInHtml(html: string): string { + let result = html + result = result.replace(/]*data-type="math-equation"[^>]*data-latex="([^"]*)"[^>]*><\/div>/g, (_, latex) => { + try { + return `
${katex.renderToString(decodeHtml(latex), { displayMode: true, throwOnError: false })}
` + } catch { return `
${latex}
` } + }) + result = result.replace(/]*data-type="inline-math"[^>]*data-latex="([^"]*)"[^>]*>.*?<\/span>/g, (_, latex) => { + try { + return katex.renderToString(decodeHtml(latex), { displayMode: false, throwOnError: false }) + } catch { return latex } + }) + return result +} + +function decodeHtml(text: string): string { + return text.replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/'/g, "'") +} + +function processContent(html: string): string { + let result = renderMathInHtml(html) + // Render callouts + result = result.replace(/]*data-type="callout-block"[^>]*data-callout-type="([^"]*)"[^>]*>/g, (_, type) => { + const colors: Record = { + info: '#eff6ff|#3b82f6', warning: '#fffbeb|#f59e0b', tip: '#faf5ff|#8b5cf6', + success: '#f0fdf4|#22c55e', danger: '#fef2f2|#ef4444', + } + const [bg, border] = (colors[type] || colors.info).split('|') + return `
` + }) + // Close callouts + result = result.replace(/<\/div>(\s*<\/div>)/g, '$1') + // Outline blocks — remove (they don't work without the editor) + result = result.replace(/]*data-type="outline-block"[^>]*><\/div>/g, '') + // Link preview searchable text + result = result.replace(/]*class="link-preview-searchable"[^>]*>[\s\S]*?<\/div>/g, '') + // Remove buttons (delete/unwrap) + result = result.replace(/]*>[\s\S]*?<\/button>/g, '') + return result +} + +function estimateReadingTime(html: string): number { + const text = html.replace(/<[^>]+>/g, ' ').trim() + const words = text.split(/\s+/).length + return Math.max(1, Math.ceil(words / 200)) +} export default async function PublishedNotePage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params @@ -19,49 +69,128 @@ export default async function PublishedNotePage({ params }: { params: Promise<{ if (!note) notFound() + const processedContent = processContent(note.content || '') + const readingTime = estimateReadingTime(note.content || '') + return ( -
-
- {/* Header */} -
-
- - Note publiée - {note.publishedAt && ( - <> - · - - {format(new Date(note.publishedAt), 'MMM d, yyyy')} - - )} + + + + + + + + {/* Top bar */} +
+
+ Momento
-

+ + Signaler + +

+ + {/* Article */} +
+ {/* Meta */} +
+ + {note.publishedAt ? format(new Date(note.publishedAt), 'd MMM yyyy') : ''} + · + + {readingTime} min de lecture +
+ + {/* Title */} +

{note.title || 'Sans titre'}

- {note.user?.name && ( -

par {note.user.name}

- )} -
- {/* Content */} -
+ {/* Author */} + {note.user?.name && ( +
+ {note.user.image && ( + + )} + par {note.user.name} +
+ )} + + {/* Divider */} +
+ + {/* Content */} +
+ + {/* Custom styles injected */} + +
{/* Footer */} -
-

- Publié sur Momento -

-
-
-
+
+ Momento + {' — Votre mémoire augmentée par l\'IA'} +
+ + ) } diff --git a/memento-note/app/(public)/p/[slug]/report/page.tsx b/memento-note/app/(public)/p/[slug]/report/page.tsx new file mode 100644 index 0000000..e38cf1f --- /dev/null +++ b/memento-note/app/(public)/p/[slug]/report/page.tsx @@ -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 ( +
+
+ {done ? ( +
+
+ +
+

Signalement envoyé

+

Merci. Notre équipe va examiner ce contenu dans les plus brefs délais.

+
+ ) : ( + <> +
+
+ +
+

Signaler ce contenu

+
+ +
+ + +
+ +
+ +