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,245 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Loader2, CheckCircle2, Sparkles } from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
interface VisionFormProps {
projectId: string;
workspace?: string;
onComplete?: () => void;
}
export function VisionForm({ projectId, workspace, onComplete }: VisionFormProps) {
const [currentQuestion, setCurrentQuestion] = useState(1);
const [answers, setAnswers] = useState({
q1: '',
q2: '',
q3: '',
});
const [saving, setSaving] = useState(false);
const [generating, setGenerating] = useState(false);
const questions = [
{
number: 1,
key: 'q1' as const,
question: 'Who has the problem you want to fix and what is it?',
placeholder: 'E.g., Fantasy hockey GMs need an advantage over their competitors...',
},
{
number: 2,
key: 'q2' as const,
question: 'Tell me a story of this person using your tool and experiencing your vision?',
placeholder: 'E.g., The user connects their hockey pool site...',
},
{
number: 3,
key: 'q3' as const,
question: 'How much did that improve things for them?',
placeholder: 'E.g., They feel relieved and excited...',
},
];
const currentQ = questions[currentQuestion - 1];
const handleNext = () => {
if (!answers[currentQ.key].trim()) {
toast.error('Please answer the question before continuing');
return;
}
setCurrentQuestion(currentQuestion + 1);
};
const handleBack = () => {
setCurrentQuestion(currentQuestion - 1);
};
const handleSubmit = async () => {
if (!answers.q3.trim()) {
toast.error('Please answer the question');
return;
}
try {
setSaving(true);
// Save vision answers to Firestore
const response = await fetch(`/api/projects/${projectId}/vision`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
visionAnswers: {
q1: answers.q1,
q2: answers.q2,
q3: answers.q3,
allAnswered: true,
updatedAt: new Date().toISOString(),
},
}),
});
if (!response.ok) {
throw new Error('Failed to save vision answers');
}
toast.success('Vision captured! Generating your MVP plan...');
setSaving(false);
setGenerating(true);
// Trigger MVP generation
const mvpResponse = await fetch(`/api/projects/${projectId}/mvp-checklist`, {
method: 'POST',
});
if (!mvpResponse.ok) {
throw new Error('Failed to generate MVP plan');
}
setGenerating(false);
toast.success('MVP plan generated! Redirecting to plan page...');
// Redirect to plan page
if (workspace) {
setTimeout(() => {
window.location.href = `/${workspace}/project/${projectId}/plan`;
}, 1000);
}
onComplete?.();
} catch (error) {
console.error('Error saving vision:', error);
toast.error('Failed to save vision and generate plan');
setSaving(false);
setGenerating(false);
}
};
if (generating) {
return (
<div className="flex flex-col items-center justify-center h-[400px] space-y-4">
<Sparkles className="h-12 w-12 text-primary animate-pulse" />
<h3 className="text-xl font-semibold">Generating your MVP plan...</h3>
<p className="text-muted-foreground">This may take up to a minute</p>
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="max-w-2xl mx-auto py-8 space-y-6">
{/* Progress indicator */}
<div className="flex items-center justify-center space-x-4 mb-8">
{questions.map((q, idx) => (
<div key={q.number} className="flex items-center">
<div
className={cn(
'flex items-center justify-center w-8 h-8 rounded-full border-2 font-semibold text-sm transition-all',
currentQuestion > q.number
? 'bg-primary text-primary-foreground border-primary'
: currentQuestion === q.number
? 'border-primary text-primary'
: 'border-muted text-muted-foreground'
)}
>
{currentQuestion > q.number ? (
<CheckCircle2 className="h-4 w-4" />
) : (
q.number
)}
</div>
{idx < questions.length - 1 && (
<div
className={cn(
'w-12 h-0.5 mx-2 transition-all',
currentQuestion > q.number ? 'bg-primary' : 'bg-muted'
)}
/>
)}
</div>
))}
</div>
{/* Current question card */}
<Card>
<CardHeader>
<CardTitle className="text-2xl">
Question {currentQ.number} of 3
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-lg font-medium leading-relaxed">
{currentQ.question}
</label>
<Textarea
value={answers[currentQ.key]}
onChange={(e) =>
setAnswers({ ...answers, [currentQ.key]: e.target.value })
}
placeholder={currentQ.placeholder}
className="min-h-[120px] text-base"
autoFocus
/>
</div>
<div className="flex items-center justify-between pt-4">
<Button
variant="outline"
onClick={handleBack}
disabled={currentQuestion === 1}
>
Back
</Button>
{currentQuestion < 3 ? (
<Button onClick={handleNext}>
Next Question
</Button>
) : (
<Button onClick={handleSubmit} disabled={saving}>
{saving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>
<Sparkles className="mr-2 h-4 w-4" />
Generate MVP Plan
</>
)}
</Button>
)}
</div>
</CardContent>
</Card>
{/* Show all answers summary */}
{currentQuestion > 1 && (
<Card className="bg-muted/50">
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Your Vision So Far
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{questions.slice(0, currentQuestion - 1).map((q) => (
<div key={q.number} className="text-sm">
<p className="font-medium text-muted-foreground mb-1">
Q{q.number}: {q.question}
</p>
<p className="text-foreground">{answers[q.key]}</p>
</div>
))}
</CardContent>
</Card>
)}
</div>
);
}