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:
@@ -19,12 +19,22 @@ interface BridgeNote {
|
||||
}
|
||||
|
||||
const CLUSTER_COLORS = ['#F87171', '#60A5FA', '#34D399', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF']
|
||||
const EASE = [0.16, 1, 0.3, 1] as const
|
||||
|
||||
function clusterPoint(index: number, count: number): { x: number; y: number } {
|
||||
if (count === 1) return { x: 50, y: 42 }
|
||||
const angle = -Math.PI / 2 + (index * 2 * Math.PI) / count
|
||||
const rx = count <= 3 ? 28 : 36
|
||||
const ry = count <= 3 ? 24 : 30
|
||||
return { x: 50 + Math.cos(angle) * rx, y: 44 + Math.sin(angle) * ry }
|
||||
}
|
||||
|
||||
export interface DashboardMindOrbitProps {
|
||||
clusters: Cluster[]
|
||||
bridgeNotes: BridgeNote[]
|
||||
loading?: boolean
|
||||
onOpenInsights: () => void
|
||||
onOpenCluster?: (clusterId: number) => void
|
||||
onNoteSelect: (id: string) => void
|
||||
prefersReducedMotion?: boolean
|
||||
}
|
||||
@@ -34,6 +44,7 @@ export function DashboardMindOrbit({
|
||||
bridgeNotes,
|
||||
loading,
|
||||
onOpenInsights,
|
||||
onOpenCluster,
|
||||
onNoteSelect,
|
||||
prefersReducedMotion,
|
||||
}: DashboardMindOrbitProps) {
|
||||
@@ -43,6 +54,8 @@ export function DashboardMindOrbit({
|
||||
.slice(0, 5)
|
||||
const maxCount = topClusters[0]?.noteIds.length || 1
|
||||
const topBridge = bridgeNotes[0]
|
||||
const reduced = !!prefersReducedMotion
|
||||
const points = topClusters.map((_, idx) => clusterPoint(idx, topClusters.length))
|
||||
|
||||
if (loading) {
|
||||
return <div className="h-[180px] rounded-2xl bg-stone-50 dark:bg-zinc-950/30 animate-pulse" />
|
||||
@@ -80,21 +93,64 @@ export function DashboardMindOrbit({
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 min-h-[100px] py-2">
|
||||
<motion.div
|
||||
className="relative h-[176px]"
|
||||
initial={reduced ? false : { clipPath: 'circle(0% at 50% 44%)' }}
|
||||
animate={{ clipPath: 'circle(120% at 50% 44%)' }}
|
||||
transition={{ duration: reduced ? 0 : 0.62, ease: EASE }}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
className="absolute inset-0 w-full h-full pointer-events-none"
|
||||
aria-hidden
|
||||
>
|
||||
{points.map((point, idx) => {
|
||||
const color = CLUSTER_COLORS[topClusters[idx].clusterId % CLUSTER_COLORS.length]
|
||||
return (
|
||||
<motion.path
|
||||
key={`spoke-${topClusters[idx].clusterId}`}
|
||||
d={`M 50 44 L ${point.x} ${point.y}`}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={0.7}
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
initial={reduced ? false : { pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 0.4 }}
|
||||
transition={{ duration: reduced ? 0 : 0.45, delay: reduced ? 0 : 0.12 + idx * 0.05, ease: EASE }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
|
||||
<div
|
||||
className="absolute left-1/2 top-[44%] w-2.5 h-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-brand-accent shadow-[0_2px_8px_rgba(164,113,72,0.45)] pointer-events-none"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
{topClusters.map((cluster, idx) => {
|
||||
const color = CLUSTER_COLORS[cluster.clusterId % CLUSTER_COLORS.length]
|
||||
const scale = 0.65 + (cluster.noteIds.length / maxCount) * 0.55
|
||||
const size = Math.round(56 * scale)
|
||||
const size = Math.round(52 * scale)
|
||||
const label = cluster.name || `${t('homeDashboard.theme')} ${cluster.clusterId + 1}`
|
||||
const point = points[idx]
|
||||
return (
|
||||
<motion.button
|
||||
key={cluster.clusterId}
|
||||
type="button"
|
||||
whileHover={prefersReducedMotion ? undefined : { scale: 1.06 }}
|
||||
whileTap={prefersReducedMotion ? undefined : { scale: 0.97 }}
|
||||
onClick={onOpenInsights}
|
||||
className="relative flex flex-col items-center gap-1.5 group"
|
||||
style={{ width: size + 16 }}
|
||||
whileHover={reduced ? undefined : { scale: 1.06 }}
|
||||
whileTap={reduced ? undefined : { scale: 0.97 }}
|
||||
onClick={() => (onOpenCluster ? onOpenCluster(cluster.clusterId) : onOpenInsights())}
|
||||
className="absolute flex flex-col items-center gap-1 group"
|
||||
style={{
|
||||
left: `${point.x}%`,
|
||||
top: `${point.y}%`,
|
||||
width: size + 16,
|
||||
}}
|
||||
initial={reduced ? { x: '-50%', y: '-50%' } : { x: '-50%', y: '-50%', scale: 0.82, opacity: 0.35 }}
|
||||
animate={{ x: '-50%', y: '-50%', scale: 1, opacity: 1 }}
|
||||
transition={{ duration: reduced ? 0 : 0.38, delay: reduced ? 0 : 0.18 + idx * 0.05, ease: EASE }}
|
||||
>
|
||||
<div
|
||||
className="rounded-full border-2 flex items-center justify-center font-mono font-bold text-white shadow-sm group-hover:shadow-md transition-shadow"
|
||||
@@ -114,13 +170,16 @@ export function DashboardMindOrbit({
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{topBridge?.note && (
|
||||
<button
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={() => onNoteSelect(topBridge.noteId)}
|
||||
className="w-full mt-2 p-2.5 rounded-xl border border-brand-accent/20 bg-brand-accent/[0.04] hover:bg-brand-accent/[0.08] transition-all text-start flex items-center gap-2 group"
|
||||
initial={reduced ? false : { opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: reduced ? 0 : 0.32, delay: reduced ? 0 : 0.48, ease: EASE }}
|
||||
>
|
||||
<Zap size={11} className="text-brand-accent shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -132,7 +191,7 @@ export function DashboardMindOrbit({
|
||||
<span className="text-[8px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded-full shrink-0">
|
||||
{Math.round(topBridge.bridgeScore * 100)}%
|
||||
</span>
|
||||
</button>
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user