+ {/* Sync Progress Notification */}
+ {syncProgress?.show && (
+
+
+
+
+
+ Syncing {syncProgress.provider.toUpperCase()} Repository
+
+
+ {syncProgress.stage}
+
+
+ Repository: {syncProgress.repositoryUrl}
+ {syncProgress.branchName !== 'main' && ` | Branch: ${syncProgress.branchName}`}
+
+
+
+
+ {syncProgress.status}
+
+
setSyncProgress(null)}
+ className="text-white/60 hover:text-white/80 transition-colors"
+ title="Dismiss sync progress"
+ >
+ β
+
+
+
+
+ )}
+
+ {/* Header */}
+
+
Choose Your Project Template
+
+ Select from our comprehensive library of professionally designed templates
+
+
+ {/* Connection Error Banner */}
+ {connectionError && (
+
+ {
+ 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.')
+ }
+ }
+ }}
+ />
+
+ )}
+ {!templateUser?.id && (
+
+
+ You're currently viewing public templates.
+ {
+ 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
+
+ {' '}to access your personal templates and create custom ones.
+
+
+ )}
+
+
+ {/* Right-aligned quick navigation to user repos */}
+
+
+
+
+
+
+
+
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 && (
+
+ )}
+
+
+ {(() => {
+ 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 (
+
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"
+ }`}
+ >
+
+
+
+
+
{category.name}
+
+ {`${displayCount} templates`}
+
+
+
+ );
+ };
+
+ const allChip = categories.find((c) => c.id === 'all');
+ const rest = categories.filter((c) => c.id !== 'all');
+
+ return (
+
+ {allChip && (
+
+ {renderChip(allChip)}
+
+ )}
+
+
+ {rest.map(renderChip)}
+ {rest.map((c) => ({ ...c, id: `${c.id}-dup` })).map(renderChip)}
+
+
+
+
+ );
+ })()}
+
+
+ {templates.length === 0 && !paginationState.loading ? (
+
+
No templates found for the current filters.
+
+ 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
+
+
+ ) : (
+
+ {templates.map((template) => (
+
+
+
+
+
+
+ {truncate(template.title, TITLE_MAX_CHARS)}
+
+
+
+
{getComplexityLabel(template.complexity)}
+ {template.popularity && (
+
+
+ {template.popularity}%
+
+ )}
+
+
+ {/* Edit and Delete buttons - only show for database templates */}
+ {template.id && template.id !== "marketing-website" && template.id !== "saas-platform" && (
+
+ {
+ 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"
+ >
+
+
+ {
+ 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"
+ >
+
+
+
+ )}
+
+ {(() => {
+ const raw = (template.description || '').trim()
+ const needsToggle = raw.length > DESC_MAX_CHARS
+ const displayText = truncate(template.description, DESC_MAX_CHARS)
+ return (
+
+
+ {displayText}
+
+ {needsToggle && (
+
{
+ 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
+
+ )}
+
+ )
+ })()}
+
+
+
+
+
+
+
+
+ {template.timeEstimate}
+
+
+
+ {template.featureCount || 0} features
+
+
+
+
+
+
+
+ Key Features
+
+
+ {template.features.slice(0, 3).map((feature, index) => (
+
+ {feature}
+
+ ))}
+ {template.features.length > 3 && (
+
+ +{template.features.length - 3} more
+
+ )}
+
+
+
+
+
+
+
onNext(template)} className="w-full bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">
+
+ Select Template
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ {
+ 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"
+ >
+ βΉ
+
+
+ {(() => {
+ const totalPages = Math.max(1, Math.ceil(paginationState.total / paginationState.pageSize));
+ const currentPage = paginationState.currentPage;
+ const pages = [];
+
+ // First page
+ pages.push(
+ {
+ 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
+
+ );
+
+ // Ellipsis before current page
+ if (currentPage > 3) {
+ pages.push(... );
+ }
+
+ // 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(
+ {
+ 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}
+
+ );
+ }
+ }
+
+ // Ellipsis after current page
+ if (currentPage < totalPages - 3) {
+ pages.push(... );
+ }
+
+ // Last page (only if not already rendered by the middle range)
+ if (totalPages > 1 && currentPage < totalPages - 2) {
+ pages.push(
+ {
+ 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}
+
+ );
+ }
+
+ return pages;
+ })()}
+
+ 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"
+ >
+ βΊ
+
+
+
+
+
{
+ if (!templateUser?.id) { window.location.href = '/signin'; return }
+ setShowCreateOptionDialog(true)
+ }}>
+
+
+
+ {templateUser?.id ? 'Create Custom Template' : 'Sign In to Create Templates'}
+
+
+ {templateUser?.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."
+ }
+
+ {
+ e.stopPropagation()
+ if (!templateUser?.id) { window.location.href = '/signin'; return }
+ setShowCreateOptionDialog(true)
+ }}>
+ {templateUser?.id ? (
+ <>
+
+ Create Custom Template
+ >
+ ) : (
+ <>
+
+ Sign In to Create
+ >
+ )}
+
+
+
+
+
+
+ {searchQuery ? (
+ <>
+ Showing {templates.length} template{templates.length !== 1 ? "s" : ""} matching "{searchQuery}"
+ {paginationState.total > 0 && ` (${paginationState.total} total)`}
+ >
+ ) : (
+ <>
+ Showing {templates.length} template{templates.length !== 1 ? "s" : ""}
+ {paginationState.total > 0 && ` of ${paginationState.total} total`}
+ >
+ )}
+ {paginationState.total > 0 && (
+
+ Page {paginationState.currentPage + 1} of {Math.ceil(paginationState.total / paginationState.pageSize)}
+
+ )}
+
+
+
+
+
+ {descDialogData.title}
+
+ {descDialogData.description}
+
+
+
+
+
+ {/* Create Template Options Modal */}
+
{
+ setShowCreateOptionDialog(open)
+ if (!open) {
+ setShowGitForm(false)
+ setGitProvider('')
+ setGitUrl('')
+ setGitBranch('main')
+ setGitAuthMethod('')
+ setGitCredentials({ username: '', password: '', token: '', sshKey: '' })
+ setGitStep('provider')
+ }
+ }}>
+
+
+ Create Template
+
+ Choose how you want to create a template.
+
+
+ {!showGitForm ? (
+
+ {
+ setShowCreateOptionDialog(false)
+ setShowCustomForm(true)
+ }}>Create Manually
+ {
+ setShowGitForm(true)
+ setGitStep('provider')
+ }}>
+ Import from Git
+
+
+ ) : gitStep === 'provider' ? (
+
+
+
Select Git Provider
+
+ {Object.entries(gitProviders).map(([key, provider]) => (
+
{
+ setGitProvider(key)
+ setGitStep('url')
+ }}
+ >
+
+
+
+ {provider.name}
+
+ ))}
+
+
+
+ { setShowGitForm(false) }}>Back
+
+
+ ) : (
+
+
+ setGitStep('provider')}
+ >
+ β Back to Provider
+
+
+ Import from {gitProviders[gitProvider as keyof typeof gitProviders]?.name}
+
+
+
+
+
Repository URL
+
{
+ setGitUrl(e.target.value)
+ }}
+ placeholder={gitProviders[gitProvider as keyof typeof gitProviders]?.placeholder}
+ className="bg-white/10 border-white/20 text-white"
+ />
+
+ Enter the full URL to your {gitProviders[gitProvider as keyof typeof gitProviders]?.name} repository
+
+
+
+
+
Branch (optional)
+
setGitBranch(e.target.value)}
+ placeholder="main"
+ className="bg-white/10 border-white/20 text-white"
+ />
+
+ Leave empty to use the default branch
+
+
+
+
+
+ setGitStep('provider')}>Back
+ { e.preventDefault(); handleCreateFromGit(); }}
+ disabled={!gitUrl.trim()}
+ >
+ Import Template
+
+
+
+ )}
+
+
+
+ )
+}
+
+// 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