codenuk_frontend_mine/src/components/main-dashboard.tsx
2025-10-10 09:02:08 +05:30

2644 lines
108 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette, GitBranch } from "lucide-react"
import { useTemplates } from "@/hooks/useTemplates"
import { CustomTemplateForm } from "@/components/custom-template-form"
import { EditTemplateForm } from "@/components/edit-template-form"
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
import { DatabaseTemplate, TemplateFeature } from "@/lib/template-service"
import AICustomFeatureCreator from "@/components/ai/AICustomFeatureCreator"
import { BACKEND_URL } from "@/config/backend"
// Removed Tooltip import as we are no longer using tooltips for title/description
import WireframeCanvas from "@/components/wireframe-canvas"
import TypeformSurvey, { SurveySummary } from "@/components/business-context/typeform-survey"
import PromptSidePanel from "@/components/prompt-side-panel"
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
import { getAccessToken } from "@/components/apis/authApiClients"
import TechStackSummary from "@/components/tech-stack-summary"
import { attachRepository, getGitHubAuthStatus, AttachRepositoryResponse, connectGitHubWithRepo, initiateGitHubOAuth } from "@/lib/api/github"
import ViewUserReposButton from "@/components/github/ViewUserReposButton"
import { ErrorBanner } from "@/components/ui/error-banner"
interface Template {
id: string
title: string
description: string
category: string
features: string[]
complexity: number
timeEstimate: string
techStack: string[]
popularity?: number
lastUpdated?: string
type?: string
icon?: string | null
gradient?: string | null
border?: string | null
text?: string | null
subtext?: string | null
is_active?: boolean
created_at?: string
updated_at?: string
featureCount?: number
is_custom?: boolean; // Add this field to identify custom templates
}
function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => void }) {
const [selectedCategory, setSelectedCategory] = useState("all")
const [searchQuery, setSearchQuery] = useState("")
const [showCustomForm, setShowCustomForm] = useState(false)
const [showCreateOptionDialog, setShowCreateOptionDialog] = useState(false)
const [showGitForm, setShowGitForm] = useState(false)
const [gitProvider, setGitProvider] = useState('')
const [gitUrl, setGitUrl] = useState('')
const [gitBranch, setGitBranch] = useState('main')
const [gitAuthMethod, setGitAuthMethod] = useState('')
const [gitCredentials, setGitCredentials] = useState({
username: '',
password: '',
token: '',
sshKey: ''
})
const [authLoading, setAuthLoading] = useState(false)
const [gitStep, setGitStep] = useState<'provider' | 'url'>('provider')
const [authUrl, setAuthUrl] = useState('')
const [isGeneratingAuth, setIsGeneratingAuth] = useState(false)
const [isGithubConnected, setIsGithubConnected] = useState<boolean | null>(null)
const [connectionError, setConnectionError] = useState<string | null>(null)
useEffect(() => {
(async () => {
try {
setConnectionError(null)
const status = await getGitHubAuthStatus()
setIsGithubConnected(!!status?.data?.connected)
} catch (error: any) {
console.warn('Failed to check GitHub auth status:', error)
setIsGithubConnected(false)
// Check if it's a connectivity issue
if (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT') {
setConnectionError('Unable to connect to the server. Please ensure the backend is running.')
} else if (error?.response?.status >= 500) {
setConnectionError('Server error. Please try again later.')
}
}
})()
}, [])
const [editingTemplate, setEditingTemplate] = useState<DatabaseTemplate | null>(null)
// Generate authentication by hitting the same attach endpoint and using its auth_url
const generateAuthUrl = async () => {
if (!gitUrl.trim()) return
setIsGeneratingAuth(true)
try {
// Persist pending attach so we can resume after OAuth
try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined)
}))
} catch {}
const result: AttachRepositoryResponse = await attachRepository({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined),
})
// Debug logging
console.log('📦 Full result object:', result)
console.log('📦 result.success value:', result?.success)
console.log('📦 result.success type:', typeof result?.success)
console.log('📦 Strict equality check:', result?.success === true)
// Check if response is successful
if (result?.success !== true) {
console.error('❌ Response indicates failure:', result)
throw new Error('Repository attachment failed')
}
const isPrivate = result?.data?.is_public === false || result?.data?.requires_auth === true
console.log('✅ Repository attached successfully:', result)
const repoType = isPrivate ? 'private' : 'public'
alert(`Repository attached successfully! (${repoType}) You can now proceed with your project.`)
setShowCreateOptionDialog(false)
setShowGitForm(false)
setGitProvider('')
setGitUrl('')
setGitBranch('main')
} catch (err: any) {
const status = err?.response?.status
let data = err?.response?.data
// Some proxies or middlewares may stringify JSON error bodies; handle that here
if (typeof data === 'string') {
try { data = JSON.parse(data) } catch {}
}
console.log('❌ Error attaching repository:', {
status,
data,
message: err?.message,
code: err?.code,
url: gitUrl.trim()
})
if (status === 401 && (data?.requires_auth || data?.auth_url || data?.service_auth_url)) {
console.log('🔐 Private repository detected - initiating GitHub OAuth with repository context')
// Reset loading state before redirect
setIsGeneratingAuth(false)
// Use the new OAuth helper that will auto-attach the repo after authentication
setTimeout(() => {
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
console.error('OAuth initiation failed:', oauthError)
alert('Failed to initiate GitHub authentication. Please try again.')
})
}, 100)
return
}
if (status === 403) {
// Reset loading state before showing dialog
setIsGeneratingAuth(false)
// Repository not accessible with current GitHub account - prompt to re-authenticate
setTimeout(() => {
const confirmReauth = confirm('Repository not accessible with your current GitHub account.\n\nThis could mean:\n- The repository belongs to a different GitHub account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with GitHub?')
if (confirmReauth) {
console.log('🔐 Re-authenticating with GitHub for private repository access')
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
console.error('OAuth initiation failed:', oauthError)
alert('Failed to initiate GitHub authentication. Please try again.')
})
}
}, 100)
return
}
if (status === 404) {
alert('Repository not found - please check the URL and try again')
return
}
console.error('❌ Full error details:', err)
const errorMessage = data?.message || err?.message || 'Failed to attach repository. Please check the URL and try again.'
alert(errorMessage)
} finally {
setIsGeneratingAuth(false)
}
}
// Authentication handlers for different providers
const handleOAuthAuth = async (provider: string) => {
setAuthLoading(true)
try {
const providerConfig = gitProviders[provider as keyof typeof gitProviders]
if (!providerConfig.oauthEndpoint) {
throw new Error('OAuth not supported for this provider')
}
// For GitHub, use the new OAuth helper
if (provider === 'github') {
console.log('Initiating GitHub OAuth flow...')
initiateGitHubOAuth()
return
}
// For other providers, use the old method
window.open(providerConfig.oauthEndpoint, '_blank', 'width=600,height=700')
alert(`Redirecting to ${providerConfig.name} OAuth...`)
} catch (error) {
console.error('OAuth error:', error)
alert('OAuth authentication failed')
} finally {
setAuthLoading(false)
}
}
const handleTokenAuth = async (provider: string, token: string) => {
setAuthLoading(true)
try {
const providerConfig = gitProviders[provider as keyof typeof gitProviders]
// Validate token with provider API
const response = await fetch(`${providerConfig.apiEndpoint}/user`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
})
if (!response.ok) {
throw new Error('Invalid token')
}
const userData = await response.json()
alert(`Authenticated as ${userData.login || userData.username || userData.name}`)
return true
} catch (error) {
console.error('Token auth error:', error)
alert('Token authentication failed')
return false
} finally {
setAuthLoading(false)
}
}
const handleUsernamePasswordAuth = async (provider: string, username: string, password: string) => {
setAuthLoading(true)
try {
// For username/password auth, you'd typically use Basic Auth
// or convert to token-based auth
const credentials = btoa(`${username}:${password}`)
const response = await fetch(`${gitProviders[provider as keyof typeof gitProviders].apiEndpoint}/user`, {
headers: {
'Authorization': `Basic ${credentials}`,
'Accept': 'application/json'
}
})
if (!response.ok) {
throw new Error('Invalid credentials')
}
const userData = await response.json()
alert(`Authenticated as ${userData.login || userData.username || userData.name}`)
return true
} catch (error) {
console.error('Username/password auth error:', error)
alert('Authentication failed')
return false
} finally {
setAuthLoading(false)
}
}
// Provider-specific configuration
const gitProviders = {
github: {
name: 'GitHub',
icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z',
placeholder: 'https://github.com/org/repo.git',
authMethods: ['token', 'ssh', 'oauth'],
oauthEndpoint: '/api/auth/github',
apiEndpoint: 'https://api.github.com'
},
bitbucket: {
name: 'Bitbucket',
icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z',
placeholder: 'https://bitbucket.org/org/repo.git',
authMethods: ['username_password', 'app_password', 'oauth'],
oauthEndpoint: '/api/auth/bitbucket',
apiEndpoint: 'https://api.bitbucket.org/2.0'
},
gitlab: {
name: 'GitLab',
icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z',
placeholder: 'https://gitlab.com/org/repo.git',
authMethods: ['token', 'oauth', 'ssh'],
oauthEndpoint: '/api/auth/gitlab',
apiEndpoint: 'https://gitlab.com/api/v4'
},
other: {
name: 'Other Git',
icon: 'M12 0C5.374 0 0 5.373 0 12s5.374 12 12 12 12-5.373 12-12S18.626 0 12 0zm0 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-6h2v2h-2v-2zm0-8h2v6h-2V8z',
placeholder: 'https://your-git-server.com/org/repo.git',
authMethods: ['username_password', 'token', 'ssh'],
oauthEndpoint: null,
apiEndpoint: null
}
}
const [deletingTemplate, setDeletingTemplate] = useState<DatabaseTemplate | null>(null)
const [deleteLoading, setDeleteLoading] = useState(false)
// Keep a stable list of all categories seen so the filter chips don't disappear
const [knownCategories, setKnownCategories] = useState<Set<string>>(new Set(["all"]))
// Cache counts per category using the API totals for each filtered fetch
// Use undefined to indicate "unknown" instead of showing wrong initial counts
const [categoryCounts, setCategoryCounts] = useState<Record<string, number | undefined>>({ all: 0 })
// Track per-card expanded state for descriptions
const [expandedDescriptions, setExpandedDescriptions] = useState<Record<string, boolean>>({})
const [descDialogOpen, setDescDialogOpen] = useState(false)
const [descDialogData, setDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' })
const {
user,
combined,
loading,
error,
paginationState,
createTemplate,
updateTemplate,
deleteTemplate,
fetchTemplatesWithPagination,
loadMoreTemplates,
categories: hookCategories,
} = useTemplates()
// Initial fetch is handled inside useTemplates hook; avoid duplicate fetch here
// Handle category changes immediately (no debounce) so switching chips is snappy
useEffect(() => {
if (selectedCategory !== paginationState.selectedCategory) {
console.log('[TemplateSelectionStep] Triggering fetch due to category change:', { selectedCategory });
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: selectedCategory,
search: searchQuery,
resetPagination: true,
});
}
}, [selectedCategory, paginationState.selectedCategory, paginationState.pageSize, searchQuery, fetchTemplatesWithPagination]);
// Handle search changes with debouncing
useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchQuery !== paginationState.searchQuery) {
console.log('[TemplateSelectionStep] Triggering fetch due to search change:', { searchQuery });
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: selectedCategory,
search: searchQuery,
resetPagination: true,
});
}
}, 500);
return () => clearTimeout(timeoutId);
}, [searchQuery, selectedCategory, paginationState.searchQuery, paginationState.pageSize, fetchTemplatesWithPagination]);
// Track categories seen across any fetch so the chips remain visible
useEffect(() => {
if (!combined?.data?.length) return;
setKnownCategories((prev) => {
const next = new Set(prev);
combined.data.forEach((t) => {
if (t.category) next.add(t.category);
});
return next;
});
}, [combined?.data]);
// Seed known categories and counts from hookCategories (pre-fetched full counts)
useEffect(() => {
if (!hookCategories || hookCategories.length === 0) return;
setKnownCategories((prev) => {
const next = new Set(prev);
hookCategories.forEach((c) => next.add(c.id));
return next;
});
setCategoryCounts((prev) => {
const next: Record<string, number | undefined> = { ...prev };
hookCategories.forEach((c) => {
next[c.id] = c.count;
});
// Ensure 'all' exists if provided by hook
const allFromHook = hookCategories.find((c) => c.id === 'all')?.count;
if (typeof allFromHook === 'number') next['all'] = allFromHook;
return next;
});
}, [hookCategories]);
// Update counts cache based on API totals for the currently selected category
useEffect(() => {
const currentCat = paginationState.selectedCategory || 'all';
const totalForFilter = paginationState.total || 0;
setCategoryCounts((prev) => ({
...prev,
[currentCat]: totalForFilter,
}));
}, [paginationState.selectedCategory, paginationState.total]);
const templates: Template[] = combined?.data?.length
? combined.data.map((t) => {
console.log('[TemplateSelectionStep] Processing template:', {
id: t.id,
title: t.title,
category: t.category,
feature_count: t.feature_count
});
return {
id: t.id,
title: t.title,
description: t.description || "No description available",
category: t.category,
features: [],
complexity: 3, // Default complexity since DatabaseTemplate doesn't have this property
timeEstimate: "2-4 weeks",
techStack: ["Next.js", "PostgreSQL", "Tailwind CSS"],
popularity: t.avg_rating ? Math.round(t.avg_rating * 20) : 75,
lastUpdated: t.updated_at ? new Date(t.updated_at).toISOString().split('T')[0] : undefined,
type: t.type,
icon: t.icon,
gradient: t.gradient,
border: t.border,
text: t.text,
subtext: t.subtext,
is_active: t.is_active,
created_at: t.created_at,
updated_at: t.updated_at,
featureCount: t.feature_count || 0, // Add feature count from API
is_custom: t.is_custom, // Add is_custom field
};
})
: [];
// Debug logging
console.log('[TemplateSelectionStep] Debug:', {
hasCombined: !!combined,
hasData: !!combined?.data,
dataType: typeof combined?.data,
isArray: Array.isArray(combined?.data),
dataLength: combined?.data?.length || 0,
templatesLength: templates.length,
paginationState,
templates: templates.map((t) => ({ id: t.id, title: t.title, category: t.category })),
});
const getCategories = () => {
const categoryMap = new Map<string, { name: string; icon: React.ComponentType<{ className?: string }>; count: number }>();
// All category: prefer live total when currently filtered by 'all',
// otherwise show the last cached overall total (if any)
const liveAllTotal = paginationState.selectedCategory === 'all' ? (paginationState.total || 0) : 0;
const cachedAllTotal = categoryCounts['all'] ?? 0;
categoryMap.set("all", { name: "All Templates", icon: Globe, count: Math.max(liveAllTotal, cachedAllTotal) });
// Build chips from the union of all categories we have ever seen
Array.from(knownCategories)
.filter((c) => c !== 'all')
.forEach((categoryId) => {
const lower = categoryId.toLowerCase();
let icon = Code;
if (lower.includes('marketing') || lower.includes('branding')) icon = Zap;
else if (lower.includes('seo') || lower.includes('content')) icon = BarChart3;
else if (lower.includes('food') || lower.includes('delivery')) icon = Users;
// Prefer cached count for this category (set when that filter is active).
// Do NOT fallback to visible page count to avoid misleading numbers.
const count = categoryCounts[categoryId];
categoryMap.set(categoryId, {
name: categoryId,
icon,
// If count is unknown, represent as 0 in data and handle placeholder in UI
count: typeof count === 'number' ? count : 0,
});
});
return Array.from(categoryMap.entries()).map(([id, data]) => ({ id, ...data }));
};
const categories = getCategories();
const getComplexityColor = (complexity: number) => {
if (complexity <= 2) return "bg-emerald-900/40 text-emerald-300 border border-emerald-800"
if (complexity <= 3) return "bg-amber-900/40 text-amber-300 border border-amber-800"
return "bg-rose-900/40 text-rose-300 border border-rose-800"
}
const getComplexityLabel = (complexity: number) => {
if (complexity <= 2) return "Simple"
if (complexity <= 3) return "Moderate"
return "Complex"
}
// Truncate helper to restrict displayed characters
const TITLE_MAX_CHARS = 15;
const DESC_MAX_CHARS = 120;
const truncate = (value: string | undefined | null, max: number) => {
const v = (value || '').trim();
if (v.length <= max) return v;
return v.slice(0, Math.max(0, max - 1)) + '…';
};
const handleCreateTemplate = async (templateData: Partial<DatabaseTemplate>): Promise<DatabaseTemplate> => {
try {
const created = await createTemplate({
...(templateData as any),
// Ensure backend routes to custom_templates table
is_custom: true,
source: 'custom',
// Attach user id so custom template is associated with creator
user_id: (user as any)?.id,
} as any);
setShowCustomForm(false);
return created;
} catch (error) {
console.error('[TemplateSelectionStep] Error creating template:', error);
// Re-throw to satisfy the return type contract when errors should propagate
throw error as Error;
}
};
const handleCreateFromGit = async () => {
try {
if (!gitUrl.trim()) {
alert('Please enter a Git repository URL');
return;
}
// Attach the repository via backend (skip template creation)
try {
await attachRepository({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined),
})
// Show success message and reset form
alert('Repository attached successfully! You can now proceed with your project.');
setShowCreateOptionDialog(false)
setShowGitForm(false)
setGitProvider('')
setGitUrl('')
setGitBranch('main')
// Return a mock template object to proceed to next step
return {
id: 'git-imported',
title: `Imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}`,
description: `Template imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}: ${gitUrl}`,
type: 'custom',
category: 'imported',
is_custom: true,
source: 'git',
git_url: gitUrl.trim(),
git_branch: gitBranch?.trim() || 'main',
git_provider: gitProvider
}
} catch (attachErr) {
console.error('[TemplateSelectionStep] attachRepository failed:', attachErr)
const err: any = attachErr
const status = err?.response?.status
const data = err?.response?.data
console.log('🔍 HandleCreateFromGit error response:', { status, data })
// If backend signals GitHub auth required, open the OAuth URL for this user
if (status === 401 && (data?.requires_auth || data?.message?.includes('authentication'))) {
const url: string = data?.auth_url
if (!url) {
alert('Authentication URL is missing.');
return
}
// Persist pending repo so we resume after OAuth callback
try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined)
}))
} catch {}
console.log('🔐 Redirecting to GitHub OAuth for repository attachment:', url)
// Force same-tab redirect directly to GitHub consent screen
window.location.replace(url)
return
}
if (status === 403) {
alert('Repository not accessible - you may not have permission to access this repository')
return
}
if (status === 404) {
alert('Repository not found - please check the URL and try again')
return
}
alert(data?.message || 'Failed to attach repository. Please verify the URL/branch and your auth.');
return;
}
} catch (error) {
console.error('[TemplateSelectionStep] Error importing from Git:', error)
const errorMessage = error instanceof Error ? error.message : 'Failed to import from Git'
alert(`Error: ${errorMessage}`)
throw error as Error
}
}
const handleUpdateTemplate = async (id: string, templateData: Partial<DatabaseTemplate>) => {
try {
// Find the template to determine if it's custom
const template = templates.find(t => t.id === id);
const isCustom = template?.is_custom || false;
await updateTemplate(id, templateData, isCustom);
setEditingTemplate(null);
} catch (error) {
console.error('[TemplateSelectionStep] Error updating template:', error);
// Show user-friendly error message
const errorMessage = error instanceof Error ? error.message : 'Failed to update template';
alert(`Error updating template: ${errorMessage}`);
}
};
const handleDeleteTemplate = async () => {
if (!deletingTemplate) return;
setDeleteLoading(true);
try {
// Find the template to determine if it's custom
const template = templates.find(t => t.id === deletingTemplate.id);
const isCustom = template?.is_custom || false;
await deleteTemplate(deletingTemplate.id, isCustom);
setDeletingTemplate(null);
} catch (error) {
console.error('[TemplateSelectionStep] Error deleting template:', error);
// Show user-friendly error message
const errorMessage = error instanceof Error ? error.message : 'Failed to delete template';
alert(`Error deleting template: ${errorMessage}`);
} finally {
setDeleteLoading(false);
}
};
if (loading && templates.length === 0) {
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Loading Templates...</h1>
<p className="text-xl text-white/60">Fetching templates from /merged API</p>
</div>
<div className="flex justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-orange-500"></div>
</div>
</div>
);
}
if (error) {
// Check if this is an authentication error
const isAuthError = error.includes('Please sign in') || error.includes('Authentication required');
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="text-center space-y-3">
{isAuthError ? (
<>
<div className="flex items-center justify-center space-x-2 text-orange-400">
<AlertCircle className="h-6 w-6" />
<h1 className="text-2xl font-bold">Sign In Required</h1>
</div>
<p className="text-white/60">{error}</p>
<div className="flex justify-center space-x-4">
<Button
onClick={() => {
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search)
window.location.href = `/signin?returnUrl=${returnUrl}`
}}
className="bg-orange-500 hover:bg-orange-400 text-black"
>
Sign In
</Button>
<Button
onClick={() => {
// Try to fetch public templates without user ID
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: selectedCategory,
search: searchQuery,
resetPagination: true,
});
}}
variant="outline"
className="border-white/20 text-white hover:bg-white/10"
>
Browse Public Templates
</Button>
</div>
</>
) : (
<>
<div className="flex items-center justify-center space-x-2 text-red-400">
<AlertCircle className="h-6 w-6" />
<h1 className="text-2xl font-bold">Error Loading Templates</h1>
</div>
<p className="text-white/60">{error}</p>
<Button
onClick={() =>
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: selectedCategory,
search: searchQuery,
resetPagination: true,
})
}
className="bg-orange-500 hover:bg-orange-400 text-black"
>
Retry
</Button>
</>
)}
</div>
</div>
);
}
// Show custom template form
if (showCustomForm) {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Create Custom Template</h1>
<p className="text-xl text-white/60">Design your own project template</p>
</div>
<CustomTemplateForm
onSubmit={async (templateData) => {
await handleCreateTemplate(templateData);
}}
onCancel={() => setShowCustomForm(false)}
/>
</div>
)
}
// Show edit template form
if (editingTemplate) {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Edit Template</h1>
<p className="text-xl text-white/60">Modify your template settings</p>
</div>
<EditTemplateForm
template={editingTemplate}
onSubmit={handleUpdateTemplate}
onCancel={() => setEditingTemplate(null)}
/>
</div>
)
}
// Show delete confirmation dialog
if (deletingTemplate) {
return (
<>
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Choose Your Project Template</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Select from our comprehensive library of professionally designed templates
</p>
</div>
</div>
<DeleteConfirmationDialog
templateTitle={deletingTemplate.title}
onConfirm={handleDeleteTemplate}
onCancel={() => setDeletingTemplate(null)}
loading={deleteLoading}
/>
</>
)
}
return (
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Choose Your Project Template</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Select from our comprehensive library of professionally designed templates
</p>
{/* Connection Error Banner */}
{connectionError && (
<div className="max-w-2xl mx-auto">
<ErrorBanner
title="Connection Issue"
message={connectionError}
onRetry={async () => {
setConnectionError(null)
// Retry the GitHub auth status check
try {
const status = await getGitHubAuthStatus()
setIsGithubConnected(!!status?.data?.connected)
} catch (error: any) {
console.warn('Retry failed:', error)
setIsGithubConnected(false)
if (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT') {
setConnectionError('Unable to connect to the server. Please ensure the backend is running.')
}
}
}}
/>
</div>
)}
{!user?.id && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 max-w-2xl mx-auto">
<p className="text-orange-300 text-sm">
You&apos;re currently viewing public templates.
<Button
variant="link"
onClick={() => {
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search)
window.location.href = `/signin?returnUrl=${returnUrl}`
}}
className="text-orange-400 hover:text-orange-300 p-0 h-auto font-semibold ml-1"
>
Sign in
</Button>
{' '}to access your personal templates and create custom ones.
</p>
</div>
)}
</div>
{/* Right-aligned quick navigation to user repos */}
<div className="flex justify-end space-x-2">
<ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" />
</div>
<div className="space-y-4">
<div className="max-w-2xl mx-auto relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 h-5 w-5" />
<Input
placeholder="Search templates, features, or technologies..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 h-12 text-lg border border-white/10 bg-white/5 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30 rounded-xl"
/>
{paginationState.loading && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-orange-500"></div>
</div>
)}
</div>
{(() => {
const renderChip = (category: { id: string; name: string; icon: React.ComponentType<{ className?: string }>; count: number }) => {
const Icon = category.icon;
const active = selectedCategory === category.id;
const knownCount = category.id === 'all'
? Math.max(
selectedCategory === 'all' ? (paginationState.total || 0) : 0,
categoryCounts['all'] ?? 0
)
: categoryCounts[category.id];
// Fallback to currently visible items count for initial render if unknown
const fallbackCount = category.id === 'all' ? templates.length : templates.filter((t) => t.category === category.id).length;
const displayCount = typeof knownCount === 'number' ? knownCount : fallbackCount;
return (
<button
key={`cat-${category.id}`}
onClick={() => setSelectedCategory(category.id)}
className={`shrink-0 whitespace-nowrap flex items-center space-x-3 px-6 py-3 rounded-xl border transition-all cursor-pointer ${
active ? "bg-orange-500 text-black border-orange-500" : "bg-white/5 text-white/80 border-white/10 hover:bg-white/10"
}`}
>
<div className={`p-2 rounded-lg ${active ? "bg-white/10" : "bg-white/10"}`}>
<Icon className="h-5 w-5" />
</div>
<div className="text-left">
<div className="font-semibold">{category.name}</div>
<div className={`text-sm ${active ? "text-black/80" : "text-white/60"}`}>
{`${displayCount} templates`}
</div>
</div>
</button>
);
};
const allChip = categories.find((c) => c.id === 'all');
const rest = categories.filter((c) => c.id !== 'all');
return (
<div className="flex items-center gap-3">
{allChip && (
<div className="shrink-0">
{renderChip(allChip)}
</div>
)}
<div className="relative overflow-hidden flex-1 pb-2">
<div className="flex gap-3 whitespace-nowrap animate-scroll-x will-change-transform">
{rest.map(renderChip)}
{rest.map((c) => ({ ...c, id: `${c.id}-dup` })).map(renderChip)}
</div>
<style jsx>{`
@keyframes scroll-x { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.animate-scroll-x { animation: scroll-x 20s linear infinite; }
`}</style>
</div>
</div>
);
})()}
</div>
{templates.length === 0 && !paginationState.loading ? (
<div className="text-center py-12 text-white/60">
<p>No templates found for the current filters.</p>
<Button
onClick={() =>
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: 'all',
search: '',
resetPagination: true,
})
}
className="mt-4 bg-orange-500 hover:bg-orange-400 text-black"
>
Reset Filters
</Button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{templates.map((template) => (
<Card key={template.id} className="group bg-white/5 border-white/10 hover:border-white/20 rounded-xl shadow-sm hover:shadow-md transition-all">
<div className="bg-white/5 px-4 py-4 border-b border-white/10">
<div className="flex items-start justify-between mb-2">
<div className="space-y-2 flex-1 min-w-0">
<CardTitle className="text-xl font-bold text-white group-hover:text-orange-400 transition-colors">
<div className="whitespace-nowrap overflow-hidden text-ellipsis max-w-full break-words hyphens-auto">
{truncate(template.title, TITLE_MAX_CHARS)}
</div>
</CardTitle>
<div className="flex items-center space-x-2">
<Badge className={`${getComplexityColor(template.complexity)} font-medium px-3 py-1 rounded-full`}>{getComplexityLabel(template.complexity)}</Badge>
{template.popularity && (
<div className="flex items-center space-x-1 bg-white/10 px-2 py-1 rounded-full text-white/80">
<Star className="h-4 w-4 text-amber-400 fill-current" />
<span className="text-sm font-semibold">{template.popularity}%</span>
</div>
)}
</div>
</div>
{/* Edit and Delete buttons - only show for database templates */}
{template.id && template.id !== "marketing-website" && template.id !== "saas-platform" && (
<div className="flex items-center space-x-1 ml-2">
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
setEditingTemplate(template as DatabaseTemplate)
}}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10 cursor-pointer"
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
setDeletingTemplate(template as DatabaseTemplate)
}}
className="h-8 w-8 p-0 text-white/60 hover:text-red-400 hover:bg-red-400/10 cursor-pointer"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
{(() => {
const raw = (template.description || '').trim()
const needsToggle = raw.length > DESC_MAX_CHARS
const displayText = truncate(template.description, DESC_MAX_CHARS)
return (
<div>
<p className={`text-white/80 text-sm leading-relaxed break-words hyphens-auto line-clamp-2 overflow-hidden`}>
{displayText}
</p>
{needsToggle && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setDescDialogData({ title: template.title, description: raw })
setDescDialogOpen(true)
}}
className="mt-1 text-xs text-orange-400 hover:text-orange-300 font-medium"
>
Show more
</button>
)}
</div>
)
})()}
</div>
<CardContent className="p-4 flex flex-col h-full text-white/80">
<div className="flex-1 space-y-4">
<div className="flex items-center justify-between text-sm bg-white/5 px-3 py-2 rounded-lg">
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-orange-400" />
<span className="font-medium">{template.timeEstimate}</span>
</div>
<div className="flex items-center space-x-2">
<Layers className="h-4 w-4 text-emerald-400" />
<span className="font-medium">{template.featureCount || 0} features</span>
</div>
</div>
<div className="space-y-2">
<div>
<h4 className="font-semibold text-sm text-white mb-1 flex items-center">
<span className="w-2 h-2 bg-orange-500 rounded-full mr-2"></span>
Key Features
</h4>
<div className="flex flex-wrap gap-1">
{template.features.slice(0, 3).map((feature, index) => (
<Badge key={index} variant="outline" className="text-xs bg-white/5 border-white/10 text-white/80 px-3 py-2 rounded-full">
{feature}
</Badge>
))}
{template.features.length > 3 && (
<Badge variant="outline" className="text-xs bg-white/5 border-white/10 text-white/70 px-3 py-2 rounded-full">
+{template.features.length - 3} more
</Badge>
)}
</div>
</div>
</div>
</div>
<div className="mt-4">
<Button onClick={() => onNext(template)} className="w-full bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">
<span className="flex items-center justify-center cursor-pointer">
Select Template
<ArrowRight className="ml-2 h-4 w-4" />
</span>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
<div className="flex justify-center pt-6">
<div className="flex items-center space-x-2">
<Button
onClick={() => {
if (paginationState.currentPage > 0) {
fetchTemplatesWithPagination({
page: paginationState.currentPage - 1,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}
}}
disabled={paginationState.currentPage === 0 || paginationState.loading}
variant="outline"
size="sm"
className="border-white/20 text-white hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed"
>
</Button>
{(() => {
const totalPages = Math.max(1, Math.ceil(paginationState.total / paginationState.pageSize));
const currentPage = paginationState.currentPage;
const pages = [];
// First page
pages.push(
<Button
key={1}
onClick={() => {
if (currentPage !== 0) {
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}
}}
variant={currentPage === 0 ? "default" : "outline"}
size="sm"
className={currentPage === 0 ? "bg-orange-500 text-black border-orange-500 hover:bg-orange-400" : "border-white/20 text-white hover:bg-white/10"}
>
1
</Button>
);
// Ellipsis before current page
if (currentPage > 3) {
pages.push(<span key="ellipsis1" className="px-2 text-white/40">...</span>);
}
// Pages around current
for (let i = Math.max(1, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) {
if (i > 0 && i < totalPages) {
pages.push(
<Button
key={i + 1}
onClick={() => {
fetchTemplatesWithPagination({
page: i,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}}
variant={currentPage === i ? "default" : "outline"}
size="sm"
className={currentPage === i ? "bg-orange-500 text-black border-orange-500 hover:bg-orange-400" : "border-white/20 text-white hover:bg-white/10"}
>
{i + 1}
</Button>
);
}
}
// Ellipsis after current page
if (currentPage < totalPages - 3) {
pages.push(<span key="ellipsis2" className="px-2 text-white/40">...</span>);
}
// Last page (only if not already rendered by the middle range)
if (totalPages > 1 && currentPage < totalPages - 2) {
pages.push(
<Button
key={totalPages}
onClick={() => {
fetchTemplatesWithPagination({
page: totalPages - 1,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}}
variant={currentPage === totalPages - 1 ? "default" : "outline"}
size="sm"
className={currentPage === totalPages - 1 ? "bg-orange-500 text-black border-orange-500 hover:bg-orange-400" : "border-white/20 text-white hover:bg-white/10"}
>
{totalPages}
</Button>
);
}
return pages;
})()}
<Button
onClick={() => loadMoreTemplates()}
disabled={paginationState.total <= paginationState.pageSize || !paginationState.hasMore || paginationState.loading}
variant="outline"
size="sm"
className="border-white/20 text-white hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed"
>
</Button>
</div>
</div>
<Card className="group border-dashed border-2 border-white/15 bg-white/5 hover:border-white/25 transition-all cursor-pointer" onClick={() => {
if (!user?.id) { window.location.href = '/signin'; return }
setShowCreateOptionDialog(true)
}}>
<CardContent className="text-center py-16 px-8 text-white/80">
<div className="w-20 h-20 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Plus className="h-10 w-10 text-orange-400" />
</div>
<h3 className="text-2xl font-bold text-white mb-3">
{user?.id ? 'Create Custom Template' : 'Sign In to Create Templates'}
</h3>
<p className="mb-8 max-w-md mx-auto text-lg leading-relaxed">
{user?.id
? "Don't worry, we'll guide you through each step. a custom project type with your specific requirements and tech stack."
: "Sign in to create custom project templates with your specific requirements and tech stack."
}
</p>
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={(e) => {
e.stopPropagation()
if (!user?.id) { window.location.href = '/signin'; return }
setShowCreateOptionDialog(true)
}}>
{user?.id ? (
<>
<Plus className="mr-2 h-5 w-5" />
Create Custom Template
</>
) : (
<>
<User className="mr-2 h-5 w-5" />
Sign In to Create
</>
)}
</Button>
</CardContent>
</Card>
<div className="text-center py-4">
<p className="text-white/70">
{searchQuery ? (
<>
Showing {templates.length} template{templates.length !== 1 ? "s" : ""} matching &quot;{searchQuery}&quot;
{paginationState.total > 0 && ` (${paginationState.total} total)`}
</>
) : (
<>
Showing {templates.length} template{templates.length !== 1 ? "s" : ""}
{paginationState.total > 0 && ` of ${paginationState.total} total`}
</>
)}
{paginationState.total > 0 && (
<span className="block text-sm text-white/50 mt-1">
Page {paginationState.currentPage + 1} of {Math.ceil(paginationState.total / paginationState.pageSize)}
</span>
)}
</p>
</div>
<Dialog open={descDialogOpen} onOpenChange={setDescDialogOpen}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">{descDialogData.title}</DialogTitle>
<DialogDescription className="text-white/80 whitespace-pre-wrap">
{descDialogData.description}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
{/* Create Template Options Modal */}
<Dialog open={showCreateOptionDialog} onOpenChange={(open) => {
setShowCreateOptionDialog(open)
if (!open) {
setShowGitForm(false)
setGitProvider('')
setGitUrl('')
setGitBranch('main')
setGitAuthMethod('')
setGitCredentials({ username: '', password: '', token: '', sshKey: '' })
setGitStep('provider')
setAuthUrl('')
}
}}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">Create Template</DialogTitle>
<DialogDescription className="text-white/80">
Choose how you want to create a template.
</DialogDescription>
</DialogHeader>
{!showGitForm ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Button className="bg-orange-500 hover:bg-orange-400 text-black" onClick={() => {
setShowCreateOptionDialog(false)
setShowCustomForm(true)
}}>Create Manually</Button>
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => {
setShowGitForm(true)
setGitStep('provider')
}}>
Import from Git
</Button>
</div>
) : gitStep === 'provider' ? (
<div className="space-y-4">
<div>
<label className="block text-sm text-white/70 mb-3">Select Git Provider</label>
<div className="grid grid-cols-2 gap-3">
{Object.entries(gitProviders).map(([key, provider]) => (
<Button
key={key}
variant="outline"
className="border-white/20 text-white hover:bg-white/10 h-12 flex flex-col items-center gap-1"
onClick={() => {
setGitProvider(key)
setGitStep('url')
}}
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={provider.icon}/>
</svg>
{provider.name}
</Button>
))}
</div>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => { setShowGitForm(false) }}>Back</Button>
</div>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center gap-2 mb-4">
<Button
variant="ghost"
size="sm"
className="text-white/70 hover:text-white p-1"
onClick={() => setGitStep('provider')}
>
Back to Provider
</Button>
<span className="text-sm text-white/70">
Import from {gitProviders[gitProvider as keyof typeof gitProviders]?.name}
</span>
</div>
<div>
<label className="block text-sm text-white/70 mb-1">Repository URL</label>
<Input
value={gitUrl}
onChange={(e) => setGitUrl(e.target.value)}
placeholder={gitProviders[gitProvider as keyof typeof gitProviders]?.placeholder}
className="bg-white/10 border-white/20 text-white"
/>
<p className="text-xs text-white/50 mt-1">
Enter the full URL to your {gitProviders[gitProvider as keyof typeof gitProviders]?.name} repository
</p>
</div>
<div>
<label className="block text-sm text-white/70 mb-1">Branch (optional)</label>
<Input
value={gitBranch}
onChange={(e) => setGitBranch(e.target.value)}
placeholder="main"
className="bg-white/10 border-white/20 text-white"
/>
<p className="text-xs text-white/50 mt-1">
Leave empty to use the default branch
</p>
</div>
{/* Authentication URL Generation: show only if not already connected */}
{gitUrl.trim() && isGithubConnected === false && (
<div className="space-y-3">
<div className="p-4 bg-blue-500/10 border border-blue-500/20 rounded-lg">
<div className="flex items-center gap-2 text-blue-200 text-sm mb-3">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
Ready to authenticate with {gitProviders[gitProvider as keyof typeof gitProviders]?.name}
</div>
<p className="text-xs text-blue-200/80 mb-3">
Click the button below to generate an authentication URL. This will open a new window where you can securely authenticate with your {gitProviders[gitProvider as keyof typeof gitProviders]?.name} account.
</p>
<Button
className="w-full bg-blue-500 hover:bg-blue-400 text-white"
type="button"
onClick={(e) => { e.preventDefault(); generateAuthUrl(); }}
disabled={isGeneratingAuth || !gitUrl.trim()}
>
{isGeneratingAuth ? (
<div className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Generating Auth URL...
</div>
) : (
`Authenticate with ${gitProviders[gitProvider as keyof typeof gitProviders]?.name}`
)}
</Button>
</div>
{authUrl && (
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
<div className="flex items-center gap-2 text-green-200 text-sm mb-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Authentication URL Generated
</div>
<p className="text-xs text-green-200/80 mb-2">
If the authentication window didn't open, you can click the link below:
</p>
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-300 hover:text-blue-200 underline break-all"
>
{authUrl}
</a>
</div>
)}
</div>
)}
<div className="flex gap-3 justify-end pt-2">
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => setGitStep('provider')}>Back</Button>
<Button
className="bg-orange-500 hover:bg-orange-400 text-black"
type="button"
onClick={(e) => { e.preventDefault(); handleCreateFromGit(); }}
disabled={!gitUrl.trim() || (isGithubConnected === false && !authUrl)}
>
Import Template
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}
// Feature Selection Step Component
function FeatureSelectionStep({
template,
onNext,
onBack,
}: { template: Template; onNext: (selected: TemplateFeature[]) => void; onBack: () => void }) {
const { fetchFeatures, createFeature, updateFeature, deleteFeature } = useTemplates()
const [features, setFeatures] = useState<TemplateFeature[]>([])
const [essentialFeatures, setEssentialFeatures] = useState<TemplateFeature[]>([])
const [customFeatures, setCustomFeatures] = useState<TemplateFeature[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [showAIModal, setShowAIModal] = useState(false)
const [expandedFeatureDescriptions, setExpandedFeatureDescriptions] = useState<Record<string, boolean>>({})
const [featureDescDialogOpen, setFeatureDescDialogOpen] = useState(false)
const [featureDescDialogData, setFeatureDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' })
const [rulesDialogOpen, setRulesDialogOpen] = useState(false)
const [rulesForFeature, setRulesForFeature] = useState<{ featureId: string; featureName: string } | null>(null)
const [featureRules, setFeatureRules] = useState<Array<string | { requirement?: string; rules?: string[] }>>([])
const FEATURE_DESC_MAX_CHARS = 180
const truncateFeatureText = (value: string | undefined | null, max: number) => {
const v = (value || '').trim()
if (v.length <= max) return v
return v.slice(0, Math.max(0, max - 1)) + ''
}
const load = async () => {
try {
setLoading(true)
console.log('[FeatureSelectionStep] Loading features for template:', template.id)
console.log('[FeatureSelectionStep] Template object:', template)
const data = await fetchFeatures(template.id)
console.log('[FeatureSelectionStep] Raw features received:', data)
console.log('[FeatureSelectionStep] API endpoint called:', `/api/templates/${template.id}/features`)
console.log('[FeatureSelectionStep] Features by type:', {
essential: data.filter(f => f.feature_type === 'essential').length,
suggested: data.filter(f => f.feature_type === 'suggested').length,
custom: data.filter(f => f.feature_type === 'custom').length,
total: data.length
})
console.log('[FeatureSelectionStep] All features with types:', data.map(f => ({ name: f.name, type: f.feature_type })))
setFeatures(data)
// Separate custom features from essential features
setEssentialFeatures(data.filter(f => f.feature_type !== 'custom'))
setCustomFeatures(data.filter(f => f.feature_type === 'custom'))
} catch (e) {
console.error('[FeatureSelectionStep] Error loading features:', e)
setError(e instanceof Error ? e.message : 'Failed to load features')
} finally {
setLoading(false)
}
}
// Initial load
useEffect(() => { load() }, [template.id])
const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high'; logic_rules?: string[]; requirements?: Array<{ text: string; rules: string[] }>; business_rules?: Array<{ requirement: string; rules: string[] }> }) => {
await createFeature(template.id, {
name: payload.name,
description: payload.description,
feature_type: 'custom',
complexity: payload.complexity,
is_default: false,
created_by_user: true,
// @ts-expect-error backend accepts additional fields
logic_rules: payload.logic_rules,
business_rules: payload.business_rules ?? (payload.requirements ? payload.requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })) : undefined),
})
await load()
}
const [editingFeature, setEditingFeature] = useState<TemplateFeature | null>(null)
const handleUpdate = async (f: TemplateFeature, updates: Partial<TemplateFeature>) => {
// Use the actual id field directly (no need to extract from feature_id)
await updateFeature(f.id, { ...updates, isCustom: f.feature_type === 'custom' })
await load()
}
const handleDelete = async (f: TemplateFeature) => {
// Use the actual id field directly (no need to extract from feature_id)
await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' })
setSelectedIds((prev) => {
const next = new Set(prev)
next.delete(f.id)
return next
})
await load()
}
const toggleSelect = (f: TemplateFeature) => {
setSelectedIds((prev) => {
const next = new Set(prev)
if (next.has(f.id)) next.delete(f.id)
else next.add(f.id)
return next
})
}
const extractRules = (f: TemplateFeature): Array<string | { requirement?: string; rules?: string[] }> => {
// Prefer structured business_rules if available; fall back to additional_business_rules
const candidate = (f as any).business_rules ?? (f as any).additional_business_rules ?? []
if (Array.isArray(candidate)) return candidate
if (candidate && typeof candidate === 'object') return Object.entries(candidate).map(([k, v]) => `${k}: ${v as string}`)
if (typeof candidate === 'string') return [candidate]
return []
}
const section = (title: string, list: TemplateFeature[]) => (
<div>
<h3 className="text-lg font-semibold text-white mb-3">{title} ({list.length})</h3>
<div className={`${list.length > 6 ? 'max-h-[480px] overflow-y-auto pr-2' : ''}`}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{list.map((f) => (
<Card key={f.id} className={`bg-white/5 ${selectedIds.has(f.id) ? 'border-orange-400' : 'border-white/10'}`}>
<CardHeader className="pb-3">
<CardTitle className="text-white flex items-center justify-between">
<div className="flex items-center gap-2">
<Checkbox
checked={selectedIds.has(f.id)}
onCheckedChange={() => toggleSelect(f)}
className="border-white/20 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
/>
<span>{f.name}</span>
</div>
{f.feature_type === 'custom' && (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="border-white/20 text-white hover:bg-white/10"
onClick={() => setEditingFeature(f)}
>
Edit
</Button>
<Button
size="sm"
variant="outline"
className="border-red-500 text-red-300 hover:bg-red-500/10"
onClick={() => handleDelete(f)}
>
Delete
</Button>
</div>
)}
</CardTitle>
</CardHeader>
<CardContent className="text-white/80 text-sm space-y-2">
{(() => {
const raw = (f.description || 'No description provided.').trim()
const needsToggle = raw.length > FEATURE_DESC_MAX_CHARS
const displayText = truncateFeatureText(raw, FEATURE_DESC_MAX_CHARS)
return (
<div>
<p className={`leading-relaxed break-words hyphens-auto line-clamp-2 overflow-hidden`}>{displayText}</p>
{needsToggle && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setFeatureDescDialogData({ title: f.name, description: raw })
setFeatureDescDialogOpen(true)
}}
className="mt-1 text-xs text-orange-400 hover:text-orange-300 font-medium"
>
Show more
</button>
)}
</div>
)
})()}
<div className="flex gap-2 text-xs">
<Badge variant="outline" className="bg-white/5 border-white/10">{f.feature_type}</Badge>
<Badge variant="outline" className="bg-white/5 border-white/10">{f.complexity}</Badge>
{typeof f.usage_count === 'number' && <Badge variant="outline" className="bg-white/5 border-white/10">used {f.usage_count}</Badge>}
<Button
size="sm"
variant="outline"
className="ml-auto h-6 px-2 border-orange-500 text-orange-300 hover:bg-orange-500/10"
onClick={async (e) => {
e.stopPropagation()
setRulesForFeature({ featureId: f.id, featureName: f.name })
setFeatureRules(extractRules(f))
setRulesDialogOpen(true)
}}
>
Rules
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</div>
)
if (loading) {
return <div className="text-center py-20 text-white/60">Loading features...</div>
}
if (error) {
return <div className="text-center py-20 text-red-400">{error}</div>
}
const essentials = features.filter(f => f.feature_type === 'essential')
const suggested = features.filter(f => f.feature_type === 'suggested')
const custom = features.filter(f => f.feature_type === 'custom')
return (
<div className="max-w-7xl mx-auto space-y-8">
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold text-white">Select Features for {template.title}</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">Choose from essential and suggested features.</p>
</div>
{essentialFeatures.length > 0 && section('Essential Features', essentialFeatures)}
{/* Add custom feature with AI */}
<div className="bg-white/5 border border-white/10 rounded-xl p-6 space-y-4">
<div className="text-center space-y-2">
<h3 className="text-white font-semibold text-lg">Add Custom Feature</h3>
<p className="text-white/60 text-sm">Use AI to analyze and create custom features for your project</p>
</div>
<div className="flex justify-center">
<Button
onClick={() => setShowAIModal(true)}
className="bg-orange-500 hover:bg-orange-400 text-black font-semibold px-8 py-3 rounded-lg cursor-pointer"
>
<Zap className="mr-2 h-5 w-5" />
Analyze with AI
</Button>
</div>
<div className="text-center text-white/60 text-sm">
AI will analyze your requirements and create optimized features
</div>
</div>
{customFeatures.length > 0 && section('Custom Features', customFeatures)}
{(showAIModal || editingFeature) && (
<AICustomFeatureCreator
projectType={template.type || template.title}
onAdd={async (f) => {
if (editingFeature) {
// Update existing feature
await handleUpdate(editingFeature, f)
setEditingFeature(null)
} else {
// Add new feature
await handleAddAIAnalyzed(f)
setShowAIModal(false)
}
}}
onClose={() => {
setShowAIModal(false)
setEditingFeature(null)
}}
editingFeature={editingFeature || undefined}
/>
)}
<div className="text-center py-4">
<div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10 cursor-pointer">Back</Button>
<Button
onClick={() => onNext([...essentialFeatures, ...customFeatures].filter(f => selectedIds.has(f.id)))}
disabled={selectedIds.size < 3}
className={`bg-orange-500 hover:bg-orange-400 text-black cursor-pointer font-semibold py-2 rounded-lg shadow ${selectedIds.size < 3 ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Continue
</Button>
</div>
<div className="text-white/60 text-sm mt-2">Select at least 3 features to continue. Selected {selectedIds.size}/3.</div>
</div>
<Dialog open={featureDescDialogOpen} onOpenChange={setFeatureDescDialogOpen}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">{featureDescDialogData.title}</DialogTitle>
<DialogDescription className="text-white/80 whitespace-pre-wrap">
{featureDescDialogData.description}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
<Dialog open={rulesDialogOpen} onOpenChange={setRulesDialogOpen}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">{rulesForFeature?.featureName || 'Feature Rules'}</DialogTitle>
<DialogDescription className="text-white/80">
{featureRules.length === 0 ? (
<div className="py-2">No rules found for this feature.</div>
) : (
<ul className="list-disc pl-5 space-y-2">
{featureRules.map((r, idx) => {
if (typeof r === 'string') return <li key={idx} className="text-sm leading-relaxed">{r}</li>
const req = (r as any).requirement
const rules = (r as any).rules
return (
<li key={idx} className="text-sm leading-relaxed">
{req ? <div className="font-medium text-white/90">{req}</div> : null}
{Array.isArray(rules) && rules.length > 0 && (
<ul className="list-disc pl-5 mt-1 space-y-1">
{rules.map((rr: string, j: number) => (
<li key={j} className="text-xs text-white/80">{rr}</li>
))}
</ul>
)}
</li>
)
})}
</ul>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
)
}
// Business Questions Step Component
function BusinessQuestionsStep({
template,
selected,
onBack,
onDone,
}: { template: Template; selected: TemplateFeature[]; onBack: () => void; onDone: (completeData: any, recommendations: any) => void }) {
const [businessQuestions, setBusinessQuestions] = useState<string[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showSummary, setShowSummary] = useState(false)
const [answers, setAnswers] = useState<any[]>([])
const [submitting, setSubmitting] = useState(false)
const [answeredQuestionsCount, setAnsweredQuestionsCount] = useState(0)
const selectedKey = selected.map(s => s.id).join(',')
useEffect(() => {
const load = async () => {
try {
setLoading(true)
setError(null)
if (selected.length === 0) {
setError('No features selected')
return
}
const token = getAccessToken()
const resp = await fetch(`${BACKEND_URL}/api/questions/generate-comprehensive-business-questions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: JSON.stringify({
allFeatures: selected,
projectName: template.title,
projectType: template.type || template.category,
totalFeatures: selected.length,
}),
})
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json()
const qs: string[] = data?.data?.businessQuestions || []
setBusinessQuestions(qs)
} catch (e: any) {
setError(e?.message || 'Failed to load questions')
} finally {
setLoading(false)
}
}
load()
}, [template.id, selectedKey])
const handleSurveyComplete = (surveyAnswers: any[]) => {
setAnswers(surveyAnswers)
setAnsweredQuestionsCount(surveyAnswers.length)
setShowSummary(true)
}
const handleSurveyProgress = (currentAnswers: any[]) => {
const answeredCount = currentAnswers.filter(answer => answer.answer && answer.answer.trim().length > 0).length
setAnsweredQuestionsCount(answeredCount)
// Store the current answers so they're available for submission
setAnswers(currentAnswers)
}
const handleSubmit = async () => {
try {
setSubmitting(true)
setError(null)
const token = getAccessToken()
// Filter to only include answered questions
const answeredQuestions = answers.filter(answer => answer.answer && answer.answer.trim().length > 0)
const questionsWithAnswers = answeredQuestions.map(answer => ({
question: answer.question,
answer: answer.answer,
}))
const completeData = {
template,
features: selected,
businessContext: {
questions: questionsWithAnswers,
},
projectName: template.title,
projectType: template.type || template.category,
}
console.log('🚀 Submitting comprehensive recommendations request...')
console.log('📊 Total answers received:', answers.length)
console.log('📊 Answered questions:', answeredQuestions.length)
console.log('📊 Questions with answers:', questionsWithAnswers)
console.log('📊 Complete data:', completeData)
const response = await fetch(`${BACKEND_URL}/api/unified/comprehensive-recommendations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({
template,
features: selected,
businessContext: {
questions: questionsWithAnswers,
},
projectName: template.title,
projectType: template.type || template.category,
templateId: template.id,
budget: 15000, // Default budget - could be made configurable
domain: template.category?.toLowerCase() || 'general',
includeClaude: true,
includeTemplateBased: true,
includeDomainBased: true,
}),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const recommendations = await response.json()
console.log('✅ Comprehensive recommendations received:', recommendations)
// Extract the tech recommendations from the response for the frontend component
const techRecommendations = recommendations?.data?.claude?.data?.claude_recommendations || null
console.log('🎯 Extracted tech recommendations:', techRecommendations)
// Proceed to next step with complete data and recommendations
onDone(completeData, techRecommendations)
} catch (error: any) {
console.error('❌ Error submitting business context:', error)
setError(error?.message || 'Failed to submit business context')
} finally {
setSubmitting(false)
}
}
if (loading) {
return (
<div className="flex items-center justify-center py-24">
<div className="text-center text-white/80">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500 mx-auto mb-4"></div>
<p>AI is generating comprehensive business questions...</p>
<p className="text-white/50 text-sm mt-2">Analyzing {selected.length} features as integrated system</p>
</div>
</div>
)
}
if (error) {
return (
<div className="text-center py-20">
<div className="max-w-md mx-auto bg-red-500/10 border border-red-500/30 rounded-lg p-6 text-red-300">
<div className="font-semibold mb-2">Error Loading Questions</div>
<div className="mb-4">{error}</div>
<div className="space-x-3">
<Button onClick={() => location.reload()} className="bg-red-500 hover:bg-red-400 text-black">Try Again</Button>
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Go Back</Button>
</div>
</div>
</div>
)
}
if (showSummary) {
return (
<SurveySummary
questions={answers}
onBack={() => setShowSummary(false)}
onSubmit={handleSubmit}
loading={submitting}
/>
)
}
return (
<div className="max-w-6xl mx-auto space-y-4">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold text-white">Business Context Questions</h1>
<p className="text-lg text-white/60 max-w-2xl mx-auto">
Help us understand your {template.title} project better with these AI-generated questions
</p>
</div>
<TypeformSurvey
questions={businessQuestions}
onComplete={handleSurveyComplete}
onProgress={handleSurveyProgress}
onBack={onBack}
projectName={template.title}
/>
{/* Navigation buttons */}
<div className="text-center py-4">
<div className="space-x-4">
<Button
variant="outline"
onClick={onBack}
className="border-white/20 text-white hover:bg-white/10 cursor-pointer"
>
Back to Features
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || answeredQuestionsCount < 3}
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow cursor-pointer ${
submitting || answeredQuestionsCount < 3 ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{submitting ? (
<div className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-black"></div>
Generating Tech Stack...
</div>
) : (
<>
Generate Tech Stack
</>
)}
</Button>
</div>
<div className="text-white/60 text-sm mt-2">
{answeredQuestionsCount < 3
? `Answer at least 3 questions to generate your tech stack recommendations (${answeredQuestionsCount}/3 answered)`
: `Ready to generate tech stack for ${template.title} (${answeredQuestionsCount} questions answered)`
}
</div>
</div>
</div>
)
}
// AI Mockup Step Component
// function AIMockupStep({
// template,
// selectedFeatures,
// onNext,
// onBack,
// }: {
// template: Template
// selectedFeatures: TemplateFeature[]
// onNext: () => void
// onBack: () => void
// }) {
// const [mounted, setMounted] = useState(false)
// const [wireframeData, setWireframeData] = useState<any>(null)
// const [isGenerating, setIsGenerating] = useState(false)
// const [selectedDevice, setSelectedDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop')
// const [initialPrompt, setInitialPrompt] = useState<string>("")
// // Load state from localStorage after component mounts
// useEffect(() => {
// setMounted(true)
// if (typeof window !== 'undefined') {
// // Load device type
// const savedDevice = localStorage.getItem('wireframe_device_type')
// if (savedDevice && ['desktop', 'tablet', 'mobile'].includes(savedDevice)) {
// setSelectedDevice(savedDevice as 'desktop' | 'tablet' | 'mobile')
// }
// // Load wireframe data
// const savedWireframeData = localStorage.getItem('wireframe_data')
// if (savedWireframeData) {
// try {
// const parsed = JSON.parse(savedWireframeData)
// setWireframeData(parsed)
// } catch (error) {
// console.error('Failed to parse saved wireframe data:', error)
// }
// }
// }
// }, [])
// // Build prompt from selected features and template
// useEffect(() => {
// if (!template) return
// const featureNames = (selectedFeatures || []).map(f => f.name).filter(Boolean)
// const base = `${template.title} dashboard`
// const parts = featureNames.length > 0 ? ` with ${featureNames.join(', ')}` : ''
// const footer = '. Optimize layout and spacing. Include navigation.'
// const prompt = `${base}${parts}${footer}`
// setInitialPrompt(prompt)
// }, [template, JSON.stringify(selectedFeatures)])
// const handleWireframeGenerated = (data: any) => {
// setWireframeData(data)
// setIsGenerating(false)
// }
// const handleWireframeGenerationStart = () => {
// setIsGenerating(true)
// }
// const handleDeviceChange = (device: 'desktop' | 'tablet' | 'mobile') => {
// console.log('DEBUG: AIMockupStep handleDeviceChange called with:', device)
// console.log('DEBUG: Previous selectedDevice state:', selectedDevice)
// setSelectedDevice(device)
// // Save to localStorage (only after mounting)
// if (mounted) {
// localStorage.setItem('wireframe_device_type', device)
// console.log('DEBUG: Saved device type to localStorage:', device)
// }
// console.log('DEBUG: New selectedDevice state:', device)
// }
// // Save wireframe data to localStorage when it changes (only after mounting)
// useEffect(() => {
// if (wireframeData && mounted) {
// localStorage.setItem('wireframe_data', JSON.stringify(wireframeData))
// }
// }, [wireframeData, mounted])
// // Debug: Log when selectedDevice prop changes
// useEffect(() => {
// console.log('DEBUG: AIMockupStep selectedDevice state changed to:', selectedDevice)
// }, [selectedDevice])
// // Debug: Log when selectedDevice prop changes
// useEffect(() => {
// console.log('DEBUG: WireframeCanvas selectedDevice prop changed to:', selectedDevice)
// }, [selectedDevice])
// return (
// <div className="max-w-7xl mx-auto space-y-6">
// <div className="text-center space-y-4">
// <h1 className="text-4xl font-bold text-white">AI Wireframe Mockup</h1>
// <p className="text-xl text-white/60 max-w-3xl mx-auto">
// Generate and customize wireframes for {template.title} using AI-powered design tools
// </p>
// <div className="flex items-center justify-center gap-2 text-orange-400">
// <Palette className="h-5 w-5" />
// <span className="text-sm font-medium">AI-Powered Wireframe Generation</span>
// </div>
// </div>
// {/* Dual Canvas Editor */}
// <div className="bg-white/5 border border-white/10 rounded-xl overflow-hidden h-[80vh] min-h-[600px]">
// <DualCanvasEditor
// className="h-full w-full"
// onWireframeGenerated={handleWireframeGenerated}
// onGenerationStart={handleWireframeGenerationStart}
// selectedDevice={selectedDevice}
// onDeviceChange={handleDeviceChange}
// initialPrompt={initialPrompt}
// />
// </div>
// {/* Wireframe Status and Controls */}
// <div className="bg-white/5 border border-white/10 rounded-xl p-6">
// <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
// <div className="text-center">
// <div className="text-2xl font-bold text-white mb-2">
// {wireframeData ? '✅' : '⏳'}
// </div>
// <div className="text-white/80 font-medium">
// {wireframeData ? 'Wireframe Generated' : 'Ready to Generate'}
// </div>
// <div className="text-white/60 text-sm">
// {wireframeData ? 'Click Continue to proceed' : 'Use the AI panel to create wireframes'}
// </div>
// </div>
// <div className="text-center">
// <div className="text-2xl font-bold text-white mb-2">
// {selectedFeatures.length}
// </div>
// <div className="text-white/80 font-medium">Features Selected</div>
// <div className="text-white/60 text-sm">
// {template.title} template
// </div>
// </div>
// <div className="text-center">
// <div className="text-2xl font-bold text-white mb-2">
// {isGenerating ? '🔄' : '🎨'}
// </div>
// <div className="text-white/80 font-medium">
// {isGenerating ? 'Generating...' : 'AI Ready'}
// </div>
// <div className="text-white/60 text-sm">
// {isGenerating ? 'Creating wireframe layout' : 'Claude AI powered'}
// </div>
// </div>
// </div>
// </div>
// {/* Navigation */}
// <div className="text-center py-6">
// <div className="space-x-4">
// <Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">
// ← Back to Features
// </Button>
// <Button
// onClick={onNext}
// disabled={!wireframeData}
// className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow ${!wireframeData ? 'opacity-50 cursor-not-allowed' : ''
// }`}
// >
// Continue to Business Context →
// </Button>
// </div>
// <div className="text-white/60 text-sm mt-2">
// {wireframeData
// ? 'Wireframe generated successfully! Continue to define business requirements.'
// : 'Generate a wireframe first to continue to the next step.'
// }
// </div>
// </div>
// </div>
// )
// }
// Main Dashboard Component
export function MainDashboard() {
const [mounted, setMounted] = useState(false)
const [currentStep, setCurrentStep] = useState(1)
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return null
})
const [selectedFeatures, setSelectedFeatures] = useState<TemplateFeature[]>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return []
})
const [finalProjectData, setFinalProjectData] = useState<any>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return null
})
const [techStackRecommendations, setTechStackRecommendations] = useState<any>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return null
})
// Load state from localStorage after component mounts
useEffect(() => {
setMounted(true)
if (typeof window !== 'undefined') {
// Load current step
const savedStep = localStorage.getItem('dashboard_current_step')
if (savedStep) {
setCurrentStep(parseInt(savedStep) || 1)
}
// Load selected template
const savedTemplate = localStorage.getItem('dashboard_selected_template')
if (savedTemplate) {
try {
setSelectedTemplate(JSON.parse(savedTemplate))
} catch (error) {
console.error('Failed to parse saved template:', error)
}
}
// Load selected features
const savedFeatures = localStorage.getItem('dashboard_selected_features')
if (savedFeatures) {
try {
setSelectedFeatures(JSON.parse(savedFeatures))
} catch (error) {
console.error('Failed to parse saved features:', error)
}
}
// Load final project data
const savedProjectData = localStorage.getItem('dashboard_final_project_data')
if (savedProjectData) {
try {
setFinalProjectData(JSON.parse(savedProjectData))
} catch (error) {
console.error('Failed to parse saved project data:', error)
}
}
// Load tech stack recommendations
const savedRecommendations = localStorage.getItem('dashboard_tech_stack_recommendations')
if (savedRecommendations) {
try {
setTechStackRecommendations(JSON.parse(savedRecommendations))
} catch (error) {
console.error('Failed to parse saved recommendations:', error)
}
}
}
}, [mounted])
const steps = [
{ id: 1, name: "Project Type", description: "Choose template" },
{ id: 2, name: "Features", description: "Select features" },
// { id: 3, name: "AI Mockup", description: "Generate wireframes" },
{ id: 3, name: "Business Context", description: "Define requirements" },
{ id: 4, name: "Generate", description: "Create project" },
{ id: 5, name: "Architecture", description: "Review & deploy" },
]
// Save state to localStorage when it changes (only after mounting)
useEffect(() => {
if (mounted) {
localStorage.setItem('dashboard_current_step', currentStep.toString())
}
}, [currentStep, mounted])
useEffect(() => {
if (mounted) {
if (selectedTemplate) {
localStorage.setItem('dashboard_selected_template', JSON.stringify(selectedTemplate))
} else {
localStorage.removeItem('dashboard_selected_template')
}
}
}, [selectedTemplate, mounted])
useEffect(() => {
if (mounted) {
if (selectedFeatures.length > 0) {
localStorage.setItem('dashboard_selected_features', JSON.stringify(selectedFeatures))
} else {
localStorage.removeItem('dashboard_selected_features')
}
}
}, [selectedFeatures, mounted])
useEffect(() => {
if (mounted) {
if (finalProjectData) {
localStorage.setItem('dashboard_final_project_data', JSON.stringify(finalProjectData))
} else {
localStorage.removeItem('dashboard_final_project_data')
}
}
}, [finalProjectData, mounted])
useEffect(() => {
if (mounted) {
if (techStackRecommendations) {
localStorage.setItem('dashboard_tech_stack_recommendations', JSON.stringify(techStackRecommendations))
} else {
localStorage.removeItem('dashboard_tech_stack_recommendations')
}
}
}, [techStackRecommendations, mounted])
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<TemplateSelectionStep
onNext={(template) => {
setSelectedTemplate(template)
setCurrentStep(2)
}}
/>
)
case 2:
return selectedTemplate ? (
<FeatureSelectionStep
template={selectedTemplate}
onNext={(sel) => { setSelectedFeatures(sel); setCurrentStep(3) }}
onBack={() => setCurrentStep(1)}
/>
) : null
case 3:
return selectedTemplate ? (
<BusinessQuestionsStep
template={selectedTemplate}
selected={selectedFeatures}
onBack={() => setCurrentStep(2)}
onDone={(data, recs) => { setFinalProjectData(data); setTechStackRecommendations(recs); setCurrentStep(4) }}
/>
) : null
case 4:
console.log('🔍 MainDashboard - techStackRecommendations:', techStackRecommendations)
console.log('🔍 MainDashboard - finalProjectData:', finalProjectData)
return (
<TechStackSummary
recommendations={techStackRecommendations}
completeData={finalProjectData}
onBack={() => setCurrentStep(3)}
onGenerate={(userTechStack) => {
// Store user's tech stack if provided
if (userTechStack) {
console.log('Storing user tech stack:', userTechStack)
// You can store this in state or pass it to the next step
}
setCurrentStep(5)
}}
/>
)
case 5:
return (
<ArchitectureDesignerStep
recommendations={techStackRecommendations}
onBack={() => setCurrentStep(4)}
/>
)
default:
return null
}
}
return (
<div className="min-h-screen bg-black">
{/* Progress Steps */}
{mounted && (
<div className="bg-white/5 border-b border-white/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="py-4">
<nav className="flex justify-center">
<ol className="flex items-center space-x-8 text-white/60">
{steps.map((step, index) => (
<li key={`step-${step.id}`} className="flex items-center">
<div className="flex items-center">
<div
className={`flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all ${currentStep >= step.id
? "bg-orange-500 border-orange-500 text-white shadow-lg"
: currentStep === step.id - 1
? "border-orange-300 text-orange-400"
: "border-white/30 text-white/40"
}`}
>
<span className="text-sm font-semibold">{step.id}</span>
</div>
<div className="ml-4">
<p
className={`text-sm font-semibold ${currentStep >= step.id ? "text-orange-400" : "text-white/60"
}`}
>
{step.name}
</p>
<p className="text-xs text-white/40">{step.description}</p>
</div>
</div>
{index < steps.length - 1 && (
<ArrowRight
key={`arrow-${step.id}`}
className={`ml-8 h-5 w-5 ${currentStep > step.id ? "text-orange-400" : "text-white/40"}`}
/>
)}
</li>
))}
</ol>
</nav>
</div>
</div>
</div>
)}
{/* Main Content */}
<main className="py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{!mounted ? (
<div className="flex items-center justify-center py-12">
<div className="text-center space-y-4">
<div className="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto"></div>
<p className="text-white/60">Loading...</p>
</div>
</div>
) : (
renderStep()
)}
</div>
</main>
</div>
)
}
function ArchitectureDesignerStep({ recommendations, onBack }: { recommendations: any; onBack: () => void }) {
const [architectureDesign, setArchitectureDesign] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'overview' | 'frontend' | 'backend' | 'database' | 'deployment'>('overview')
useEffect(() => {
const generate = async () => {
if (!recommendations) {
setError('Missing technology recommendations')
return
}
try {
setLoading(true)
setError(null)
const resp = await fetch(`${BACKEND_URL}/api/v1/design-architecture`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tech_stack_recommendations: recommendations }),
})
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json()
setArchitectureDesign(data)
} catch (e: any) {
setError(e?.message || 'Failed to generate architecture')
} finally {
setLoading(false)
}
}
generate()
}, [JSON.stringify(recommendations)])
if (loading) {
return (
<div className="flex items-center justify-center py-24">
<div className="text-center text-white/80">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500 mx-auto mb-4"></div>
<div className="font-semibold mb-1">AI Designing Your Architecture</div>
<div className="text-white/60 text-sm">Creating React components, Node.js APIs, and PostgreSQL schema</div>
</div>
</div>
)
}
if (error) {
return (
<div className="text-center py-20">
<div className="max-w-md mx-auto bg-red-500/10 border border-red-500/30 rounded-lg p-6 text-red-300">
<div className="font-semibold mb-2">Architecture Generation Failed</div>
<div className="mb-4">{error}</div>
<div className="space-x-3">
<Button onClick={() => location.reload()} className="bg-red-500 hover:bg-red-400 text-black">Try Again</Button>
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
</div>
</div>
</div>
)
}
if (!architectureDesign) {
return (
<div className="text-center py-20">
<div className="max-w-md mx-auto bg-white/5 border border-white/10 rounded-lg p-6 text-white/80">
<div className="font-semibold mb-2">No Architecture Data</div>
<div className="mb-4">Please complete the tech stack selection first.</div>
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Go Back</Button>
</div>
</div>
)
}
const projectMeta = architectureDesign?.project_metadata
const arch = architectureDesign?.architecture_design
const tech = architectureDesign?.technology_specifications
return (
<div className="space-y-6">
<div className="bg-white/5 border-b border-white/10">
<div className="max-w-7xl mx-auto px-4 py-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white">Architecture Design</h1>
<p className="text-white/60">AI-generated architecture for <span className="text-orange-400 font-semibold">{projectMeta?.project_name}</span></p>
</div>
<div className="space-x-3">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
<Button onClick={() => location.reload()} className="bg-orange-500 hover:bg-orange-400 text-black">Regenerate</Button>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4">
<div className="border-b border-white/10 mb-6">
<nav className="-mb-px flex space-x-8">
{[
{ id: 'overview', name: 'Overview' },
{ id: 'frontend', name: 'Frontend (React)' },
{ id: 'backend', name: 'Backend (Node.js)' },
{ id: 'database', name: 'Database (PostgreSQL)' },
{ id: 'deployment', name: 'Deployment' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`py-2 px-1 border-b-2 text-sm transition-colors ${activeTab === (tab.id as any)
? 'border-orange-400 text-orange-400'
: 'border-transparent text-white/60 hover:text-white/80 hover:border-white/20'
}`}
>
{tab.name}
</button>
))}
</nav>
</div>
{activeTab === 'overview' && (
<div className="space-y-6">
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Project Overview</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-white/80">
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Project Name</div>
<div>{projectMeta?.project_name}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Complexity</div>
<div className="capitalize">{projectMeta?.complexity}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Generated</div>
<div>{projectMeta?.architecture_generated_at ? new Date(projectMeta.architecture_generated_at).toLocaleDateString() : '-'}</div>
</div>
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Technology Stack</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-white/80">
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Frontend</div>
<div>{tech?.frontend_framework}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Backend</div>
<div>{tech?.backend_language}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Database</div>
<div>{tech?.database_system}</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'frontend' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">React Architecture</h3>
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.frontend_architecture, null, 2)}</pre>
</div>
)}
{activeTab === 'backend' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Node.js Architecture</h3>
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.backend_architecture, null, 2)}</pre>
</div>
)}
{activeTab === 'database' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">PostgreSQL Architecture</h3>
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.database_architecture, null, 2)}</pre>
</div>
)}
{activeTab === 'deployment' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6 text-white/80">
<h3 className="text-xl font-bold text-white mb-4">Deployment Configuration</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold mb-2">Frontend Deployment</div>
<ul className="text-sm space-y-1">
<li> Vercel/Netlify hosting</li>
<li> React build optimization</li>
<li> CDN distribution</li>
</ul>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold mb-2">Backend Deployment</div>
<ul className="text-sm space-y-1">
<li> Docker containerization</li>
<li> AWS/GCP hosting</li>
<li> Auto-scaling setup</li>
</ul>
</div>
</div>
</div>
)}
</div>
</div>
)
}