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>
117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { motion, AnimatePresence } from 'motion/react'
|
|
import { Pencil, CreditCard, Lightbulb, Check, ArrowRight, Sparkles } from 'lucide-react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface Props {
|
|
onDone: () => void
|
|
onTry: (href: string) => void
|
|
}
|
|
|
|
const ACTIONS = [
|
|
{ icon: Pencil, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'write', href: '/home' },
|
|
{ icon: CreditCard, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'flashcards', href: '/revision' },
|
|
{ icon: Lightbulb, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'brainstorm', href: '/brainstorm' },
|
|
]
|
|
|
|
export function OnboardingStepFeatures({ onDone, onTry }: Props) {
|
|
const { t } = useLanguage()
|
|
const [checked, setChecked] = useState<Set<string>>(new Set())
|
|
|
|
function handleTry(key: string, href: string) {
|
|
setChecked(prev => new Set([...prev, key]))
|
|
onTry(href)
|
|
}
|
|
|
|
const allChecked = checked.size === ACTIONS.length
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -16 }}
|
|
className="flex flex-col items-center gap-5 w-full"
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.7, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
transition={{ delay: 0.1, type: 'spring', stiffness: 200 }}
|
|
className="flex h-20 w-20 items-center justify-center rounded-2xl bg-amber-500/10 text-amber-500"
|
|
>
|
|
<Sparkles className="h-10 w-10" />
|
|
</motion.div>
|
|
|
|
<div className="space-y-1 text-center">
|
|
<h2 className="text-2xl font-bold tracking-tight text-foreground">
|
|
{t('onboarding.step_features_title')}
|
|
</h2>
|
|
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
|
{t('onboarding.step_features_subtitle')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2.5 w-full">
|
|
{ACTIONS.map(({ icon: Icon, color, bg, key, href }, i) => {
|
|
const done = checked.has(key)
|
|
return (
|
|
<motion.div
|
|
key={key}
|
|
initial={{ opacity: 0, x: -12 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: 0.08 + i * 0.08 }}
|
|
className={`flex items-center gap-3 rounded-xl border p-3 transition-all ${
|
|
done ? 'border-emerald-500/30 bg-emerald-500/5' : 'border-border bg-muted/10 hover:border-border/80'
|
|
}`}
|
|
>
|
|
{/* Checkmark */}
|
|
<span className={`shrink-0 flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors ${
|
|
done ? 'border-emerald-500 bg-emerald-500' : 'border-border'
|
|
}`}>
|
|
<AnimatePresence>
|
|
{done && (
|
|
<motion.span initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
|
|
<Check className="h-3 w-3 text-white" />
|
|
</motion.span>
|
|
)}
|
|
</AnimatePresence>
|
|
</span>
|
|
|
|
{/* Icon */}
|
|
<span className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${bg} ${color}`}>
|
|
<Icon className="h-4 w-4" />
|
|
</span>
|
|
|
|
{/* Text */}
|
|
<div className="flex-1 min-w-0">
|
|
<p className={`text-sm font-medium leading-tight ${done ? 'text-muted-foreground' : 'text-foreground'}`}>
|
|
{t(`onboarding.action_${key}_title`)}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground leading-tight mt-0.5">
|
|
{t(`onboarding.action_${key}_desc`)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Essayer — minimise wizard + navigue */}
|
|
<button
|
|
onClick={() => handleTry(key, href)}
|
|
className={`shrink-0 flex items-center gap-1 text-xs font-medium px-2.5 py-1.5 rounded-lg transition-opacity ${color} ${bg} ${done ? 'opacity-40' : 'hover:opacity-80'}`}
|
|
>
|
|
{done ? t('onboarding.action_done') : t('onboarding.action_try')}
|
|
{!done && <ArrowRight className="h-3 w-3" />}
|
|
</button>
|
|
</motion.div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<Button onClick={onDone} size="lg" className="w-full gap-2 mt-1">
|
|
{allChecked ? t('onboarding.step_features_cta_all') : t('onboarding.step_features_cta')}
|
|
<ArrowRight className="h-4 w-4" />
|
|
</Button>
|
|
</motion.div>
|
|
)
|
|
}
|