Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* ╔═══════════════════════════════════════════════════════════════════════════╗
* β•‘ 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;