Files
Momento/memento-note/components/onboarding/onboarding-editor-hints.tsx
Antigravity 6b4ed8514f
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m37s
CI / Deploy production (on server) (push) Has been cancelled
Epic 6: Stories 6-2 (Markdown roundtrip) + 6-3 (Brainstorm PPTX + Canvas)
Story 6-2 — Markdown roundtrip export/import:
- lib/editor/markdown-export.ts: tiptapHTMLToMarkdown, markdownToHTML, looksLikeMarkdown
- lib/editor/markdown-paste-extension.ts: TipTap extension paste Markdown → blocs
- note-editor-toolbar.tsx: export .md + import .md (file picker)
- rich-text-editor.tsx: intégration MarkdownPasteExtension
- 40 tests unitaires markdown-export.test.ts

Story 6-3 — Brainstorm PPTX + Canvas:
- lib/brainstorm/export-pptx.ts: génération PPTX 5 slides (pptxgenjs)
- app/api/brainstorm/[sessionId]/export-pptx/route.ts: route POST protégée
- brainstorm-page.tsx: bouton PPTX, auto-select session, fix emoji, fix router.replace
- wave-canvas.tsx: fitTrigger recentrage, légende bas-droite

Onboarding activation wizard (Story 6-1):
- components/onboarding/: wizard multi-étapes, hints éditeur
- app/api/onboarding/: route PATCH onboarding
- prisma/migrations: champs onboarding user

Locales: 15 langues mises à jour (brainstorm, markdown, onboarding keys)
Sprint: 6-1 done, 6-2 review, 6-3 review

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 11:24:56 +00:00

