VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View 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>
);
}