Files
Momento/memento-note/components/note-editor/publish-dialog.tsx
Antigravity 324bf40658
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m20s
CI / Deploy production (on server) (push) Successful in 24s
fix: replace raw ai_consent_required code with translatable message and consent prompt
- Centralize AI consent 403 responses in server-consent helpers
- Return human-readable message + code + i18n errorKey from all AI APIs
- Update dashboard and AI clients to detect code/errorKey and translate
- Add consent.ai.requiredToast i18n keys (fr/en)
2026-07-18 17:32:00 +00:00

144 lines
6.4 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { Globe, X, Copy, Check, Loader2, ExternalLink } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { toast } from 'sonner'
import { copyTextToClipboard } from '@/lib/editor/copy-text-to-clipboard'
interface PublishDialogProps {
open: boolean
onClose: () => void
noteId: string
noteTitle: string
isPublic: boolean
publicSlug: string | null
}
export function PublishDialog({ open, onClose, noteId, noteTitle, isPublic: initialPublic, publicSlug }: PublishDialogProps) {
const { t } = useLanguage()
const [isPublic, setIsPublic] = useState(initialPublic)
const [slug, setSlug] = useState(publicSlug)
const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false)
useEffect(() => { setIsPublic(initialPublic); setSlug(publicSlug) }, [initialPublic, publicSlug])
const publicUrl = slug ? `${window.location.origin}/p/${slug}` : ''
const handlePublish = async () => {
setLoading(true)
try {
const res = await fetch('/api/notes/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ noteId, action: 'publish' }),
})
const data = await res.json()
if (res.ok && data.slug) {
setIsPublic(true)
setSlug(data.slug)
toast.success(t('richTextEditor.publishSuccess') || 'Note publiée !')
} else if (data.error === 'blocked') {
toast.error(t('richTextEditor.publishBlocked') || 'Publication refusée', {
description: data.reason || 'Le contenu ne respecte pas les règles.',
duration: 6000,
})
} else {
toast.error(data.errorKey ? t(data.errorKey) : (data.error || 'Erreur'))
}
} catch { toast.error('Erreur') }
finally { setLoading(false) }
}
const handleUnpublish = async () => {
setLoading(true)
try {
const res = await fetch('/api/notes/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ noteId, action: 'unpublish' }),
})
if (res.ok) {
setIsPublic(false)
setSlug(null)
toast.success(t('richTextEditor.unpublishSuccess') || 'Note dépubliée')
} else { toast.error('Erreur') }
} catch { toast.error('Erreur') }
finally { setLoading(false) }
}
const copyLink = async () => {
const ok = await copyTextToClipboard(publicUrl)
if (ok) { setCopied(true); setTimeout(() => setCopied(false), 2000); toast.success('Lien copié !') }
}
return createPortal(
<div
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40 backdrop-blur-sm"
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
>
<div
className="bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden border border-black/10 dark:border-white/10"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-black/5 dark:border-white/5">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-brand-accent/10 flex items-center justify-center">
<Globe size={15} className="text-brand-accent" />
</div>
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">{t('richTextEditor.publishTitle') || 'Publication publique'}</h3>
</div>
<button onClick={onClose} className="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 text-gray-400">
<X size={16} />
</button>
</div>
{/* Body */}
<div className="px-5 py-4">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 leading-relaxed">
{t('richTextEditor.publishDesc') || 'Publiez cette note sur une URL publique. Tout le monde avec le lien pourra la lire.'}
</p>
{isPublic && slug ? (
<div className="space-y-3">
<div className="flex items-center gap-2 p-2.5 rounded-xl bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800/50">
<Check size={14} className="text-green-500 shrink-0" />
<span className="text-xs font-medium text-green-700 dark:text-green-400">{t('richTextEditor.publishLive') || 'En ligne'}</span>
</div>
<div className="flex items-center gap-1.5 p-2 rounded-xl border border-gray-200 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-800">
<span className="flex-1 text-xs text-gray-500 dark:text-gray-400 truncate px-1">{publicUrl}</span>
<button onClick={copyLink} className="p-1.5 rounded-md hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors shrink-0">
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
</button>
<a href={publicUrl} target="_blank" rel="noopener noreferrer" className="p-1.5 rounded-md hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors shrink-0">
<ExternalLink size={14} />
</a>
</div>
<button
onClick={handleUnpublish}
disabled={loading}
className="w-full py-2 rounded-xl border border-red-200 dark:border-red-800/50 text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/20 transition-colors disabled:opacity-40"
>
{loading ? <Loader2 size={14} className="animate-spin mx-auto" /> : (t('richTextEditor.unpublish') || 'Dépublier')}
</button>
</div>
) : (
<button
onClick={handlePublish}
disabled={loading}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl bg-brand-accent text-white text-sm font-medium hover:bg-brand-accent/90 transition-colors disabled:opacity-40"
>
{loading ? <Loader2 size={14} className="animate-spin" /> : <Globe size={14} />}
{t('richTextEditor.publish') || 'Publier'}
</button>
)}
</div>
</div>
</div>,
document.body
)
}