Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée. Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect, useCallback, type ReactNode } from 'react'
|
||
import { AnimatePresence } from 'framer-motion'
|
||
import { useRouter, useSearchParams } from 'next/navigation'
|
||
import { toast } from 'sonner'
|
||
import type { Note } from '@/lib/types'
|
||
import { getNoteById } from '@/app/actions/notes'
|
||
import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
|
||
import {
|
||
NOTE_PEEK_OPEN_EVENT,
|
||
NOTE_PEEK_CLOSE_EVENT,
|
||
type NotePeekOpenDetail,
|
||
} from '@/lib/note-peek-sync'
|
||
import { NoteEditorSplitPeek } from './note-editor-split-peek'
|
||
import { useLanguage } from '@/lib/i18n'
|
||
|
||
interface NoteEditorPeekHostProps {
|
||
noteId: string
|
||
fullPage?: boolean
|
||
children: ReactNode
|
||
}
|
||
|
||
export function NoteEditorPeekHost({ noteId, fullPage, children }: NoteEditorPeekHostProps) {
|
||
const router = useRouter()
|
||
const searchParams = useSearchParams()
|
||
const peekNoteId = searchParams.get('peekNote')
|
||
const { t, language } = useLanguage()
|
||
const isRtl = language === 'fa' || language === 'ar'
|
||
const [peekState, setPeekState] = useState<{ note: Note; blockId?: string } | null>(null)
|
||
|
||
const stripPeekFromUrl = useCallback(() => {
|
||
if (!searchParams.get('peekNote')) return
|
||
const params = new URLSearchParams(searchParams.toString())
|
||
params.delete('peekNote')
|
||
const qs = params.toString()
|
||
router.replace(qs ? `/home?${qs}` : '/home', { scroll: false })
|
||
}, [router, searchParams])
|
||
|
||
useEffect(() => {
|
||
const onOpenPeek = (event: Event) => {
|
||
const detail = (event as CustomEvent<NotePeekOpenDetail>).detail
|
||
if (!detail?.noteId) return
|
||
if (detail.noteId === noteId) return
|
||
|
||
void getNoteById(detail.noteId).then((fetched) => {
|
||
if (fetched) {
|
||
setPeekState({ note: fetched, blockId: detail.blockId })
|
||
} else {
|
||
toast.error(t('notePeek.loadFailed'))
|
||
}
|
||
})
|
||
}
|
||
const onClosePeek = () => setPeekState(null)
|
||
|
||
window.addEventListener(NOTE_PEEK_OPEN_EVENT, onOpenPeek)
|
||
window.addEventListener(NOTE_PEEK_CLOSE_EVENT, onClosePeek)
|
||
return () => {
|
||
window.removeEventListener(NOTE_PEEK_OPEN_EVENT, onOpenPeek)
|
||
window.removeEventListener(NOTE_PEEK_CLOSE_EVENT, onClosePeek)
|
||
}
|
||
}, [noteId, t])
|
||
|
||
// Dashboard « Comparer / Lier » : ouvrir la note liée à droite dès que l’éditeur est monté.
|
||
useEffect(() => {
|
||
if (!peekNoteId || peekNoteId === noteId) return
|
||
let cancelled = false
|
||
void getNoteById(peekNoteId).then((fetched) => {
|
||
if (cancelled) return
|
||
if (fetched) {
|
||
setPeekState(prev => (prev?.note.id === fetched.id ? prev : { note: fetched }))
|
||
} else {
|
||
toast.error(t('notePeek.loadFailed'))
|
||
}
|
||
})
|
||
return () => { cancelled = true }
|
||
}, [peekNoteId, noteId, t])
|
||
|
||
const handleClosePeek = useCallback(() => {
|
||
setPeekState(null)
|
||
stripPeekFromUrl()
|
||
}, [stripPeekFromUrl])
|
||
|
||
const handleOpenPeekFully = useCallback(() => {
|
||
if (!peekState) return
|
||
window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
|
||
detail: { noteId, reason: 'before-peek-full-open' },
|
||
}))
|
||
const params = new URLSearchParams(searchParams.toString())
|
||
params.set('openNote', peekState.note.id)
|
||
params.delete('peekNote')
|
||
router.replace(params.toString() ? `/home?${params.toString()}` : '/home', { scroll: false })
|
||
setPeekState(null)
|
||
}, [noteId, peekState, router, searchParams])
|
||
|
||
const shellClass = fullPage
|
||
? 'flex flex-1 min-h-0 h-full w-full items-stretch overflow-hidden'
|
||
: 'relative flex min-h-0 flex-1 flex-col overflow-hidden'
|
||
|
||
return (
|
||
<div className={`${shellClass}${peekState ? (isRtl ? ' flex-row-reverse' : ' flex-row') : ''}`}>
|
||
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">{children}</div>
|
||
<AnimatePresence initial={false}>
|
||
{peekState && (
|
||
<NoteEditorSplitPeek
|
||
key={peekState.note.id}
|
||
note={peekState.note}
|
||
blockId={peekState.blockId}
|
||
onClose={handleClosePeek}
|
||
onOpenFully={handleOpenPeekFully}
|
||
onBackToDashboard={
|
||
searchParams.get('from') === 'dashboard'
|
||
? () => router.replace('/home')
|
||
: undefined
|
||
}
|
||
/>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)
|
||
}
|