VIBN Frontend for Coolify deployment
This commit is contained in:
479
components/project-creation-modal.tsx
Normal file
479
components/project-creation-modal.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Github,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
ChevronLeft
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { auth } from '@/lib/firebase/config';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
interface ProjectCreationModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initialWorkspacePath?: string;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export function ProjectCreationModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialWorkspacePath,
|
||||
workspace,
|
||||
}: ProjectCreationModalProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Steps: 1 = Name, 2 = GitHub, 3 = Instructions
|
||||
const [step, setStep] = useState(1);
|
||||
const [productName, setProductName] = useState('');
|
||||
const [selectedRepo, setSelectedRepo] = useState<any>(null);
|
||||
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [githubConnected, setGithubConnected] = useState(false);
|
||||
const [githubRepos, setGithubRepos] = useState<any[]>([]);
|
||||
const [loadingGithub, setLoadingGithub] = useState(false);
|
||||
|
||||
// Check GitHub connection on mount and when moving to step 2
|
||||
useEffect(() => {
|
||||
async function checkGitHub() {
|
||||
if (!open || step !== 2) return;
|
||||
|
||||
setLoadingGithub(true);
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
console.log('[ProjectModal] No user found');
|
||||
setLoadingGithub(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
|
||||
const statusResponse = await fetch('/api/github/connect', {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (statusResponse.ok) {
|
||||
const statusData = await statusResponse.json();
|
||||
console.log('[ProjectModal] GitHub status:', statusData);
|
||||
const isConnected = statusData.connected || false;
|
||||
setGithubConnected(isConnected);
|
||||
|
||||
if (isConnected) {
|
||||
const reposResponse = await fetch('/api/github/repos', {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (reposResponse.ok) {
|
||||
const repos = await reposResponse.json(); // API returns array directly, not { repos: [] }
|
||||
console.log('[ProjectModal] GitHub repos loaded:', repos.length, 'repos');
|
||||
setGithubRepos(Array.isArray(repos) ? repos : []);
|
||||
} else {
|
||||
console.error('[ProjectModal] Failed to fetch repos:', reposResponse.status);
|
||||
setGithubRepos([]);
|
||||
}
|
||||
} else {
|
||||
console.log('[ProjectModal] GitHub not connected');
|
||||
setGithubRepos([]);
|
||||
}
|
||||
} else {
|
||||
console.error('[ProjectModal] Failed to check GitHub status:', statusResponse.status);
|
||||
setGithubConnected(false);
|
||||
setGithubRepos([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ProjectModal] Error checking GitHub:', error);
|
||||
setGithubConnected(false);
|
||||
setGithubRepos([]);
|
||||
} finally {
|
||||
setLoadingGithub(false);
|
||||
}
|
||||
}
|
||||
|
||||
checkGitHub();
|
||||
}, [open, step]);
|
||||
|
||||
const resetModal = () => {
|
||||
setStep(1);
|
||||
setProductName('');
|
||||
setSelectedRepo(null);
|
||||
setCreatedProjectId(null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
// Reset after closing animation
|
||||
setTimeout(resetModal, 200);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleCreateProject = async () => {
|
||||
if (!productName.trim()) {
|
||||
toast.error('Product name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error('You must be signed in');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
|
||||
const projectData = {
|
||||
projectName: productName.trim(),
|
||||
projectType: selectedRepo ? 'existing' : 'scratch',
|
||||
slug: productName.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||
product: {
|
||||
name: productName,
|
||||
},
|
||||
...(selectedRepo && {
|
||||
githubRepo: selectedRepo.full_name,
|
||||
githubRepoId: selectedRepo.id,
|
||||
githubRepoUrl: selectedRepo.html_url,
|
||||
githubDefaultBranch: selectedRepo.default_branch,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch('/api/projects/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(projectData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCreatedProjectId(data.projectId);
|
||||
toast.success('Project created!');
|
||||
setStep(3); // Move to instructions
|
||||
} else {
|
||||
const error = await response.json();
|
||||
toast.error(error.error || 'Failed to create project');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
toast.error('An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success('Copied to clipboard!');
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
onOpenChange(false);
|
||||
if (createdProjectId) {
|
||||
router.push(`/${workspace}/project/${createdProjectId}/v_ai_chat`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === 1 && 'Create New Project'}
|
||||
{step === 2 && 'Connect GitHub Repository'}
|
||||
{step === 3 && 'Setup Complete!'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{step === 1 && 'Give your project a name to get started.'}
|
||||
{step === 2 && 'Select a GitHub repository to connect (optional).'}
|
||||
{step === 3 && 'Add the .vibn file to your project to enable tracking.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Step 1: Name */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="productName">Product Name *</Label>
|
||||
<Input
|
||||
id="productName"
|
||||
placeholder="e.g., TaskMaster, HealthTracker, MyApp"
|
||||
value={productName}
|
||||
onChange={(e) => setProductName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && productName.trim()) {
|
||||
if (githubConnected) {
|
||||
setStep(2);
|
||||
} else {
|
||||
handleCreateProject();
|
||||
}
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => setStep(2)}
|
||||
disabled={!productName.trim()}
|
||||
className="flex-1"
|
||||
>
|
||||
Next: {githubConnected ? 'Select Repository' : 'Connect GitHub'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{githubConnected
|
||||
? 'Connect your GitHub repository (optional)'
|
||||
: 'Connect GitHub or skip to continue'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: GitHub Selection */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setStep(1)}
|
||||
className="mb-2"
|
||||
>
|
||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
{loadingGithub ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">Checking GitHub connection...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto" key={`repos-${githubRepos.length}`}>
|
||||
<Button
|
||||
variant={selectedRepo === null ? "default" : "outline"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => setSelectedRepo(null)}
|
||||
>
|
||||
<CheckCircle2 className={`mr-2 h-4 w-4 ${selectedRepo === null ? '' : 'opacity-0'}`} />
|
||||
Skip - No GitHub repository
|
||||
</Button>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
{(() => {
|
||||
console.log('[ProjectModal Render] githubConnected:', githubConnected, 'repos:', githubRepos.length, 'reposData:', githubRepos);
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{githubRepos.length > 0 ? (
|
||||
// Show repos if we have any
|
||||
githubRepos.map((repo) => (
|
||||
<Button
|
||||
key={repo.id}
|
||||
variant={selectedRepo?.id === repo.id ? "default" : "outline"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => setSelectedRepo(repo)}
|
||||
>
|
||||
<CheckCircle2 className={`mr-2 h-4 w-4 ${selectedRepo?.id === repo.id ? '' : 'opacity-0'}`} />
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">{repo.full_name}</span>
|
||||
</Button>
|
||||
))
|
||||
) : githubConnected ? (
|
||||
// Connected but no repos
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No repositories found
|
||||
</p>
|
||||
) : (
|
||||
// Not connected - show connect button
|
||||
<div className="space-y-3 py-4">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
GitHub not connected yet
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
toast.error('Please sign in first');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await user.getIdToken();
|
||||
const response = await fetch('/api/github/oauth-url', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
returnTo: `/${workspace}/projects`
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const { authUrl } = await response.json();
|
||||
window.location.href = authUrl;
|
||||
} else {
|
||||
toast.error('Failed to initiate GitHub OAuth');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('GitHub OAuth error:', error);
|
||||
toast.error('Error connecting to GitHub');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
Connect GitHub
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleCreateProject}
|
||||
disabled={loading}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Project'
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Instructions */}
|
||||
{step === 3 && createdProjectId && (
|
||||
<div className="space-y-4">
|
||||
<Card className="border-green-500/50 bg-green-500/5">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-green-900 dark:text-green-100">
|
||||
Project created successfully!
|
||||
</p>
|
||||
<p className="text-sm text-green-700 dark:text-green-300 mt-1">
|
||||
Project ID: <code className="font-mono">{createdProjectId}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base font-semibold">Next Step: Add .vibn file</Label>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create a <code className="font-mono bg-muted px-1.5 py-0.5 rounded">.vibn</code> file
|
||||
in your project root with the following content:
|
||||
</p>
|
||||
|
||||
<div className="relative">
|
||||
<pre className="bg-muted p-4 rounded-lg text-sm font-mono overflow-x-auto">
|
||||
{`{
|
||||
"projectId": "${createdProjectId}",
|
||||
"version": "1.0.0"
|
||||
}`}
|
||||
</pre>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => copyToClipboard(`{\n "projectId": "${createdProjectId}",\n "version": "1.0.0"\n}`)}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">This enables:</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-1 ml-4">
|
||||
<li>• Automatic session tracking from your cursor</li>
|
||||
<li>• Cost monitoring per project</li>
|
||||
<li>• AI chat history linking</li>
|
||||
<li>• GitHub commit tracking</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{selectedRepo && (
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Github className="h-4 w-4 text-muted-foreground" />
|
||||
<a
|
||||
href={selectedRepo.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline flex items-center gap-1"
|
||||
>
|
||||
Open {selectedRepo.full_name}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => copyToClipboard(`{\n "projectId": "${createdProjectId}",\n "version": "1.0.0"\n}`)}
|
||||
className="flex-1"
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy .vibn Content
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleFinish}
|
||||
className="flex-1"
|
||||
size="lg"
|
||||
>
|
||||
Start AI Chat
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user