233 lines
8.7 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { useSession } from 'next-auth/react'
import { usePathname } from 'next/navigation'
import {
Slash, Sparkles, History, GraduationCap, Link2,
PenLine, FlipVertical, Keyboard, Lightbulb, ArrowRight,
FileDown, Network, Zap, RefreshCw,
X, ChevronLeft, ChevronRight,
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import type { LucideIcon } from 'lucide-react'
// ── Types ──────────────────────────────────────────────────────────────────
interface HintDef {
icon: LucideIcon
color: string
bg: string
key: string
}
interface RouteHintSet {
hints: HintDef[]
storageKey: string
}
// ── Hint definitions per route ─────────────────────────────────────────────
// Every item here maps to a real, verified UI element or gesture in the codebase.
const ROUTE_HINTS: Record<string, RouteHintSet> = {
'/home': {
storageKey: 'momento_hints_home',
hints: [
{ icon: PenLine, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'create_note' },
{ icon: Slash, color: 'text-indigo-500', bg: 'bg-indigo-500/10', key: 'slash' },
{ icon: Sparkles, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'ai' },
{ icon: History, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'version' },
{ icon: Link2, color: 'text-rose-500', bg: 'bg-rose-500/10', key: 'links' },
{ icon: GraduationCap, color: 'text-emerald-500',bg: 'bg-emerald-500/10',key: 'flashcards' },
],
},
'/revision': {
storageKey: 'momento_hints_revision',
hints: [
{ icon: FlipVertical, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'flip' },
{ icon: Keyboard, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'rate_keys' },
{ icon: GraduationCap,color: 'text-emerald-500',bg: 'bg-emerald-500/10',key: 'generate_from_note' },
],
},
'/brainstorm': {
storageKey: 'momento_hints_brainstorm',
hints: [
{ icon: Lightbulb, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'brainstorm_start' },
{ icon: ArrowRight, color: 'text-violet-500',bg: 'bg-violet-500/10', key: 'brainstorm_deepen' },
{ icon: FileDown, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'brainstorm_export' },
],
},
'/insights': {
storageKey: 'momento_hints_insights',
hints: [
{ icon: Network, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'insights_clusters' },
{ icon: Zap, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'insights_bridge' },
{ icon: RefreshCw, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'insights_refresh' },
],
},
}
function getRouteSet(pathname: string): RouteHintSet | null {
// Exact match first
if (ROUTE_HINTS[pathname]) return ROUTE_HINTS[pathname]
// Prefix match (e.g. /brainstorm?session=xxx)
for (const route of Object.keys(ROUTE_HINTS)) {
if (pathname.startsWith(route)) return ROUTE_HINTS[route]
}
return null
}
// ── Component ──────────────────────────────────────────────────────────────
export function OnboardingEditorHints() {
const { data: session } = useSession()
const pathname = usePathname()
const { t } = useLanguage()
const [visible, setVisible] = useState(false)
const [index, setIndex] = useState(0)
const [routeKey, setRouteKey] = useState<string | null>(null)
const user = session?.user as any
// When route changes, check if we should show hints for the new page
useEffect(() => {
if (!session) return
if (user?.onboardingCompleted !== false) return
const routeSet = getRouteSet(pathname)
if (!routeSet) {
setVisible(false)
return
}
if (typeof window !== 'undefined' && localStorage.getItem(routeSet.storageKey)) {
setVisible(false)
return
}
// Reset to first hint when page changes
setIndex(0)
setRouteKey(pathname)
const timer = setTimeout(() => setVisible(true), 900)
return () => clearTimeout(timer)
}, [pathname, session, user?.onboardingCompleted])
// Hide when hints change (route switch)
useEffect(() => {
if (routeKey !== null && routeKey !== pathname) {
setVisible(false)
}
}, [pathname, routeKey])
function dismiss() {
const routeSet = getRouteSet(pathname)
if (routeSet && typeof window !== 'undefined') {
localStorage.setItem(routeSet.storageKey, '1')
}
setVisible(false)
}
const routeSet = getRouteSet(pathname)
if (!routeSet) return null
const hints = routeSet.hints
const hint = hints[Math.min(index, hints.length - 1)]
const Icon = hint.icon
return (
<AnimatePresence>
{visible && (
<motion.div
key={`hints-${pathname}`}
initial={{ opacity: 0, x: 60 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 60 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className="fixed bottom-24 right-4 z-[150] w-72 rounded-2xl border border-border bg-background shadow-xl"
>
{/* Header */}
<div className="flex items-center justify-between px-4 pt-3 pb-2 border-b border-border/50">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{t('onboarding.editor_hints_title')}
</p>
<button
onClick={dismiss}
className="rounded p-0.5 text-muted-foreground/40 hover:text-muted-foreground transition-colors"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
{/* Hint content */}
<AnimatePresence mode="wait">
<motion.div
key={hint.key}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.18 }}
className="px-4 py-3 flex items-start gap-3"
>
<span className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${hint.bg} ${hint.color}`}>
<Icon className="h-4 w-4" />
</span>
<div>
<p className="text-sm font-semibold text-foreground">
{t(`onboarding.hint_${hint.key}_title`)}
</p>
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
{t(`onboarding.hint_${hint.key}_desc`)}
</p>
</div>
</motion.div>
</AnimatePresence>
{/* Navigation */}
<div className="flex items-center justify-between px-4 pb-3">
{/* Dots */}
<div className="flex gap-1">
{hints.map((_, i) => (
<button
key={i}
onClick={() => setIndex(i)}
className={`h-1.5 rounded-full transition-all ${
i === index ? 'w-4 bg-violet-500' : 'w-1.5 bg-border'
}`}
/>
))}
</div>
{/* Arrows + dismiss */}
<div className="flex items-center gap-1">
<button
onClick={() => setIndex(i => Math.max(0, i - 1))}
disabled={index === 0}
className="flex h-6 w-6 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-30 transition-colors"
>
<ChevronLeft className="h-3.5 w-3.5" />
</button>
{index < hints.length - 1 ? (
<button
onClick={() => setIndex(i => i + 1)}
className="flex h-6 w-6 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<ChevronRight className="h-3.5 w-3.5" />
</button>
) : (
<button
onClick={dismiss}
className="flex items-center gap-1 text-xs font-medium px-2.5 h-6 rounded-lg bg-violet-500/10 text-violet-500 hover:bg-violet-500/20 transition-colors"
>
{t('onboarding.editor_hints_got_it')}
</button>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
)
}