feat: pages publiées utilisateur (settings) + fix import Shield dupliqué
- /settings/published : l'utilisateur voit ses notes publiées - Copier le lien, ouvrir, dépublier - API /api/user/published - Onglet 'Mes pages' (Globe) dans la nav settings - Fix: import Shield dupliqué dans admin-sidebar.tsx - i18n FR/EN
This commit is contained in:
103
memento-note/app/(main)/settings/published/page.tsx
Normal file
103
memento-note/app/(main)/settings/published/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Globe, ExternalLink, Trash2, Loader2, Copy, Check } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface PublishedNote {
|
||||
id: string
|
||||
title: string | null
|
||||
publicSlug: string | null
|
||||
publishedAt: string | null
|
||||
}
|
||||
|
||||
export default function UserPublishedPage() {
|
||||
const { t } = useLanguage()
|
||||
const [notes, setNotes] = useState<PublishedNote[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/user/published')
|
||||
const data = await res.json()
|
||||
setNotes(data.notes || [])
|
||||
} catch { toast.error('Erreur') }
|
||||
finally { setLoading(false) }
|
||||
}, [])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
const unpublish = async (noteId: string) => {
|
||||
try {
|
||||
const res = await fetch('/api/notes/publish', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ noteId, action: 'unpublish' }),
|
||||
})
|
||||
if (res.ok) { toast.success(t('richTextEditor.unpublishSuccess') || 'Note dépubliée'); load() }
|
||||
else { toast.error('Erreur') }
|
||||
} catch { toast.error('Erreur') }
|
||||
}
|
||||
|
||||
const copyLink = (slug: string, noteId: string) => {
|
||||
const url = `${window.location.origin}/p/${slug}`
|
||||
navigator.clipboard?.writeText(url).then(() => {
|
||||
setCopiedId(noteId); setTimeout(() => setCopiedId(null), 2000); toast.success('Lien copié !')
|
||||
}).catch(() => {
|
||||
const el = document.createElement('textarea'); el.value = url; document.body.appendChild(el); el.select()
|
||||
document.execCommand('copy'); document.body.removeChild(el)
|
||||
setCopiedId(noteId); setTimeout(() => setCopiedId(null), 2000); toast.success('Lien copié !')
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6" dir="auto">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-1">{t('settings.publishedTitle') || 'Mes pages publiées'}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('settings.publishedDesc') || 'Gérez les notes que vous avez publiées publiquement.'}</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8"><Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /></div>
|
||||
) : notes.length === 0 ? (
|
||||
<div className="text-center py-12 rounded-xl border border-dashed border-border">
|
||||
<Globe size={32} className="mx-auto text-muted-foreground/40 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">{t('settings.publishedEmpty') || 'Aucune note publiée pour le moment.'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{notes.map(note => (
|
||||
<div key={note.id} className="flex items-center gap-3 p-3.5 rounded-xl border border-border bg-card">
|
||||
<div className="w-9 h-9 rounded-lg bg-green-50 dark:bg-green-950/20 flex items-center justify-center shrink-0">
|
||||
<Globe size={15} className="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium truncate">{note.title || 'Sans titre'}</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{note.publishedAt ? new Date(note.publishedAt).toLocaleDateString() : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button onClick={() => copyLink(note.publicSlug!, note.id)}
|
||||
className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title="Copier le lien">
|
||||
{copiedId === note.id ? <Check size={15} className="text-green-500" /> : <Copy size={15} />}
|
||||
</button>
|
||||
<a href={`/p/${note.publicSlug}`} target="_blank" rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title="Ouvrir">
|
||||
<ExternalLink size={15} />
|
||||
</a>
|
||||
<button onClick={() => unpublish(note.id)}
|
||||
className="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-950/20 text-muted-foreground hover:text-red-500 transition-colors" title="Dépublier">
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
memento-note/app/api/user/published/route.ts
Normal file
16
memento-note/app/api/user/published/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where: { userId: session.user.id, isPublic: true, trashedAt: null },
|
||||
select: { id: true, title: true, publicSlug: true, publishedAt: true },
|
||||
orderBy: { publishedAt: 'desc' },
|
||||
})
|
||||
|
||||
return NextResponse.json({ notes })
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Shield,
|
||||
Settings,
|
||||
StickyNote,
|
||||
Shield,
|
||||
ArrowLeft,
|
||||
User,
|
||||
LogOut,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Settings, Sparkles, Palette, User, Database, Info, Key, CreditCard, Plug } from 'lucide-react'
|
||||
import { Settings, Sparkles, Palette, User, Database, Info, Key, CreditCard, Plug, Globe } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
@@ -21,6 +21,7 @@ export function SettingsNav({ className }: SettingsNavProps) {
|
||||
{ id: 'appearance', label: t('appearance.title'), icon: <Palette size={14} />, href: '/settings/appearance' },
|
||||
{ id: 'profile', label: t('profile.title'), icon: <User size={14} />, href: '/settings/profile' },
|
||||
{ id: 'data', label: t('dataManagement.title'), icon: <Database size={14} />, href: '/settings/data' },
|
||||
{ id: 'published', label: t('settings.publishedTitle') || 'Mes pages', icon: <Globe size={14} />, href: '/settings/published' },
|
||||
{ id: 'integrations', label: t('integrations.title') || 'Intégrations', icon: <Plug size={14} />, href: '/settings/integrations' },
|
||||
{ id: 'mcp', label: t('mcpSettings.title'), icon: <Key size={14} />, href: '/settings/mcp' },
|
||||
{ id: 'about', label: t('about.title'), icon: <Info size={14} />, href: '/settings/about' },
|
||||
|
||||
@@ -2569,6 +2569,9 @@
|
||||
"publish": "Publish",
|
||||
"publishSuccess": "Note published!",
|
||||
"publishLive": "Live",
|
||||
"settingsPublishedTitle": "My published pages",
|
||||
"settingsPublishedDesc": "Manage your publicly published notes.",
|
||||
"settingsPublishedEmpty": "No published notes yet.",
|
||||
"unpublish": "Unpublish",
|
||||
"unpublishSuccess": "Note unpublished",
|
||||
"slashSubPage": "Sub-page",
|
||||
|
||||
@@ -2573,6 +2573,9 @@
|
||||
"publish": "Publier",
|
||||
"publishSuccess": "Note publiée !",
|
||||
"publishLive": "En ligne",
|
||||
"settingsPublishedTitle": "Mes pages publiées",
|
||||
"settingsPublishedDesc": "Gérez les notes que vous avez publiées publiquement.",
|
||||
"settingsPublishedEmpty": "Aucune note publiée pour le moment.",
|
||||
"unpublish": "Dépublier",
|
||||
"unpublishSuccess": "Note dépubliée",
|
||||
"slashSubPage": "Sous-page",
|
||||
|
||||
Reference in New Issue
Block a user