Files
Momento/memento-note/components/note-editor/publish-dialog.tsx
Antigravity 902fe95a69
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m16s
CI / Deploy production (on server) (push) Successful in 21s
fix: PublishDialog sorti du toolbar div — position fixed correcte
2026-06-20 17:40:34 +00:00

139 lines
6.1 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { Globe, X, Copy, Check, Loader2, ExternalLink } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
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])
if (!open) return null
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)
if (data.moderation === 'flagged') {
toast.success(t('richTextEditor.publishSuccess') || 'Note publiée !', {
description: '⚠️ Un modérateur examinera le contenu sous peu.',
})
} else {
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 de publication.',
duration: 6000,
})
} else {
toast.error(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 (
<div className="fixed inset-0 z-[300] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm" dir="auto" onClick={onClose}>
<div className="w-full max-w-md max-h-[90vh] overflow-y-auto rounded-2xl border border-border bg-card shadow-2xl p-5" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<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">{t('richTextEditor.publishTitle') || 'Publication publique'}</h3>
</div>
<button onClick={onClose} className="p-1 rounded-lg hover:bg-muted text-muted-foreground"><X size={16} /></button>
</div>
<p className="text-xs text-muted-foreground 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-border bg-background">
<span className="flex-1 text-xs text-muted-foreground truncate px-1">{publicUrl}</span>
<button onClick={copyLink} className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground 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-muted text-muted-foreground hover:text-foreground 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>
)
}