Files
Momento/memento-note/components/smart-paste-extended-menu.tsx
Antigravity ba3ab3422a
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m29s
CI / Deploy production (on server) (push) Has been skipped
feat: Link Preview block (carte aperçu URL) + proxy images
- 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
2026-06-14 17:43:53 +00:00

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,
)
}