'use client' import { useEffect, useMemo, useState } from 'react' import { useRouter } from 'next/navigation' import { useMutation, useQuery } from 'convex/react' import { api } from '@/convex/_generated/api' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Slider } from '@/components/ui/slider' import { Alert, AlertDescription } from '@/components/ui/alert' import { Separator } from '@/components/ui/separator' import { ScrollArea } from '@/components/ui/scroll-area' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' import { Search, Loader2, ExternalLink, MessageSquare, Twitter, Users, HelpCircle, Filter, ChevronDown, ChevronUp, Target, Zap, AlertCircle, BarChart3, CheckCircle2, Eye, Copy } from 'lucide-react' import { useProject } from '@/components/project-context' import type { EnhancedProductAnalysis, Opportunity, PlatformConfig, SearchStrategy } from '@/lib/types' const STRATEGY_INFO: Record = { 'direct-keywords': { name: 'Direct Keywords', description: 'People looking for your product category' }, 'problem-pain': { name: 'Problem/Pain', description: 'People experiencing problems you solve' }, 'competitor-alternative': { name: 'Competitor Alternatives', description: 'People switching from competitors' }, 'how-to': { name: 'How-To/Tutorials', description: 'People learning about solutions' }, 'emotional-frustrated': { name: 'Frustration Posts', description: 'Emotional posts about pain points' }, 'comparison': { name: 'Comparisons', description: '"X vs Y" comparison posts' }, 'recommendation': { name: 'Recommendations', description: '"What do you use" requests' } } export default function OpportunitiesPage() { const router = useRouter() const { selectedProjectId } = useProject() const upsertOpportunities = useMutation(api.opportunities.upsertBatch) const updateOpportunity = useMutation(api.opportunities.updateStatus) const [analysis, setAnalysis] = useState(null) const [platforms, setPlatforms] = useState([]) const [strategies, setStrategies] = useState([ 'direct-keywords', 'problem-pain', 'competitor-alternative' ]) const [intensity, setIntensity] = useState<'broad' | 'balanced' | 'targeted'>('balanced') const [isSearching, setIsSearching] = useState(false) const [opportunities, setOpportunities] = useState([]) const [generatedQueries, setGeneratedQueries] = useState([]) const [showQueries, setShowQueries] = useState(false) const [selectedOpportunity, setSelectedOpportunity] = useState(null) const [replyText, setReplyText] = useState('') const [stats, setStats] = useState(null) const [searchError, setSearchError] = useState('') const [missingSources, setMissingSources] = useState([]) const [statusFilter, setStatusFilter] = useState('all') const [intentFilter, setIntentFilter] = useState('all') const [minScore, setMinScore] = useState(0) const [limit, setLimit] = useState(50) const [statusInput, setStatusInput] = useState('new') const [notesInput, setNotesInput] = useState('') const [tagsInput, setTagsInput] = useState('') const projects = useQuery(api.projects.getProjects) const latestAnalysis = useQuery( api.analyses.getLatestByProject, selectedProjectId ? { projectId: selectedProjectId as any } : 'skip' ) const savedOpportunities = useQuery( api.opportunities.listByProject, selectedProjectId ? { projectId: selectedProjectId as any, status: statusFilter === 'all' ? undefined : statusFilter, intent: intentFilter === 'all' ? undefined : intentFilter, minScore: minScore > 0 ? minScore / 100 : undefined, limit, } : 'skip' ) const displayOpportunities = useMemo(() => { if (!savedOpportunities || savedOpportunities.length === 0) return opportunities return savedOpportunities.map((opp: any) => ({ id: opp._id, title: opp.title, url: opp.url, snippet: opp.snippet, platform: opp.platform, source: opp.platform, relevanceScore: opp.relevanceScore, emotionalIntensity: 'low', intent: opp.intent, matchedKeywords: opp.matchedKeywords, matchedProblems: opp.matchedProblems, suggestedApproach: opp.suggestedApproach, softPitch: opp.softPitch, status: opp.status, notes: opp.notes, tags: opp.tags, })) as Opportunity[] }, [savedOpportunities, opportunities]) const selectedSources = useQuery( api.dataSources.getProjectDataSources, selectedProjectId ? { projectId: selectedProjectId as any } : "skip" ) const selectedProject = projects?.find((project: any) => project._id === selectedProjectId) const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || [] const activeSources = selectedSources?.filter((source: any) => selectedSourceIds.includes(source._id) ) || [] useEffect(() => { const stored = localStorage.getItem('productAnalysis') if (stored) { setAnalysis(JSON.parse(stored)) } fetch('/api/opportunities') .then(r => { if (r.redirected) { router.push('/auth?next=/opportunities') return null } return r.json() }) .then(data => { if (data) { setPlatforms(data.platforms) } }) }, [router]) useEffect(() => { if (!analysis && latestAnalysis) { setAnalysis(latestAnalysis as any) } }, [analysis, latestAnalysis]) useEffect(() => { if (!analysis && latestAnalysis === null) { router.push('/onboarding') } }, [analysis, latestAnalysis, router]) const togglePlatform = (platformId: string) => { setPlatforms(prev => prev.map(p => p.id === platformId ? { ...p, enabled: !p.enabled } : p )) } const toggleStrategy = (strategy: SearchStrategy) => { setStrategies(prev => prev.includes(strategy) ? prev.filter(s => s !== strategy) : [...prev, strategy] ) } const executeSearch = async () => { if (!analysis) return if (!selectedProjectId) return setIsSearching(true) setOpportunities([]) setSearchError('') try { const config = { platforms, strategies, intensity, maxResults: intensity === 'broad' ? 80 : intensity === 'balanced' ? 50 : 30 } const response = await fetch('/api/opportunities', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: selectedProjectId, config }) }) if (response.redirected) { router.push('/auth?next=/opportunities') return } const data = await response.json() if (!response.ok) { throw new Error(data.error || 'Failed to search for opportunities') } if (data.success) { const mapped = data.data.opportunities.map((opp: Opportunity) => ({ ...opp, status: 'new', })) setOpportunities(mapped) setGeneratedQueries(data.data.queries) setStats(data.data.stats) setMissingSources(data.data.missingSources || []) await upsertOpportunities({ projectId: selectedProjectId as any, analysisId: latestAnalysis?._id, opportunities: mapped.map((opp: Opportunity) => ({ url: opp.url, platform: opp.platform, title: opp.title, snippet: opp.snippet, relevanceScore: opp.relevanceScore, intent: opp.intent, suggestedApproach: opp.suggestedApproach, matchedKeywords: opp.matchedKeywords, matchedProblems: opp.matchedProblems, softPitch: opp.softPitch, })), }) } } catch (error: any) { console.error('Search error:', error) setSearchError(error?.message || 'Search failed. Please try again.') } finally { setIsSearching(false) } } const generateReply = (opp: Opportunity) => { const template = opp.softPitch ? `Hey, I saw your post about ${opp.matchedProblems[0] || 'your challenge'}. We faced something similar and ended up building ${analysis?.productName} specifically for this. Happy to share what worked for us.` : `Hi! I noticed you're looking for solutions to ${opp.matchedProblems[0]}. I work on ${analysis?.productName} that helps teams with this - specifically ${opp.matchedKeywords.slice(0, 2).join(' and ')}. Would love to show you how it works.` setReplyText(template) } const getIntentIcon = (intent: string) => { switch (intent) { case 'frustrated': return case 'comparing': return case 'learning': return default: return } } useEffect(() => { if (selectedOpportunity) { setStatusInput(selectedOpportunity.status || 'new') setNotesInput(selectedOpportunity.notes || '') setTagsInput(selectedOpportunity.tags?.join(', ') || '') } }, [selectedOpportunity]) const handleSaveOpportunity = async () => { if (!selectedOpportunity?.id) return const tags = tagsInput .split(',') .map((tag) => tag.trim()) .filter(Boolean) await updateOpportunity({ id: selectedOpportunity.id as any, status: statusInput, notes: notesInput || undefined, tags: tags.length > 0 ? tags : undefined, }) } if (!analysis) return null return (
{/* Sidebar */}

Search Configuration

{/* Platforms */}
{platforms.map(platform => (
togglePlatform(platform.id)} />
))}
{/* Strategies */}
{(Object.keys(STRATEGY_INFO) as SearchStrategy[]).map(strategy => (
toggleStrategy(strategy)} />

{STRATEGY_INFO[strategy].description}

))}
{/* Intensity */}
setIntensity(v < 33 ? 'broad' : v < 66 ? 'balanced' : 'targeted')} max={100} step={50} />
Broad Targeted
{/* Main Content */}
{/* Header */}

Opportunity Finder

Discover potential customers for {analysis.productName}

{stats && (
{stats.opportunitiesFound}
Found
{stats.highRelevance}
High Quality
)}
{searchError && ( {searchError} )} {/* Active Sources */} {activeSources.length > 0 && ( Active Data Sources Sources selected for this project will drive opportunity search. {activeSources.map((source: any) => ( {source.name || source.url} ))} )} {selectedSources && selectedSourceIds.length === 0 && ( No data sources selected. Add and select sources to generate opportunities. )} {missingSources.length > 0 && ( Some selected sources don't have analysis yet. Run onboarding or re-analyze them for best results. )} {/* Generated Queries */} {generatedQueries.length > 0 && (
Generated Queries ({generatedQueries.length}) {showQueries ? : }
{generatedQueries.slice(0, 20).map((q, i) => (
{q.query}
))}
)} {/* Results Table */} {displayOpportunities.length > 0 ? (
setMinScore(Number(e.target.value))} className="h-8 w-20 rounded-md border border-input bg-background px-2 text-xs" />
Platform Intent Score Status Post Actions {displayOpportunities.slice(0, limit).map((opp) => ( {opp.platform}
{getIntentIcon(opp.intent)} {opp.intent}
= 0.8 ? 'bg-green-500/20 text-green-400' : opp.relevanceScore >= 0.6 ? 'bg-amber-500/20 text-amber-400' : 'bg-red-500/20 text-red-400'}> {Math.round(opp.relevanceScore * 100)}% {opp.status || 'new'}

{opp.title}

{opp.snippet}

))}
) : isSearching ? (

Searching...

Scanning platforms for opportunities

) : (

Ready to Search

Select platforms and strategies, then click Find Opportunities

)}
{/* Detail Dialog */} setSelectedOpportunity(null)}> {selectedOpportunity && ( <>
= 0.8 ? 'bg-green-500/20 text-green-400' : selectedOpportunity.relevanceScore >= 0.6 ? 'bg-amber-500/20 text-amber-400' : 'bg-red-500/20 text-red-400'}> {Math.round(selectedOpportunity.relevanceScore * 100)}% Match {selectedOpportunity.platform} {selectedOpportunity.intent}
{selectedOpportunity.title} {selectedOpportunity.snippet}
setTagsInput(e.target.value)} className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm" placeholder="reddit, high-intent" />