355 lines
10 KiB
TypeScript
355 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useParams, usePathname } from "next/navigation";
|
|
import {
|
|
Target,
|
|
Users,
|
|
AlertCircle,
|
|
TrendingUp,
|
|
Lightbulb,
|
|
Plus,
|
|
Edit,
|
|
Search,
|
|
Loader2,
|
|
Layout,
|
|
CheckCircle,
|
|
DollarSign,
|
|
Link as LinkIcon,
|
|
} from "lucide-react";
|
|
import {
|
|
PageTemplate,
|
|
PageSection,
|
|
PageCard,
|
|
PageGrid,
|
|
PageEmptyState,
|
|
} from "@/components/layout/page-template";
|
|
import { Button } from "@/components/ui/button";
|
|
import { MissionContextTree } from "@/components/mission/mission-context-tree";
|
|
import { MissionIdeaSection } from "@/components/mission/mission-idea-section";
|
|
import { auth } from "@/lib/firebase/config";
|
|
import { toast } from "sonner";
|
|
|
|
const MISSION_NAV_ITEMS = [
|
|
{ title: "Target Customer", icon: Users, href: "/mission" },
|
|
{ title: "Existing Solutions", icon: Layout, href: "/mission#solutions" },
|
|
];
|
|
|
|
interface MissionFramework {
|
|
targetCustomer: {
|
|
primaryAudience: string;
|
|
theirSituation: string;
|
|
relatedMarkets?: string[];
|
|
};
|
|
existingSolutions: Array<{
|
|
category: string;
|
|
description: string;
|
|
products: Array<{
|
|
name: string;
|
|
url?: string;
|
|
}>;
|
|
}>;
|
|
innovations: Array<{
|
|
title: string;
|
|
description: string;
|
|
}>;
|
|
ideaValidation: Array<{
|
|
title: string;
|
|
description: string;
|
|
}>;
|
|
financialSuccess: {
|
|
subscribers: number;
|
|
pricePoint: number;
|
|
retentionRate: number;
|
|
};
|
|
}
|
|
|
|
export default function MissionPage() {
|
|
const params = useParams();
|
|
const pathname = usePathname();
|
|
const workspace = params.workspace as string;
|
|
const projectId = params.projectId as string;
|
|
const [researchingMarket, setResearchingMarket] = useState(false);
|
|
const [framework, setFramework] = useState<MissionFramework | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [generating, setGenerating] = useState(false);
|
|
|
|
// Fetch mission framework on mount
|
|
useEffect(() => {
|
|
fetchFramework();
|
|
}, [projectId]);
|
|
|
|
const fetchFramework = async () => {
|
|
setLoading(true);
|
|
try {
|
|
// Fetch project data from Firestore to get the saved framework
|
|
const user = auth.currentUser;
|
|
const headers: HeadersInit = {};
|
|
|
|
if (user) {
|
|
const token = await user.getIdToken();
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch(`/api/projects/${projectId}`, {
|
|
headers,
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.project?.phaseData?.missionFramework) {
|
|
setFramework(data.project.phaseData.missionFramework);
|
|
console.log('[Mission] Loaded saved framework');
|
|
} else {
|
|
console.log('[Mission] No saved framework found');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[Mission] Error fetching framework:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleGenerateFramework = async () => {
|
|
setGenerating(true);
|
|
try {
|
|
const user = auth.currentUser;
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (user) {
|
|
const token = await user.getIdToken();
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch(`/api/projects/${projectId}/mission/generate`, {
|
|
method: 'POST',
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to generate mission framework');
|
|
}
|
|
|
|
const data = await response.json();
|
|
setFramework(data.framework);
|
|
toast.success('Mission framework generated successfully!');
|
|
} catch (error) {
|
|
console.error('Error generating framework:', error);
|
|
toast.error('Failed to generate mission framework');
|
|
} finally {
|
|
setGenerating(false);
|
|
}
|
|
};
|
|
|
|
const handleResearchMarket = async () => {
|
|
setResearchingMarket(true);
|
|
try {
|
|
const user = auth.currentUser;
|
|
if (!user) {
|
|
toast.error('Please sign in');
|
|
return;
|
|
}
|
|
|
|
const token = await user.getIdToken();
|
|
const response = await fetch(`/api/projects/${projectId}/research/market`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to conduct market research');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
toast.success(
|
|
`Market research complete! Found ${data.research.targetNiches.length} niches, ` +
|
|
`${data.research.competitors.length} competitors, and ${data.research.marketGaps.length} gaps.`
|
|
);
|
|
|
|
// Regenerate framework with new insights
|
|
await handleGenerateFramework();
|
|
} catch (error) {
|
|
console.error('Error conducting market research:', error);
|
|
toast.error('Failed to conduct market research');
|
|
} finally {
|
|
setResearchingMarket(false);
|
|
}
|
|
};
|
|
|
|
// Build sidebar items with full hrefs and active states
|
|
const sidebarItems = MISSION_NAV_ITEMS.map((item) => {
|
|
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
|
|
return {
|
|
...item,
|
|
href: fullHref,
|
|
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
|
};
|
|
});
|
|
|
|
if (loading) {
|
|
return (
|
|
<PageTemplate
|
|
sidebar={{
|
|
items: sidebarItems,
|
|
customContent: <MissionContextTree projectId={projectId} />,
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-center h-64">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
</PageTemplate>
|
|
);
|
|
}
|
|
|
|
if (!framework) {
|
|
return (
|
|
<PageTemplate
|
|
sidebar={{
|
|
items: sidebarItems,
|
|
customContent: <MissionContextTree projectId={projectId} />,
|
|
}}
|
|
>
|
|
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
|
<div className="rounded-full bg-muted p-6 mb-4">
|
|
<Lightbulb className="h-12 w-12 text-muted-foreground" />
|
|
</div>
|
|
<h3 className="text-xl font-semibold mb-2">No Mission Framework Yet</h3>
|
|
<p className="text-muted-foreground mb-6 max-w-md">
|
|
Generate your mission framework based on your project's insights and knowledge
|
|
</p>
|
|
<Button onClick={handleGenerateFramework} disabled={generating} size="lg">
|
|
{generating ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Generating...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Generate Mission Framework
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</PageTemplate>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageTemplate
|
|
sidebar={{
|
|
items: sidebarItems,
|
|
customContent: <MissionContextTree projectId={projectId} />,
|
|
}}
|
|
>
|
|
{/* Target Customer */}
|
|
<PageSection
|
|
title="Target Customer"
|
|
description="Who you're building for"
|
|
headerAction={
|
|
<Button size="sm" variant="ghost" onClick={handleGenerateFramework} disabled={generating}>
|
|
{generating ? (
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
) : (
|
|
<Edit className="h-4 w-4 mr-2" />
|
|
)}
|
|
Regenerate
|
|
</Button>
|
|
}
|
|
>
|
|
<PageCard>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="font-semibold mb-2">Primary Audience</h4>
|
|
<p className="text-muted-foreground">
|
|
{framework.targetCustomer.primaryAudience}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold mb-2">Their Situation</h4>
|
|
<p className="text-muted-foreground">
|
|
{framework.targetCustomer.theirSituation}
|
|
</p>
|
|
</div>
|
|
{framework.targetCustomer.relatedMarkets && framework.targetCustomer.relatedMarkets.length > 0 && (
|
|
<div>
|
|
<h4 className="font-semibold mb-2">Related Markets</h4>
|
|
<ul className="list-disc list-inside space-y-1">
|
|
{framework.targetCustomer.relatedMarkets.map((market, idx) => (
|
|
<li key={idx} className="text-sm text-muted-foreground">
|
|
{market}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PageCard>
|
|
</PageSection>
|
|
|
|
{/* Existing Solutions */}
|
|
<PageSection
|
|
title="Existing Solutions"
|
|
description="What alternatives already exist"
|
|
headerAction={
|
|
<Button
|
|
size="sm"
|
|
variant="default"
|
|
onClick={handleResearchMarket}
|
|
disabled={researchingMarket}
|
|
>
|
|
{researchingMarket ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Researching...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Search className="h-4 w-4 mr-2" />
|
|
Research Market
|
|
</>
|
|
)}
|
|
</Button>
|
|
}
|
|
>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{framework.existingSolutions.map((solution, idx) => (
|
|
<PageCard key={idx}>
|
|
<h4 className="font-semibold text-sm mb-3">{solution.category}</h4>
|
|
{solution.products && solution.products.length > 0 && (
|
|
<div className="space-y-2">
|
|
{solution.products.map((product, prodIdx) => (
|
|
<div key={prodIdx} className="text-sm">
|
|
{product.url ? (
|
|
<a
|
|
href={product.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
>
|
|
{product.name}
|
|
<LinkIcon className="h-3 w-3" />
|
|
</a>
|
|
) : (
|
|
<span className="text-muted-foreground">{product.name}</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</PageCard>
|
|
))}
|
|
</div>
|
|
</PageSection>
|
|
|
|
</PageTemplate>
|
|
);
|
|
}
|