Spaces:
Paused
Paused
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β IDEA STICKY NOTE β | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β Playful sticky note visualization for incubated ideas β | |
| * β Part of the Liquid UI Arsenal β | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| */ | |
| import { useState } from 'react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { cn } from '@/lib/utils'; | |
| import { | |
| Lightbulb, Sparkles, ArrowUpRight, X, | |
| ThumbsUp, Brain, Tag, Clock, User | |
| } from 'lucide-react'; | |
| export interface Idea { | |
| id: string; | |
| title: string; | |
| hypothesis: string; | |
| confidence: number; | |
| status: 'INCUBATED' | 'PROMOTED' | 'REJECTED' | 'IMPLEMENTED'; | |
| proposedBy: string; | |
| tags?: string[]; | |
| relatedTo?: string; | |
| created_at: string; | |
| votes?: number; | |
| } | |
| export interface IdeaStickyNoteProps { | |
| idea: Idea; | |
| onPromote?: (ideaId: string) => void; | |
| onReject?: (ideaId: string) => void; | |
| onVote?: (ideaId: string) => void; | |
| color?: 'yellow' | 'pink' | 'blue' | 'green' | 'purple'; | |
| tilt?: number; | |
| } | |
| const colorStyles = { | |
| yellow: { | |
| bg: 'bg-gradient-to-br from-yellow-200 to-yellow-300', | |
| border: 'border-yellow-400/50', | |
| text: 'text-yellow-900', | |
| accent: 'text-yellow-700', | |
| shadow: 'shadow-yellow-500/20', | |
| }, | |
| pink: { | |
| bg: 'bg-gradient-to-br from-pink-200 to-pink-300', | |
| border: 'border-pink-400/50', | |
| text: 'text-pink-900', | |
| accent: 'text-pink-700', | |
| shadow: 'shadow-pink-500/20', | |
| }, | |
| blue: { | |
| bg: 'bg-gradient-to-br from-blue-200 to-blue-300', | |
| border: 'border-blue-400/50', | |
| text: 'text-blue-900', | |
| accent: 'text-blue-700', | |
| shadow: 'shadow-blue-500/20', | |
| }, | |
| green: { | |
| bg: 'bg-gradient-to-br from-green-200 to-green-300', | |
| border: 'border-green-400/50', | |
| text: 'text-green-900', | |
| accent: 'text-green-700', | |
| shadow: 'shadow-green-500/20', | |
| }, | |
| purple: { | |
| bg: 'bg-gradient-to-br from-purple-200 to-purple-300', | |
| border: 'border-purple-400/50', | |
| text: 'text-purple-900', | |
| accent: 'text-purple-700', | |
| shadow: 'shadow-purple-500/20', | |
| }, | |
| }; | |
| const statusBadges = { | |
| INCUBATED: { icon: Brain, label: 'Incubating', class: 'bg-yellow-500/80 text-yellow-900' }, | |
| PROMOTED: { icon: ArrowUpRight, label: 'Promoted!', class: 'bg-green-500/80 text-green-900' }, | |
| REJECTED: { icon: X, label: 'Rejected', class: 'bg-red-500/80 text-red-900' }, | |
| IMPLEMENTED: { icon: Sparkles, label: 'Implemented', class: 'bg-blue-500/80 text-blue-900' }, | |
| }; | |
| export function IdeaStickyNote({ | |
| idea, | |
| onPromote, | |
| onReject, | |
| onVote, | |
| color = 'yellow', | |
| tilt = 0, | |
| }: IdeaStickyNoteProps) { | |
| const [isHovered, setIsHovered] = useState(false); | |
| const styles = colorStyles[color]; | |
| const status = statusBadges[idea.status]; | |
| const StatusIcon = status.icon; | |
| // Random tilt for playful look | |
| const rotation = tilt || (Math.random() * 6 - 3); | |
| // Confidence as visual indicator | |
| const confidenceStars = Math.round(idea.confidence * 5); | |
| const timeSince = (dateStr: string) => { | |
| const date = new Date(dateStr); | |
| const now = new Date(); | |
| const diffMs = now.getTime() - date.getTime(); | |
| const diffHours = Math.floor(diffMs / 3600000); | |
| const diffDays = Math.floor(diffHours / 24); | |
| if (diffDays > 0) return `${diffDays}d`; | |
| if (diffHours > 0) return `${diffHours}h`; | |
| return 'now'; | |
| }; | |
| return ( | |
| <div | |
| className={cn( | |
| 'relative w-64 p-4 rounded-sm border-2 transition-all duration-300', | |
| styles.bg, styles.border, | |
| 'shadow-lg hover:shadow-xl', | |
| styles.shadow, | |
| isHovered && 'scale-105 z-10' | |
| )} | |
| style={{ | |
| transform: `rotate(${isHovered ? 0 : rotation}deg)`, | |
| fontFamily: "'Caveat', 'Patrick Hand', cursive, sans-serif", | |
| }} | |
| onMouseEnter={() => setIsHovered(true)} | |
| onMouseLeave={() => setIsHovered(false)} | |
| > | |
| {/* Pin decoration */} | |
| <div className="absolute -top-2 left-1/2 -translate-x-1/2 w-4 h-4 rounded-full bg-red-500 shadow-md border-2 border-red-600" /> | |
| {/* Status badge */} | |
| <Badge className={cn('absolute -top-1 -right-1 text-[9px]', status.class)}> | |
| <StatusIcon className="w-2 h-2 mr-0.5" /> | |
| {status.label} | |
| </Badge> | |
| {/* Title */} | |
| <div className="flex items-start gap-2 mb-3"> | |
| <Lightbulb className={cn('w-5 h-5 flex-shrink-0', styles.accent)} /> | |
| <h3 className={cn('text-lg font-bold leading-tight', styles.text)}> | |
| {idea.title} | |
| </h3> | |
| </div> | |
| {/* Hypothesis */} | |
| <p className={cn('text-sm leading-relaxed mb-3', styles.text, 'opacity-80')}> | |
| "{idea.hypothesis}" | |
| </p> | |
| {/* Confidence stars */} | |
| <div className="flex items-center gap-1 mb-3"> | |
| <span className={cn('text-[10px] uppercase tracking-wider', styles.accent)}> | |
| Confidence: | |
| </span> | |
| <div className="flex"> | |
| {[1, 2, 3, 4, 5].map(star => ( | |
| <Sparkles | |
| key={star} | |
| className={cn( | |
| 'w-3 h-3', | |
| star <= confidenceStars ? 'text-amber-500' : 'text-gray-400/50' | |
| )} | |
| /> | |
| ))} | |
| </div> | |
| <span className={cn('text-[10px] ml-1', styles.accent)}> | |
| {(idea.confidence * 100).toFixed(0)}% | |
| </span> | |
| </div> | |
| {/* Tags */} | |
| {idea.tags && idea.tags.length > 0 && ( | |
| <div className="flex flex-wrap gap-1 mb-3"> | |
| {idea.tags.slice(0, 3).map(tag => ( | |
| <span | |
| key={tag} | |
| className={cn( | |
| 'inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[9px]', | |
| 'bg-black/10', styles.text | |
| )} | |
| > | |
| <Tag className="w-2 h-2" /> | |
| {tag} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| {/* Metadata footer */} | |
| <div className={cn('flex items-center justify-between text-[10px] pt-2 border-t border-black/10', styles.accent)}> | |
| <div className="flex items-center gap-1"> | |
| <User className="w-3 h-3" /> | |
| {idea.proposedBy} | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> | |
| {timeSince(idea.created_at)} | |
| </div> | |
| </div> | |
| {/* Actions (show on hover) */} | |
| {idea.status === 'INCUBATED' && isHovered && ( | |
| <div className="absolute -bottom-12 left-0 right-0 flex justify-center gap-2"> | |
| {onVote && ( | |
| <Button | |
| size="sm" | |
| variant="secondary" | |
| onClick={() => onVote(idea.id)} | |
| className="h-7 px-2 shadow-md" | |
| > | |
| <ThumbsUp className="w-3 h-3 mr-1" /> | |
| {idea.votes || 0} | |
| </Button> | |
| )} | |
| {onPromote && ( | |
| <Button | |
| size="sm" | |
| variant="default" | |
| onClick={() => onPromote(idea.id)} | |
| className="h-7 px-2 bg-green-600 hover:bg-green-700 shadow-md" | |
| > | |
| <ArrowUpRight className="w-3 h-3 mr-1" /> | |
| Promote | |
| </Button> | |
| )} | |
| {onReject && ( | |
| <Button | |
| size="sm" | |
| variant="destructive" | |
| onClick={() => onReject(idea.id)} | |
| className="h-7 px-2 shadow-md" | |
| > | |
| <X className="w-3 h-3" /> | |
| </Button> | |
| )} | |
| </div> | |
| )} | |
| {/* Related reference */} | |
| {idea.relatedTo && ( | |
| <div className={cn( | |
| 'absolute -bottom-2 -right-2 px-2 py-0.5 rounded text-[8px] rotate-3', | |
| 'bg-white/80 shadow-sm', styles.text | |
| )}> | |
| β {idea.relatedTo} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export default IdeaStickyNote; | |