- Bloc Link Preview : colle une URL → carte avec titre, description, image, favicon - API /api/link-preview : extraction OpenGraph + meta tags - API /api/image-proxy : contourne le hotlinking (Referer spoofing) - Métadonnées persistées en HTML (data-preview JSON) — pas de refetch au reload - Texte indexable : titre + description + URL inclus pour recherche/embeddings - Modal propre pour saisir l'URL (plus de prompt()) - Slash menu + smart paste 'Coller comme carte aperçu' - i18n FR/EN complet - Fix: bouton calendrier retiré du sélecteur de vue
131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { Link2, ImageIcon, Video, Code, FileText, Layout } from 'lucide-react'
|
|
|
|
export type SmartPasteExtendedMenuProps = {
|
|
type: 'url' | 'code'
|
|
text: string
|
|
anchor: { top: number; left: number }
|
|
isImage?: boolean
|
|
isVideo?: boolean
|
|
onLink?: () => void
|
|
onLinkPreview?: () => void
|
|
onImage?: () => void
|
|
onVideo?: () => void
|
|
onCodeBlock?: () => void
|
|
onPlain: () => void
|
|
onClose: () => void
|
|
}
|
|
|
|
export function SmartPasteExtendedMenu({
|
|
type,
|
|
text,
|
|
anchor,
|
|
isImage,
|
|
isVideo,
|
|
onLink,
|
|
onLinkPreview,
|
|
onImage,
|
|
onVideo,
|
|
onCodeBlock,
|
|
onPlain,
|
|
onClose,
|
|
}: SmartPasteExtendedMenuProps) {
|
|
const { t } = useLanguage()
|
|
const menuRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
onClose()
|
|
}
|
|
}
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') onClose()
|
|
}
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside)
|
|
document.removeEventListener('keydown', handleKeyDown)
|
|
}
|
|
}, [onClose])
|
|
|
|
const menuStyle: React.CSSProperties = {
|
|
position: 'fixed',
|
|
left: anchor.left,
|
|
top: anchor.top + 8,
|
|
zIndex: 9999,
|
|
maxWidth: 340,
|
|
width: '100%',
|
|
}
|
|
|
|
if (Number(menuStyle.left) + 340 > window.innerWidth) {
|
|
menuStyle.left = Math.max(8, window.innerWidth - 348)
|
|
}
|
|
// Estimation de hauteur pour éviter les débordements
|
|
const expectedHeight = type === 'url' ? (200) : (140)
|
|
if (Number(menuStyle.top) + expectedHeight > window.innerHeight) {
|
|
menuStyle.top = Math.max(8, anchor.top - expectedHeight - 8)
|
|
}
|
|
|
|
const shortenedText = text.length > 50 ? `${text.slice(0, 47)}...` : text
|
|
|
|
return createPortal(
|
|
<div ref={menuRef} style={menuStyle} className="block-action-menu smart-paste-menu">
|
|
<p className="smart-paste-menu__hint font-semibold">
|
|
{type === 'url' ? t('richTextEditor.smartPasteUrlTitle') || 'Lien ou Média détecté' : t('richTextEditor.smartPasteCodeTitle') || 'Code source détecté'}
|
|
</p>
|
|
<p className="smart-paste-menu__source text-xs truncate max-w-full" title={text}>
|
|
{shortenedText}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground px-3 mb-2">
|
|
{type === 'url' ? t('richTextEditor.smartPasteUrlHint') || 'Que souhaitez-vous faire avec ce lien ?' : t('richTextEditor.smartPasteCodeHint') || 'Du code source a été détecté. Souhaitez-vous l\'insérer comme bloc de code ?'}
|
|
</p>
|
|
|
|
{type === 'url' && (
|
|
<>
|
|
<button type="button" className="block-action-item" onClick={onLink}>
|
|
<Link2 size={15} className="text-blue-500" />
|
|
<span>{t('richTextEditor.smartPasteUrlLink') || 'Coller comme lien hypertexte'}</span>
|
|
</button>
|
|
{onLinkPreview && (
|
|
<button type="button" className="block-action-item" onClick={onLinkPreview}>
|
|
<Layout size={15} className="text-indigo-500" />
|
|
<span>{t('richTextEditor.smartPasteUrlPreview') || 'Coller comme carte aperçu'}</span>
|
|
</button>
|
|
)}
|
|
{isImage && onImage && (
|
|
<button type="button" className="block-action-item" onClick={onImage}>
|
|
<ImageIcon size={15} className="text-emerald-500" />
|
|
<span>{t('richTextEditor.smartPasteUrlImage') || 'Insérer comme image'}</span>
|
|
</button>
|
|
)}
|
|
{isVideo && onVideo && (
|
|
<button type="button" className="block-action-item" onClick={onVideo}>
|
|
<Video size={15} className="text-rose-500" />
|
|
<span>{t('richTextEditor.smartPasteUrlVideo') || 'Insérer comme lecteur vidéo'}</span>
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{type === 'code' && onCodeBlock && (
|
|
<button type="button" className="block-action-item" onClick={onCodeBlock}>
|
|
<Code size={15} className="text-purple-500" />
|
|
<span>{t('richTextEditor.smartPasteCodeBlock') || 'Insérer comme bloc de code'}</span>
|
|
</button>
|
|
)}
|
|
|
|
<button type="button" className="block-action-item border-t border-border/50 mt-1 pt-1" onClick={onPlain}>
|
|
<FileText size={15} className="text-muted-foreground" />
|
|
<span>{t('richTextEditor.smartPastePlain') || 'Coller en texte brut'}</span>
|
|
</button>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
}
|