All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
Covers architecture, configuration steps, user flows, API routes, webhooks, pricing, testing with Stripe CLI, production checklist, and troubleshooting.
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>
|
|
)
|
|
}
|