VIBN Frontend for Coolify deployment
This commit is contained in:
354
app/[workspace]/project/[projectId]/mission/page.tsx
Normal file
354
app/[workspace]/project/[projectId]/mission/page.tsx
Normal file
@@ -0,0 +1,354 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user