Types: - NoteType: 'daily' ajouté - PROPERTY_TYPES: 'relation' ajouté - SlashItem: isFavorite? ajouté - SuggestChartsResponse: error? ajouté Fixes propres: - import/route: Date | null → ?? undefined (3 occ) - notes/route: JSON.stringify sur checkItems/labels - user/export: b.title → b.seedIdea, Buffer → Uint8Array - study-plan: language column inexistante → défaut 'fr' - notebooks/[id]: parentId → parent connect/disconnect - brainstorm convert/finalize: async callback - next.config: @ts-expect-error inutile supprimé - ai-settings: revalidateTag(tag, 'default') Casts (TipTap/Prisma, safe at runtime): - tiptap-chart/math extensions: InputRule/options as any - chat/route: system messages as any - note-graph-view: ForceGraph2D as any - property-value-editor: relation comparison - sanitize-content: TrustedHTML cast
121 lines
4.9 KiB
TypeScript
121 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { motion, AnimatePresence } from 'motion/react'
|
|
import { Activity, Lightbulb, UserPlus, X, Zap, Eye } from 'lucide-react'
|
|
import type { BrainstormActivityItem } from '@/types/brainstorm'
|
|
|
|
interface ActivityFeedProps {
|
|
activities: BrainstormActivityItem[]
|
|
isOpen: boolean
|
|
onToggle: () => void
|
|
t: (key: string) => string | undefined
|
|
}
|
|
|
|
function getActionIcon(action: string) {
|
|
switch (action) {
|
|
case 'manual_idea': return <Lightbulb size={12} className="text-brand-accent" />
|
|
case 'wave_generated': return <Zap size={12} className="text-orange-500" />
|
|
case 'joined': return <UserPlus size={12} className="text-emerald-500" />
|
|
case 'idea_dismissed': return <X size={12} className="text-rose-500" />
|
|
case 'invite_created': return <UserPlus size={12} className="text-violet-500" />
|
|
default: return <Activity size={12} className="text-muted-foreground" />
|
|
}
|
|
}
|
|
|
|
function getActionLabel(action: string, t: (key: string) => string | undefined): string {
|
|
const key = `brainstorm.activity.${action}`
|
|
const translated = t(key)
|
|
if (translated) return translated
|
|
switch (action) {
|
|
case 'manual_idea': return 'added an idea'
|
|
case 'wave_generated': return 'generated a wave'
|
|
case 'joined': return 'joined the session'
|
|
case 'idea_dismissed': return 'dismissed an idea'
|
|
case 'invite_created': return 'created an invite'
|
|
default: return action
|
|
}
|
|
}
|
|
|
|
function timeAgo(dateStr: string, t: (key: string) => string | undefined): string {
|
|
const diff = Date.now() - new Date(dateStr).getTime()
|
|
const mins = Math.floor(diff / 60000)
|
|
if (mins < 1) return t('brainstorm.justNow') || ''
|
|
if (mins < 60) return `${mins}m`
|
|
const hours = Math.floor(mins / 60)
|
|
if (hours < 24) return `${hours}h`
|
|
const days = Math.floor(hours / 24)
|
|
return `${days}d`
|
|
}
|
|
|
|
export function ActivityFeed({ activities, isOpen, onToggle, t }: ActivityFeedProps) {
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ x: 320, opacity: 0 }}
|
|
animate={{ x: 0, opacity: 1 }}
|
|
exit={{ x: 320, opacity: 0 }}
|
|
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
|
className="absolute top-0 right-16 bottom-0 w-[320px] bg-white/95 dark:bg-[#1A1A1A]/95 backdrop-blur-xl border-l border-border z-30 flex flex-col shadow-[-20px_0_40px_rgba(0,0,0,0.05)]"
|
|
>
|
|
<div className="p-6 border-b border-border/40 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Activity size={14} className="text-orange-500" />
|
|
<h3 className="text-[10px] font-bold uppercase tracking-[0.2em] text-foreground">
|
|
{t('brainstorm.activityTitle')}
|
|
</h3>
|
|
</div>
|
|
<button
|
|
onClick={onToggle}
|
|
className="p-1.5 hover:bg-foreground/5 rounded-full transition-colors"
|
|
>
|
|
<X size={14} className="text-muted-foreground" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto">
|
|
{activities.length === 0 ? (
|
|
<div className="p-6 text-center">
|
|
<p className="text-xs italic text-muted-foreground">
|
|
{t('brainstorm.noActivity')}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="py-2">
|
|
{activities.map((item, idx) => (
|
|
<motion.div
|
|
key={item.id}
|
|
initial={{ opacity: 0, x: 10 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: idx * 0.03 }}
|
|
className="px-6 py-3 flex items-start gap-3 hover:bg-foreground/[0.02] transition-colors"
|
|
>
|
|
<div className="mt-0.5 w-5 h-5 rounded-full bg-foreground/5 flex items-center justify-center shrink-0">
|
|
{getActionIcon(item.action)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-xs text-foreground leading-relaxed">
|
|
<span className="font-semibold">
|
|
{item.user?.name || 'AI'}
|
|
</span>{' '}
|
|
{getActionLabel(item.action, t)}
|
|
{item.details?.ideaTitle && (
|
|
<span className="text-muted-foreground italic"> « {item.details.ideaTitle.length > 30 ? item.details.ideaTitle.substring(0, 30) + '…' : item.details.ideaTitle} »</span>
|
|
)}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
{timeAgo(item.createdAt, t)}
|
|
</p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|