Spaces:
Paused
Paused
| 'use client' | |
| import { useState, useEffect } from 'react' | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" | |
| import { Badge } from "@/components/ui/badge" | |
| import { Button } from "@/components/ui/button" | |
| import { Input } from "@/components/ui/input" | |
| import { Textarea } from "@/components/ui/textarea" | |
| import { Label } from "@/components/ui/label" | |
| import { Loader2, Plus, ArrowLeft, Check } from "lucide-react" | |
| import { useToast } from '@/hooks/use-toast' | |
| import type { TemplateRegister } from '../project/[...id]/types' | |
| interface ExploreTemplatesProps { | |
| onProjectCreated?: (project: any) => void | |
| } | |
| type Step = 'select-template' | 'enter-details' | 'creating' | |
| export default function ExploreTemplates({ onProjectCreated }: ExploreTemplatesProps) { | |
| const [templateData, setTemplateData] = useState<TemplateRegister[]>([]) | |
| const [isLoadingTemplates, setIsLoadingTemplates] = useState(false) | |
| const [templatesLoaded, setTemplatesLoaded] = useState(false) | |
| const [currentStep, setCurrentStep] = useState<Step>('select-template') | |
| const [selectedTemplate, setSelectedTemplate] = useState<TemplateRegister | null>(null) | |
| const [projectName, setProjectName] = useState('') | |
| const [projectDescription, setProjectDescription] = useState('') | |
| const [isCreatingProject, setIsCreatingProject] = useState(false) | |
| const { toast } = useToast() | |
| // Fetch template data | |
| useEffect(() => { | |
| if (templatesLoaded) return | |
| const fetchTemplateData = async () => { | |
| setIsLoadingTemplates(true) | |
| try { | |
| const response = await fetch('/api/template/list') | |
| if (response.ok) { | |
| const data = await response.json() | |
| setTemplateData(data) | |
| setTemplatesLoaded(true) | |
| } else { | |
| console.error('Failed to fetch template data:', response.statusText) | |
| } | |
| } catch (error) { | |
| console.error('Error fetching template data:', error) | |
| } finally { | |
| setIsLoadingTemplates(false) | |
| } | |
| } | |
| fetchTemplateData() | |
| }, [templatesLoaded]) | |
| const handleTemplateSelect = (template: TemplateRegister) => { | |
| setSelectedTemplate(template) | |
| // Pre-fill with template-based suggestions | |
| setProjectName(`${template.name} Project`) | |
| setProjectDescription(template.description) | |
| setCurrentStep('enter-details') | |
| } | |
| const handleBackToTemplates = () => { | |
| setCurrentStep('select-template') | |
| setSelectedTemplate(null) | |
| setProjectName('') | |
| setProjectDescription('') | |
| } | |
| const handleCreateProject = async () => { | |
| if (!selectedTemplate || !projectName.trim() || !projectDescription.trim()) { | |
| toast({ | |
| title: "Error", | |
| description: "Please fill in both project name and description.", | |
| variant: "destructive", | |
| duration: 3000, | |
| }) | |
| return | |
| } | |
| setIsCreatingProject(true) | |
| setCurrentStep('creating') | |
| try { | |
| const projectData = { | |
| project_name: projectName.trim(), | |
| project_description: projectDescription.trim(), | |
| template_name: selectedTemplate.name, | |
| template_description: selectedTemplate.description | |
| } | |
| const response = await fetch('/api/projects/create', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(projectData) | |
| }) | |
| if (!response.ok) { | |
| const errorData = await response.json() | |
| throw new Error(errorData.error || 'Failed to create project') | |
| } | |
| const newProject = await response.json() | |
| toast({ | |
| title: "Project created successfully!", | |
| description: `Project "${newProject.name}" has been created using the ${selectedTemplate.name} template.`, | |
| duration: 5000, | |
| }) | |
| // Reset to initial state | |
| setCurrentStep('select-template') | |
| setSelectedTemplate(null) | |
| setProjectName('') | |
| setProjectDescription('') | |
| // Notify parent component about the new project | |
| if (onProjectCreated) { | |
| onProjectCreated(newProject) | |
| } | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : 'Unknown error occurred' | |
| toast({ | |
| title: "Error", | |
| description: `There was an error creating your project: ${message}`, | |
| variant: "destructive", | |
| duration: 5000, | |
| }) | |
| // Go back to details step on error | |
| setCurrentStep('enter-details') | |
| } finally { | |
| setIsCreatingProject(false) | |
| } | |
| } | |
| const renderStepIndicator = () => ( | |
| <div className="flex items-center justify-center mb-6"> | |
| <div className="flex items-center space-x-4"> | |
| {/* Step 1: Select Template */} | |
| <div className="flex items-center"> | |
| <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${ | |
| currentStep === 'select-template' | |
| ? 'bg-pink-600 text-white' | |
| : selectedTemplate | |
| ? 'bg-green-600 text-white' | |
| : 'bg-gray-200 text-gray-600' | |
| }`}> | |
| {selectedTemplate && currentStep !== 'select-template' ? <Check className="w-4 h-4" /> : '1'} | |
| </div> | |
| <span className="ml-2 text-sm font-medium text-gray-700">Select Template</span> | |
| </div> | |
| {/* Arrow */} | |
| <div className="w-8 h-px bg-gray-300"></div> | |
| {/* Step 2: Enter Details */} | |
| <div className="flex items-center"> | |
| <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${ | |
| currentStep === 'enter-details' | |
| ? 'bg-pink-600 text-white' | |
| : currentStep === 'creating' && projectName && projectDescription | |
| ? 'bg-green-600 text-white' | |
| : 'bg-gray-200 text-gray-600' | |
| }`}> | |
| {currentStep === 'creating' && projectName && projectDescription ? <Check className="w-4 h-4" /> : '2'} | |
| </div> | |
| <span className="ml-2 text-sm font-medium text-gray-700">Enter Details</span> | |
| </div> | |
| {/* Arrow */} | |
| <div className="w-8 h-px bg-gray-300"></div> | |
| {/* Step 3: Create Project */} | |
| <div className="flex items-center"> | |
| <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${ | |
| currentStep === 'creating' | |
| ? 'bg-pink-600 text-white' | |
| : 'bg-gray-200 text-gray-600' | |
| }`}> | |
| 3 | |
| </div> | |
| <span className="ml-2 text-sm font-medium text-gray-700">Create Project</span> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| const renderTemplateSelection = () => ( | |
| <div className="space-y-6"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h2 className="text-lg sm:text-xl font-bold text-gray-900">Choose a Template</h2> | |
| <p className="text-sm text-gray-600 mt-1">Select a template to get started with your project</p> | |
| </div> | |
| {isLoadingTemplates && ( | |
| <div className="flex items-center gap-2"> | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| <span className="text-sm text-gray-600">Loading templates...</span> | |
| </div> | |
| )} | |
| </div> | |
| {!isLoadingTemplates && templateData.length === 0 ? ( | |
| <div className="text-center py-12"> | |
| <p className="text-gray-500">No templates available</p> | |
| </div> | |
| ) : ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6"> | |
| {templateData.map((template, index) => ( | |
| <Card | |
| key={index} | |
| className="cursor-pointer transition-all hover:shadow-lg border-gray-200 hover:border-pink-300" | |
| onClick={() => handleTemplateSelect(template)} | |
| > | |
| <CardHeader className="pb-3"> | |
| <div className="flex items-start justify-between"> | |
| <CardTitle className="text-base sm:text-lg">{template.name}</CardTitle> | |
| <Badge variant="secondary" className="text-xs"> | |
| v{template.starfish_version} | |
| </Badge> | |
| </div> | |
| <CardDescription className="text-sm text-gray-600"> | |
| by {template.author} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <p className="text-sm text-gray-700 line-clamp-3"> | |
| {template.description} | |
| </p> | |
| {template.dependencies.length > 0 && ( | |
| <div> | |
| <p className="text-xs font-medium text-gray-600 mb-1">Dependencies:</p> | |
| <div className="flex flex-wrap gap-1"> | |
| {template.dependencies.slice(0, 3).map((dep, depIndex) => ( | |
| <Badge key={depIndex} variant="outline" className="text-xs"> | |
| {dep} | |
| </Badge> | |
| ))} | |
| {template.dependencies.length > 3 && ( | |
| <Badge variant="outline" className="text-xs"> | |
| +{template.dependencies.length - 3} more | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {template.input_example && ( | |
| <div> | |
| <p className="text-xs font-medium text-gray-600 mb-1">Example Input:</p> | |
| <code className="text-xs bg-gray-100 p-2 rounded block overflow-hidden text-ellipsis whitespace-nowrap"> | |
| {typeof template.input_example === 'object' | |
| ? JSON.stringify(template.input_example) | |
| : template.input_example} | |
| </code> | |
| </div> | |
| )} | |
| <Button className="w-full bg-pink-600 hover:bg-pink-700 focus:ring-pink-500 text-sm"> | |
| Select Template | |
| </Button> | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| const renderProjectDetails = () => ( | |
| <div className="space-y-6"> | |
| <div className="flex items-center gap-4"> | |
| <Button | |
| variant="outline" | |
| onClick={handleBackToTemplates} | |
| className="flex items-center gap-2" | |
| > | |
| <ArrowLeft className="h-4 w-4" /> | |
| Back to Templates | |
| </Button> | |
| <div> | |
| <h2 className="text-lg sm:text-xl font-bold text-gray-900">Project Details</h2> | |
| <p className="text-sm text-gray-600 mt-1">Configure your project name and description</p> | |
| </div> | |
| </div> | |
| {/* Selected Template Info */} | |
| {selectedTemplate && ( | |
| <Card className="border-pink-200 bg-pink-50"> | |
| <CardHeader className="pb-3"> | |
| <div className="flex items-center gap-2"> | |
| <Check className="h-5 w-5 text-pink-600" /> | |
| <CardTitle className="text-pink-800">Selected Template: {selectedTemplate.name}</CardTitle> | |
| </div> | |
| <CardDescription className="text-pink-700"> | |
| {selectedTemplate.description} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="flex items-center gap-4 text-sm text-pink-600"> | |
| <span>by {selectedTemplate.author}</span> | |
| <span>β’</span> | |
| <span>v{selectedTemplate.starfish_version}</span> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {/* Project Details Form */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Project Information</CardTitle> | |
| <CardDescription> | |
| Enter the name and description for your new project | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="project-name">Project Name *</Label> | |
| <Input | |
| id="project-name" | |
| value={projectName} | |
| onChange={(e) => setProjectName(e.target.value)} | |
| placeholder="Enter your project name" | |
| className="w-full" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="project-description">Project Description *</Label> | |
| <Textarea | |
| id="project-description" | |
| value={projectDescription} | |
| onChange={(e) => setProjectDescription(e.target.value)} | |
| placeholder="Describe what your project will do" | |
| className="min-h-[100px] w-full" | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <div className="flex justify-end"> | |
| <Button | |
| onClick={handleCreateProject} | |
| disabled={!projectName.trim() || !projectDescription.trim()} | |
| className="bg-pink-600 hover:bg-pink-700 text-white flex items-center gap-2 disabled:bg-gray-400 disabled:cursor-not-allowed" | |
| > | |
| <Plus className="h-4 w-4" /> | |
| Create Project | |
| </Button> | |
| </div> | |
| </div> | |
| ) | |
| const renderCreatingProject = () => ( | |
| <div className="space-y-6"> | |
| <div className="text-center py-12"> | |
| <div className="flex justify-center mb-4"> | |
| <Loader2 className="h-12 w-12 animate-spin text-pink-600" /> | |
| </div> | |
| <h2 className="text-xl font-bold text-gray-900 mb-2">Creating Your Project</h2> | |
| <p className="text-gray-600 mb-4"> | |
| Setting up "{projectName}" with the {selectedTemplate?.name} template... | |
| </p> | |
| <div className="bg-gray-50 rounded-lg p-4 max-w-md mx-auto"> | |
| <div className="text-sm text-gray-600 space-y-1"> | |
| <div>β Template selected: {selectedTemplate?.name}</div> | |
| <div>β Project details configured</div> | |
| <div className="flex items-center gap-2"> | |
| <Loader2 className="h-3 w-3 animate-spin" /> | |
| Creating project... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| return ( | |
| <div className="px-4 sm:px-0"> | |
| <div className="bg-white shadow-[0_1px_2px_rgba(0,0,0,0.05)] rounded-lg p-4 sm:p-6 mb-6 sm:mb-8 border border-gray-200"> | |
| {renderStepIndicator()} | |
| {currentStep === 'select-template' && renderTemplateSelection()} | |
| {currentStep === 'enter-details' && renderProjectDetails()} | |
| {currentStep === 'creating' && renderCreatingProject()} | |
| </div> | |
| </div> | |
| ) | |
| } |