feat: pages publiées utilisateur (settings) + fix import Shield dupliqué
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 0s
CI / Deploy production (on server) (push) Has been skipped

- /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:
Antigravity
2026-06-20 07:21:02 +00:00
parent 722cb905e4
commit 3f3d37ebeb
6 changed files with 127 additions and 2 deletions

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

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

View File

@@ -9,7 +9,6 @@ import {
Shield,
Settings,
StickyNote,
Shield,
ArrowLeft,
User,
LogOut,

View File

@@ -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' },

View File

@@ -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",

View File

@@ -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",