feat: dashboard Second Brain, essai 7 jours et vérification e-mail
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>
This commit is contained in:
@@ -24,10 +24,19 @@ interface NoteEditorPeekHostProps {
|
||||
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
|
||||
@@ -52,9 +61,25 @@ export function NoteEditorPeekHost({ noteId, fullPage, children }: NoteEditorPee
|
||||
}
|
||||
}, [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
|
||||
@@ -63,6 +88,7 @@ export function NoteEditorPeekHost({ noteId, fullPage, children }: NoteEditorPee
|
||||
}))
|
||||
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])
|
||||
@@ -82,6 +108,11 @@ export function NoteEditorPeekHost({ noteId, fullPage, children }: NoteEditorPee
|
||||
blockId={peekState.blockId}
|
||||
onClose={handleClosePeek}
|
||||
onOpenFully={handleOpenPeekFully}
|
||||
onBackToDashboard={
|
||||
searchParams.get('from') === 'dashboard'
|
||||
? () => router.replace('/home')
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { X, Maximize2 } from 'lucide-react'
|
||||
import { X, Maximize2, LayoutGrid } from 'lucide-react'
|
||||
import type { Note } from '@/lib/types'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { NoteEditorProvider, useNoteEditorContext } from './note-editor-context'
|
||||
@@ -17,6 +17,7 @@ interface NoteEditorSplitPeekProps {
|
||||
blockId?: string
|
||||
onClose: () => void
|
||||
onOpenFully: () => void
|
||||
onBackToDashboard?: () => void
|
||||
}
|
||||
|
||||
function PeekEditorBody({ blockId }: { blockId?: string }) {
|
||||
@@ -54,7 +55,7 @@ function PeekEditorBody({ blockId }: { blockId?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function NoteEditorSplitPeek({ note, blockId, onClose, onOpenFully }: NoteEditorSplitPeekProps) {
|
||||
export function NoteEditorSplitPeek({ note, blockId, onClose, onOpenFully, onBackToDashboard }: NoteEditorSplitPeekProps) {
|
||||
const { t, language } = useLanguage()
|
||||
const isRtl = language === 'fa' || language === 'ar'
|
||||
|
||||
@@ -77,6 +78,16 @@ export function NoteEditorSplitPeek({ note, blockId, onClose, onOpenFully }: Not
|
||||
{t('notePeek.label')}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{onBackToDashboard && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBackToDashboard}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wide text-ink dark:text-dark-ink hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<LayoutGrid size={12} />
|
||||
{t('notes.backToDashboard')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenFully}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useNoteEditorContext } from './note-editor-context'
|
||||
import { LabelManager } from '@/components/label-manager'
|
||||
import { LabelBadge } from '@/components/label-badge'
|
||||
@@ -48,6 +49,8 @@ interface NoteEditorToolbarProps {
|
||||
export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachmentsCount }: NoteEditorToolbarProps) {
|
||||
const { state, actions, note, readOnly, fullPage, notebooks, fileInputRef, richTextEditorRef } = useNoteEditorContext()
|
||||
const { t, language } = useLanguage()
|
||||
const searchParams = useSearchParams()
|
||||
const fromDashboard = searchParams.get('from') === 'dashboard' || Boolean(searchParams.get('peekNote'))
|
||||
const { requestAiConsent } = useAiConsent()
|
||||
const [isConverting, setIsConverting] = useState(false)
|
||||
const [shareOpen, setShareOpen] = useState(false)
|
||||
@@ -355,19 +358,24 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
|
||||
const handlePublishInteractivePage = async () => {
|
||||
if (publishLoading) return
|
||||
const consented = await requestAiConsent()
|
||||
if (!consented) return
|
||||
if (state.isDirty && !state.isSaving) {
|
||||
await actions.handleSaveInPlace()
|
||||
setPublishLoading(true)
|
||||
try {
|
||||
const consented = await requestAiConsent()
|
||||
if (!consented) return
|
||||
if (state.isDirty && !state.isSaving) {
|
||||
await actions.handleSaveInPlace()
|
||||
}
|
||||
const html =
|
||||
richTextEditorRef?.current?.getEditor()?.getHTML?.() ||
|
||||
state.content ||
|
||||
note.content ||
|
||||
''
|
||||
setInteractivePageContent(html)
|
||||
setPublishOpen(false)
|
||||
setInteractivePageOpen(true)
|
||||
} finally {
|
||||
setPublishLoading(false)
|
||||
}
|
||||
const html =
|
||||
richTextEditorRef?.current?.getEditor()?.getHTML?.() ||
|
||||
state.content ||
|
||||
note.content ||
|
||||
''
|
||||
setInteractivePageContent(html)
|
||||
setPublishOpen(false)
|
||||
setInteractivePageOpen(true)
|
||||
}
|
||||
|
||||
const handlePublishWithAi = async () => {
|
||||
@@ -564,7 +572,9 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
className="flex items-center gap-2 text-foreground hover:opacity-60 transition-opacity"
|
||||
>
|
||||
<ArrowLeft size={18} />
|
||||
<span className="text-sm font-medium">{t('notes.backToCollection')}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{fromDashboard ? t('notes.backToDashboard') : t('notes.backToCollection')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
|
||||
Reference in New Issue
Block a user