From eef9eca13c677acf08531203fe53728661e2ac34 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 16 Jan 2026 16:35:26 +0000 Subject: [PATCH] Generated by Spark: Allow grouping related ideas with colored borders or containers --- src/components/FeatureIdeaCloud.tsx | 271 +++++++++++++++++++++++++++- 1 file changed, 266 insertions(+), 5 deletions(-) diff --git a/src/components/FeatureIdeaCloud.tsx b/src/components/FeatureIdeaCloud.tsx index c0e62a7..0fb6189 100644 --- a/src/components/FeatureIdeaCloud.tsx +++ b/src/components/FeatureIdeaCloud.tsx @@ -26,7 +26,7 @@ import { Badge } from '@/components/ui/badge' import { Textarea } from '@/components/ui/textarea' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { Plus, Trash, Sparkle, DotsThree } from '@phosphor-icons/react' +import { Plus, Trash, Sparkle, DotsThree, Package } from '@phosphor-icons/react' import { toast } from 'sonner' type ConnectionType = 'dependency' | 'association' | 'inheritance' | 'composition' | 'aggregation' @@ -39,6 +39,14 @@ interface FeatureIdea { priority: 'low' | 'medium' | 'high' status: 'idea' | 'planned' | 'in-progress' | 'completed' createdAt: number + parentGroup?: string +} + +interface IdeaGroup { + id: string + label: string + color: string + createdAt: number } interface IdeaEdgeData { @@ -173,6 +181,56 @@ const PRIORITY_COLORS = { high: 'border-red-400/60 bg-red-50/80 dark:bg-red-950/40', } +const GROUP_COLORS = [ + { name: 'Blue', value: '#3b82f6', bg: 'rgba(59, 130, 246, 0.08)', border: 'rgba(59, 130, 246, 0.3)' }, + { name: 'Purple', value: '#a855f7', bg: 'rgba(168, 85, 247, 0.08)', border: 'rgba(168, 85, 247, 0.3)' }, + { name: 'Green', value: '#10b981', bg: 'rgba(16, 185, 129, 0.08)', border: 'rgba(16, 185, 129, 0.3)' }, + { name: 'Red', value: '#ef4444', bg: 'rgba(239, 68, 68, 0.08)', border: 'rgba(239, 68, 68, 0.3)' }, + { name: 'Orange', value: '#f97316', bg: 'rgba(249, 115, 22, 0.08)', border: 'rgba(249, 115, 22, 0.3)' }, + { name: 'Pink', value: '#ec4899', bg: 'rgba(236, 72, 153, 0.08)', border: 'rgba(236, 72, 153, 0.3)' }, + { name: 'Cyan', value: '#06b6d4', bg: 'rgba(6, 182, 212, 0.08)', border: 'rgba(6, 182, 212, 0.3)' }, + { name: 'Amber', value: '#f59e0b', bg: 'rgba(245, 158, 11, 0.08)', border: 'rgba(245, 158, 11, 0.3)' }, +] + +function GroupNode({ data, selected }: NodeProps) { + const colorScheme = GROUP_COLORS.find(c => c.value === data.color) || GROUP_COLORS[0] + + return ( +
+
+ {data.label} +
+ +
+ ) +} + function IdeaNode({ data, selected }: NodeProps) { return (
@@ -237,10 +295,12 @@ function IdeaNode({ data, selected }: NodeProps) { const nodeTypes = { ideaNode: IdeaNode, + groupNode: GroupNode, } export function FeatureIdeaCloud() { const [ideas, setIdeas] = useKV('feature-ideas', SEED_IDEAS) + const [groups, setGroups] = useKV('feature-idea-groups', []) const [savedEdges, setSavedEdges] = useKV[]>('feature-idea-edges', [ { id: 'edge-1', @@ -281,14 +341,17 @@ export function FeatureIdeaCloud() { const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [selectedIdea, setSelectedIdea] = useState(null) + const [selectedGroup, setSelectedGroup] = useState(null) const [selectedEdge, setSelectedEdge] = useState | null>(null) const [editDialogOpen, setEditDialogOpen] = useState(false) + const [groupDialogOpen, setGroupDialogOpen] = useState(false) const [viewDialogOpen, setViewDialogOpen] = useState(false) const [edgeDialogOpen, setEdgeDialogOpen] = useState(false) const [connectionType, setConnectionType] = useState('association') const edgeReconnectSuccessful = useRef(true) const safeIdeas = ideas || SEED_IDEAS + const safeGroups = groups || [] const safeEdges = savedEdges || [] useEffect(() => { @@ -298,14 +361,30 @@ export function FeatureIdeaCloud() { }, [ideas, setIdeas]) useEffect(() => { - const initialNodes: Node[] = safeIdeas.map((idea, index) => ({ + const groupNodes: Node[] = safeGroups.map((group) => ({ + id: group.id, + type: 'groupNode', + position: { x: 0, y: 0 }, + data: group, + style: { + zIndex: -1, + }, + })) + + const ideaNodes: Node[] = safeIdeas.map((idea, index) => ({ id: idea.id, type: 'ideaNode', position: { x: 100 + (index % 3) * 350, y: 100 + Math.floor(index / 3) * 250 }, data: idea, + parentNode: idea.parentGroup, + extent: idea.parentGroup ? 'parent' : undefined, + style: { + zIndex: 1, + }, })) - setNodes(initialNodes) - }, [safeIdeas, setNodes]) + + setNodes([...groupNodes, ...ideaNodes]) + }, [safeIdeas, safeGroups, setNodes]) useEffect(() => { setEdges(safeEdges) @@ -318,8 +397,18 @@ export function FeatureIdeaCloud() { setEditDialogOpen(true) } + const handleEditGroup = (e: Event) => { + const customEvent = e as CustomEvent + setSelectedGroup(customEvent.detail) + setGroupDialogOpen(true) + } + window.addEventListener('editIdea', handleEditIdea) - return () => window.removeEventListener('editIdea', handleEditIdea) + window.addEventListener('editGroup', handleEditGroup) + return () => { + window.removeEventListener('editIdea', handleEditIdea) + window.removeEventListener('editGroup', handleEditGroup) + } }, []) const onNodesChangeWrapper = useCallback( @@ -486,6 +575,17 @@ export function FeatureIdeaCloud() { setEditDialogOpen(true) } + const handleAddGroup = () => { + const newGroup: IdeaGroup = { + id: `group-${Date.now()}`, + label: '', + color: GROUP_COLORS[0].value, + createdAt: Date.now(), + } + setSelectedGroup(newGroup) + setGroupDialogOpen(true) + } + const handleSaveIdea = () => { if (!selectedIdea || !selectedIdea.title.trim()) { toast.error('Please enter a title') @@ -530,6 +630,54 @@ export function FeatureIdeaCloud() { toast.success('Idea deleted') } + const handleSaveGroup = () => { + if (!selectedGroup || !selectedGroup.label.trim()) { + toast.error('Please enter a group name') + return + } + + setGroups((currentGroups) => { + const existing = (currentGroups || []).find(g => g.id === selectedGroup.id) + if (existing) { + return (currentGroups || []).map(g => g.id === selectedGroup.id ? selectedGroup : g) + } else { + return [...(currentGroups || []), selectedGroup] + } + }) + + if (!(groups || []).find(g => g.id === selectedGroup.id)) { + const newNode: Node = { + id: selectedGroup.id, + type: 'groupNode', + position: { x: 200, y: 200 }, + data: selectedGroup, + style: { + zIndex: -1, + }, + } + setNodes((nds) => [newNode, ...nds]) + } + + setGroupDialogOpen(false) + setSelectedGroup(null) + toast.success('Group saved!') + } + + const handleDeleteGroup = (id: string) => { + setIdeas((currentIdeas) => + (currentIdeas || []).map(idea => + idea.parentGroup === id ? { ...idea, parentGroup: undefined } : idea + ) + ) + + setGroups((currentGroups) => (currentGroups || []).filter(g => g.id !== id)) + setNodes((nds) => nds.filter(n => n.id !== id)) + + setGroupDialogOpen(false) + setSelectedGroup(null) + toast.success('Group deleted') + } + const handleDeleteEdge = (edgeId: string) => { const updatedEdges = edges.filter(e => e.id !== edgeId) setEdges(updatedEdges) @@ -672,6 +820,15 @@ export function FeatureIdeaCloud() { AI Generate Ideas + + + + + Add Group + +
+ + +
+

💡 Tips:

+
    +
  • • Groups provide visual organization for related ideas
  • +
  • • Drag ideas into groups or assign them in the idea editor
  • +
  • • Ideas stay within their group boundaries when moved
  • +
+
+ + )} + + +
+
+ {selectedGroup && groups?.find(g => g.id === selectedGroup.id) && ( + + )} +
+
+ + +
+
+
+ + + @@ -810,6 +1046,22 @@ export function FeatureIdeaCloud() { + +
+ + +
)} @@ -869,6 +1121,15 @@ export function FeatureIdeaCloud() { + {selectedIdea.parentGroup && ( +
+ +

+ {safeGroups.find(g => g.id === selectedIdea.parentGroup)?.label || 'Unknown'} +

+
+ )} +

{new Date(selectedIdea.createdAt).toLocaleDateString()}