From 5fc144f31fab83e9db443ebafd59eba22c01f7e5 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 16 Jan 2026 15:54:57 +0000 Subject: [PATCH] Generated by Spark: Feature Idea Cloud should be closer to Miro - its a bit rigid / wooden right now --- src/components/FeatureIdeaCloud.tsx | 549 +++++++++++++++++----------- 1 file changed, 341 insertions(+), 208 deletions(-) diff --git a/src/components/FeatureIdeaCloud.tsx b/src/components/FeatureIdeaCloud.tsx index d45b256..35cfa9a 100644 --- a/src/components/FeatureIdeaCloud.tsx +++ b/src/components/FeatureIdeaCloud.tsx @@ -6,9 +6,9 @@ import { Card } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Textarea } from '@/components/ui/textarea' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Plus, Trash, Cloud, Sparkle, ArrowsClockwise, Eye } from '@phosphor-icons/react' -import { motion, AnimatePresence } from 'framer-motion' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { Plus, Trash, Sparkle, MagnifyingGlassMinus, MagnifyingGlassPlus, ArrowsOut, Hand, Link as LinkIcon, Selection, DotsThree } from '@phosphor-icons/react' +import { motion } from 'framer-motion' import { toast } from 'sonner' interface FeatureIdea { @@ -21,6 +21,7 @@ interface FeatureIdea { createdAt: number x: number y: number + connectedTo?: string[] } const SEED_IDEAS: FeatureIdea[] = [ @@ -32,8 +33,9 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'completed', createdAt: Date.now() - 10000000, - x: 10, - y: 15, + x: 100, + y: 150, + connectedTo: ['idea-8'], }, { id: 'idea-2', @@ -43,8 +45,9 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'idea', createdAt: Date.now() - 9000000, - x: 60, - y: 25, + x: 600, + y: 250, + connectedTo: ['idea-4'], }, { id: 'idea-3', @@ -54,8 +57,8 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'idea', createdAt: Date.now() - 8000000, - x: 25, - y: 55, + x: 250, + y: 550, }, { id: 'idea-4', @@ -65,8 +68,9 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'planned', createdAt: Date.now() - 7000000, - x: 70, - y: 60, + x: 700, + y: 600, + connectedTo: ['idea-8'], }, { id: 'idea-5', @@ -76,8 +80,8 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'idea', createdAt: Date.now() - 6000000, - x: 15, - y: 80, + x: 150, + y: 800, }, { id: 'idea-6', @@ -87,8 +91,8 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'idea', createdAt: Date.now() - 5000000, - x: 80, - y: 35, + x: 800, + y: 350, }, { id: 'idea-7', @@ -98,8 +102,8 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'low', status: 'completed', createdAt: Date.now() - 4000000, - x: 45, - y: 10, + x: 450, + y: 100, }, { id: 'idea-8', @@ -109,8 +113,9 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'in-progress', createdAt: Date.now() - 3000000, - x: 30, - y: 40, + x: 300, + y: 400, + connectedTo: ['idea-5'], }, { id: 'idea-9', @@ -120,8 +125,8 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'planned', createdAt: Date.now() - 2000000, - x: 55, - y: 75, + x: 550, + y: 750, }, { id: 'idea-10', @@ -131,30 +136,8 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'idea', createdAt: Date.now() - 1000000, - x: 85, - y: 50, - }, - { - id: 'idea-11', - title: 'Code Templates', - description: 'Reusable code snippets and patterns library', - category: 'Productivity', - priority: 'medium', - status: 'completed', - createdAt: Date.now() - 900000, - x: 40, - y: 85, - }, - { - id: 'idea-12', - title: 'Webhook Testing', - description: 'Test and debug webhooks locally with request inspection', - category: 'DevOps', - priority: 'low', - status: 'idea', - createdAt: Date.now() - 800000, - x: 65, - y: 45, + x: 850, + y: 500, }, ] @@ -170,9 +153,9 @@ const STATUS_COLORS = { } const PRIORITY_COLORS = { - low: 'border-blue-400 bg-blue-50 dark:bg-blue-950', - medium: 'border-amber-400 bg-amber-50 dark:bg-amber-950', - high: 'border-red-400 bg-red-50 dark:bg-red-950', + low: 'border-blue-400/60 bg-blue-50/80 dark:bg-blue-950/40', + medium: 'border-amber-400/60 bg-amber-50/80 dark:bg-amber-950/40', + high: 'border-red-400/60 bg-red-50/80 dark:bg-red-950/40', } export function FeatureIdeaCloud() { @@ -180,11 +163,16 @@ export function FeatureIdeaCloud() { const [selectedIdea, setSelectedIdea] = useState(null) const [editDialogOpen, setEditDialogOpen] = useState(false) const [viewDialogOpen, setViewDialogOpen] = useState(false) - const [filterCategory, setFilterCategory] = useState('all') - const [filterStatus, setFilterStatus] = useState('all') - const [filterPriority, setFilterPriority] = useState('all') - const containerRef = useRef(null) - const [containerSize, setContainerSize] = useState({ width: 800, height: 600 }) + const canvasRef = useRef(null) + const [zoom, setZoom] = useState(1) + const [pan, setPan] = useState({ x: 0, y: 0 }) + const [isPanning, setIsPanning] = useState(false) + const [panStart, setPanStart] = useState({ x: 0, y: 0 }) + const [draggedIdea, setDraggedIdea] = useState(null) + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) + const [tool, setTool] = useState<'select' | 'pan' | 'connect'>('select') + const [connectingFrom, setConnectingFrom] = useState(null) + const [filtersPanelOpen, setFiltersPanelOpen] = useState(false) const safeIdeas = ideas || SEED_IDEAS @@ -194,22 +182,10 @@ export function FeatureIdeaCloud() { } }, [ideas, setIdeas]) - useEffect(() => { - const updateSize = () => { - if (containerRef.current) { - setContainerSize({ - width: containerRef.current.offsetWidth, - height: containerRef.current.offsetHeight, - }) - } - } - - updateSize() - window.addEventListener('resize', updateSize) - return () => window.removeEventListener('resize', updateSize) - }, []) - const handleAddIdea = () => { + const canvasCenterX = (window.innerWidth / 2 - pan.x) / zoom + const canvasCenterY = (window.innerHeight / 2 - pan.y) / zoom + const newIdea: FeatureIdea = { id: `idea-${Date.now()}`, title: '', @@ -218,8 +194,8 @@ export function FeatureIdeaCloud() { priority: 'medium', status: 'idea', createdAt: Date.now(), - x: Math.random() * 80, - y: Math.random() * 80, + x: canvasCenterX, + y: canvasCenterY, } setSelectedIdea(newIdea) setEditDialogOpen(true) @@ -230,9 +206,34 @@ export function FeatureIdeaCloud() { setEditDialogOpen(true) } - const handleViewIdea = (idea: FeatureIdea) => { - setSelectedIdea(idea) - setViewDialogOpen(true) + const handleIdeaClick = (idea: FeatureIdea, e: React.MouseEvent) => { + if (tool === 'connect') { + e.stopPropagation() + if (!connectingFrom) { + setConnectingFrom(idea.id) + toast.info('Click another idea to connect') + } else if (connectingFrom !== idea.id) { + setIdeas((currentIdeas) => + (currentIdeas || []).map(i => { + if (i.id === connectingFrom) { + const connectedTo = i.connectedTo || [] + if (!connectedTo.includes(idea.id)) { + return { ...i, connectedTo: [...connectedTo, idea.id] } + } + } + return i + }) + ) + setConnectingFrom(null) + toast.success('Ideas connected!') + } + return + } + + if (tool === 'select' && !draggedIdea) { + setSelectedIdea(idea) + setViewDialogOpen(true) + } } const handleSaveIdea = () => { @@ -267,13 +268,14 @@ export function FeatureIdeaCloud() { toast.info('Generating ideas with AI...') try { + const categoryList = CATEGORIES.join('|') const promptText = `Generate 3 innovative feature ideas for a low-code application builder. Each idea should be practical and valuable. Return as JSON with this structure: { "ideas": [ { "title": "Feature Name", "description": "Brief description", - "category": "${CATEGORIES.join('|')}", + "category": "${categoryList}", "priority": "low|medium|high" } ] @@ -291,8 +293,8 @@ export function FeatureIdeaCloud() { priority: idea.priority || 'medium', status: 'idea' as const, createdAt: Date.now(), - x: 20 + (index * 20), - y: 20 + (index * 15), + x: 400 + (index * 250), + y: 300 + (index * 150), })) setIdeas((currentIdeas) => [...(currentIdeas || []), ...newIdeas]) @@ -304,154 +306,286 @@ export function FeatureIdeaCloud() { } } - const handleRandomizePositions = () => { - setIdeas((currentIdeas) => - (currentIdeas || []).map(idea => ({ - ...idea, - x: Math.random() * 80, - y: Math.random() * 80, - })) - ) - toast.success('Positions randomized!') + const handleZoomIn = () => { + setZoom(z => Math.min(z * 1.2, 3)) } - const filteredIdeas = safeIdeas.filter(idea => { - if (filterCategory !== 'all' && idea.category !== filterCategory) return false - if (filterStatus !== 'all' && idea.status !== filterStatus) return false - if (filterPriority !== 'all' && idea.priority !== filterPriority) return false - return true - }) + const handleZoomOut = () => { + setZoom(z => Math.max(z / 1.2, 0.25)) + } - const categoryStats = safeIdeas.reduce((acc, idea) => { - acc[idea.category] = (acc[idea.category] || 0) + 1 - return acc - }, {} as Record) + const handleResetView = () => { + setZoom(1) + setPan({ x: 0, y: 0 }) + } - const statusStats = safeIdeas.reduce((acc, idea) => { - acc[idea.status] = (acc[idea.status] || 0) + 1 - return acc - }, {} as Record) + const handleCanvasMouseDown = (e: React.MouseEvent) => { + if (tool === 'pan' || e.button === 1 || (e.button === 0 && e.ctrlKey)) { + setIsPanning(true) + setPanStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }) + e.preventDefault() + } + } + + const handleCanvasMouseMove = (e: React.MouseEvent) => { + if (isPanning) { + setPan({ + x: e.clientX - panStart.x, + y: e.clientY - panStart.y, + }) + } else if (draggedIdea) { + const canvasX = (e.clientX - pan.x) / zoom + const canvasY = (e.clientY - pan.y) / zoom + + setIdeas((currentIdeas) => + (currentIdeas || []).map(idea => + idea.id === draggedIdea + ? { ...idea, x: canvasX - dragOffset.x, y: canvasY - dragOffset.y } + : idea + ) + ) + } + } + + const handleCanvasMouseUp = () => { + setIsPanning(false) + setDraggedIdea(null) + } + + const handleIdeaMouseDown = (idea: FeatureIdea, e: React.MouseEvent) => { + if (tool === 'select') { + e.stopPropagation() + const canvasX = (e.clientX - pan.x) / zoom + const canvasY = (e.clientY - pan.y) / zoom + setDraggedIdea(idea.id) + setDragOffset({ + x: canvasX - idea.x, + y: canvasY - idea.y, + }) + } + } + + const handleWheel = (e: React.WheelEvent) => { + e.preventDefault() + const delta = e.deltaY > 0 ? 0.9 : 1.1 + const newZoom = Math.max(0.25, Math.min(3, zoom * delta)) + + const mouseX = e.clientX + const mouseY = e.clientY + + const zoomPointX = (mouseX - pan.x) / zoom + const zoomPointY = (mouseY - pan.y) / zoom + + const newPanX = mouseX - zoomPointX * newZoom + const newPanY = mouseY - zoomPointY * newZoom + + setZoom(newZoom) + setPan({ x: newPanX, y: newPanY }) + } + + const renderConnections = () => { + const connections: React.ReactNode[] = [] + safeIdeas.forEach((fromIdea) => { + fromIdea.connectedTo?.forEach((toId) => { + const toIdea = safeIdeas.find(i => i.id === toId) + if (toIdea) { + const fromX = fromIdea.x * zoom + pan.x + 120 + const fromY = fromIdea.y * zoom + pan.y + 80 + const toX = toIdea.x * zoom + pan.x + 120 + const toY = toIdea.y * zoom + pan.y + 80 + + connections.push( + + ) + } + }) + }) + return connections + } return ( -
-
-
-
-

- - Feature Idea Cloud -

-

- Brainstorm and visualize your app features -

-
-
- - - -
-
+
+
+ + + + + + Select & Drag + -
-
- - -
+ + + + + Pan Canvas + -
- - -
+ + + + + Connect Ideas + + -
- - -
+
-
- -
+ + + + + + Zoom In + + + + + + + Zoom Out + + + + + + + Reset View + + + +
+ {Math.round(zoom * 100)}%
-
- - {filteredIdeas.map((idea) => ( +
+ + + + + + AI Generate Ideas + + + + + + + Add Idea + + +
+ +
+

💡 Tip: Drag ideas to move, scroll to zoom

+
+ +
+ + {renderConnections()} + + +
+ {safeIdeas.map((idea) => ( handleViewIdea(idea)} + initial={{ scale: 0, opacity: 0 }} + animate={{ scale: 1, opacity: 1 }} + transition={{ type: 'spring', stiffness: 260, damping: 20 }} + onMouseDown={(e) => handleIdeaMouseDown(idea, e)} + onClick={(e) => handleIdeaClick(idea, e)} > - +
-

{idea.title}

+

{idea.title}

+

{idea.description} @@ -468,13 +602,12 @@ export function FeatureIdeaCloud() { ))} - +

- {filteredIdeas.length === 0 && ( + {safeIdeas.length === 0 && (
- -

No ideas match your filters

+

No ideas yet