Merge branch 'live-working'

This commit is contained in:
Chandini 2025-09-10 11:20:20 +05:30
commit c007095a05
5 changed files with 717 additions and 49 deletions

221
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,221 @@
pipeline {
agent any
environment {
SSH_CREDENTIALS = 'cloudtopiaa'
REMOTE_SERVER = 'ubuntu@160.187.166.39'
REMOTE_WORKSPACE = '/home/ubuntu'
PROJECT_NAME = 'codenuk-frontend-live'
DEPLOY_PATH = '/var/www/html/codenuk-frontend-live'
GIT_CREDENTIALS = 'git-cred'
REPO_URL = 'https://git.tech4biz.wiki/Tech4Biz-Services/codenuk-frontend-live.git'
NPM_PATH = '/home/ubuntu/.nvm/versions/node/v22.18.0/bin/npm'
NODE_PATH = '/home/ubuntu/.nvm/versions/node/v22.18.0/bin/node'
EMAIL_RECIPIENT = 'jassim.mohammed@tech4biz.io, chandini.pachigunta@tech4biz.org'
}
options {
timeout(time: 30, unit: 'MINUTES')
retry(2)
timestamps()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Preparation') {
steps {
script {
echo "Starting ${PROJECT_NAME} deployment pipeline"
echo "Server: ${REMOTE_SERVER}"
echo "Deploy Path: ${DEPLOY_PATH}"
}
}
}
stage('Git Operations on Remote Server') {
steps {
script {
sshagent(credentials: [SSH_CREDENTIALS]) {
withCredentials([usernamePassword(credentialsId: GIT_CREDENTIALS, usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) {
sh """
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
set -e
echo "Checking Git repo..."
if [ -d "${DEPLOY_PATH}/.git" ]; then
echo "Pulling latest code..."
cd ${DEPLOY_PATH}
# Fix ownership issues
sudo chown -R ubuntu:ubuntu ${DEPLOY_PATH}
git config --global --add safe.directory ${DEPLOY_PATH}
git reset --hard
git clean -fd
git config pull.rebase false
git pull https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/Tech4Biz-Services/codenuk-frontend-live.git main
else
echo "Cloning fresh repo..."
sudo rm -rf ${DEPLOY_PATH}
sudo mkdir -p /var/www/html
sudo git clone https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/Tech4Biz-Services/codenuk-frontend-live.git ${DEPLOY_PATH}
sudo chown -R ubuntu:ubuntu ${DEPLOY_PATH}
git config --global --add safe.directory ${DEPLOY_PATH}
fi
cd ${DEPLOY_PATH}
echo "Commit: \$(git rev-parse HEAD)"
'
"""
}
}
}
}
}
stage('Verify Node.js Environment') {
steps {
script {
sshagent(credentials: [SSH_CREDENTIALS]) {
sh """
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
set -e
export PATH="/home/ubuntu/.nvm/versions/node/v22.18.0/bin:\$PATH"
cd ${DEPLOY_PATH}
echo "Node: \$(${NODE_PATH} -v)"
echo "NPM: \$(${NPM_PATH} -v)"
${NPM_PATH} cache clean --force
'
"""
}
}
}
}
stage('Install Dependencies') {
steps {
sshagent(credentials: [SSH_CREDENTIALS]) {
sh """
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
set -e
export PATH="/home/ubuntu/.nvm/versions/node/v22.18.0/bin:\$PATH"
cd ${DEPLOY_PATH}
echo "Installing dependencies..."
rm -rf node_modules package-lock.json
${NPM_PATH} install --force
'
"""
}
}
}
stage('Build Application') {
steps {
sshagent(credentials: [SSH_CREDENTIALS]) {
sh """
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
set -e
export PATH="/home/ubuntu/.nvm/versions/node/v22.18.0/bin:\$PATH"
cd ${DEPLOY_PATH}
echo "Building..."
${NPM_PATH} run build
echo "Build directory contents:"
ls -la dist || ls -la build || echo "No build/dist directory found"
'
"""
}
}
}
stage('Deploy Static Files') {
steps {
sshagent(credentials: [SSH_CREDENTIALS]) {
sh """
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
set -e
cd ${DEPLOY_PATH}
echo "Setting up static files for nginx..."
# Ensure proper ownership and permissions
sudo chown -R www-data:www-data ${DEPLOY_PATH}
sudo find ${DEPLOY_PATH} -type d -exec chmod 755 {} \\;
sudo find ${DEPLOY_PATH} -type f -exec chmod 644 {} \\;
echo "Static files prepared for nginx serving"
'
"""
}
}
}
stage('Restart Nginx') {
steps {
sshagent(credentials: [SSH_CREDENTIALS]) {
sh """
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
set -e
echo "Testing nginx configuration..."
sudo nginx -t
echo "Restarting nginx..."
sudo systemctl restart nginx
echo "Checking nginx status..."
sudo systemctl status nginx --no-pager -l
'
"""
}
}
}
stage('Health Check') {
steps {
sshagent(credentials: [SSH_CREDENTIALS]) {
sh """
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
set -e
echo "Verifying deployment..."
echo "Directory structure:"
ls -la ${DEPLOY_PATH}
echo "Build files:"
ls -la ${DEPLOY_PATH}/dist || ls -la ${DEPLOY_PATH}/build || echo "No build directory found"
echo "Nginx status:"
sudo systemctl is-active nginx
'
"""
}
}
}
}
post {
always {
cleanWs()
}
success {
mail to: "${EMAIL_RECIPIENT}",
subject: "✅ Jenkins - ${PROJECT_NAME} Deployment Successful",
body: """The deployment of '${PROJECT_NAME}' to ${REMOTE_SERVER} was successful.
Build Number: ${BUILD_NUMBER}
URL: ${BUILD_URL}
Time: ${new Date()}
The static files have been deployed and nginx has been restarted.
"""
}
failure {
mail to: "${EMAIL_RECIPIENT}",
subject: "❌ Jenkins - ${PROJECT_NAME} Deployment Failed",
body: """Deployment failed. Please review logs at:
${BUILD_URL}console
Time: ${new Date()}
"""
}
}
}

View File

@ -137,7 +137,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
} }
} }
const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high' }) => { const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high'; logic_rules?: string[] }) => {
// Check if template ID is valid UUID format // Check if template ID is valid UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
if (!uuidRegex.test(template.id)) { if (!uuidRegex.test(template.id)) {
@ -153,6 +153,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
complexity: payload.complexity, complexity: payload.complexity,
is_default: true, is_default: true,
created_by_user: true, created_by_user: true,
logic_rules: payload.logic_rules,
}) })
await load() await load()
} catch (error) { } catch (error) {

View File

@ -0,0 +1,176 @@
"use client"
import { useState } from 'react'
import { AdminSidebar } from './admin-sidebar'
import { AdminTemplatesManager } from './admin-templates-manager'
import { AdminDashboard } from './admin-dashboard'
import { TemplateFeaturesManager } from './template-features-manager'
import { AdminNotificationsPanel } from './admin-notifications-panel'
type AdminView =
| 'dashboard'
| 'templates'
| 'features'
| 'ai-features'
| 'users'
| 'notifications'
| 'analytics'
| 'settings'
| 'create-template'
| 'add-feature'
| 'ai-analyze'
export function AdminLayout() {
const [currentView, setCurrentView] = useState<AdminView>('dashboard')
const [featuresDialogOpen, setFeaturesDialogOpen] = useState(false)
const renderMainContent = () => {
switch (currentView) {
case 'dashboard':
return <AdminDashboard />
case 'templates':
case 'create-template':
return <AdminTemplatesManager />
case 'features':
case 'add-feature':
const defaultTemplate = {
id: "default",
type: "default",
title: "Default Template",
description: "Default template for feature management",
complexity: "medium" as const,
approved: true,
usage_count: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
is_custom: false,
status: "approved" as const
}
return (
<TemplateFeaturesManager
template={defaultTemplate}
open={true}
onOpenChange={(open) => {
if (!open) {
setCurrentView('dashboard')
}
}}
/>
)
case 'ai-features':
case 'ai-analyze':
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 Feature Analysis</h1>
<p className="text-xl text-white/60">AI-powered feature creation and optimization</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-8 text-center">
<div className="text-6xl mb-4">🤖</div>
<h3 className="text-2xl font-bold text-white mb-2">AI Feature Creator</h3>
<p className="text-white/60 mb-6">Analyze project requirements and generate optimized features</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white/5 rounded-lg p-4">
<h4 className="font-semibold text-white mb-2">Smart Analysis</h4>
<p className="text-white/60 text-sm">AI analyzes your project type and suggests relevant features</p>
</div>
<div className="bg-white/5 rounded-lg p-4">
<h4 className="font-semibold text-white mb-2">Essential Features</h4>
<p className="text-white/60 text-sm">Creates essential features that integrate with your template</p>
</div>
<div className="bg-white/5 rounded-lg p-4">
<h4 className="font-semibold text-white mb-2">Auto-Optimization</h4>
<p className="text-white/60 text-sm">Optimizes complexity and requirements automatically</p>
</div>
</div>
</div>
</div>
)
case 'users':
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">User Management</h1>
<p className="text-xl text-white/60">Manage user accounts and permissions</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-8 text-center">
<div className="text-6xl mb-4">👥</div>
<h3 className="text-2xl font-bold text-white mb-2">User Administration</h3>
<p className="text-white/60">User management features coming soon</p>
</div>
</div>
)
case 'notifications':
return <AdminNotificationsPanel open={true} onOpenChange={() => {}} />
case 'analytics':
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">Analytics Dashboard</h1>
<p className="text-xl text-white/60">Usage statistics and performance metrics</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-lg font-semibold text-white mb-2">Total Templates</h3>
<div className="text-3xl font-bold text-orange-400">24</div>
<p className="text-white/60 text-sm">+3 this week</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-lg font-semibold text-white mb-2">Active Users</h3>
<div className="text-3xl font-bold text-emerald-400">156</div>
<p className="text-white/60 text-sm">+12 this week</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-lg font-semibold text-white mb-2">Features Created</h3>
<div className="text-3xl font-bold text-blue-400">89</div>
<p className="text-white/60 text-sm">+7 this week</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-lg font-semibold text-white mb-2">AI Analyses</h3>
<div className="text-3xl font-bold text-purple-400">42</div>
<p className="text-white/60 text-sm">+8 this week</p>
</div>
</div>
</div>
)
case 'settings':
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">System Settings</h1>
<p className="text-xl text-white/60">Configure system preferences and options</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-8 text-center">
<div className="text-6xl mb-4"></div>
<h3 className="text-2xl font-bold text-white mb-2">Configuration Panel</h3>
<p className="text-white/60">System settings panel coming soon</p>
</div>
</div>
)
default:
return <AdminDashboard />
}
}
return (
<div className="min-h-screen bg-black flex">
<AdminSidebar
currentView={currentView}
onViewChange={(view) => setCurrentView(view as AdminView)}
/>
<main className="flex-1 overflow-y-auto">
<div className="p-8">
{renderMainContent()}
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,246 @@
"use client"
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
LayoutDashboard,
FileText,
Settings,
Users,
Bell,
Plus,
Search,
Filter,
ChevronLeft,
ChevronRight,
Home,
Database,
Layers,
Zap,
BarChart3,
Globe,
Code
} from 'lucide-react'
import { cn } from '@/lib/utils'
interface AdminSidebarProps {
currentView: string
onViewChange: (view: string) => void
className?: string
}
export function AdminSidebar({ currentView, onViewChange, className }: AdminSidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(false)
const navigationItems = [
{
id: 'dashboard',
label: 'Dashboard',
icon: LayoutDashboard,
description: 'Overview & Analytics',
badge: null
},
{
id: 'templates',
label: 'Templates',
icon: FileText,
description: 'Manage Templates',
badge: null
},
{
id: 'features',
label: 'Features',
icon: Layers,
description: 'Feature Management',
badge: null
},
{
id: 'ai-features',
label: 'AI Features',
icon: Zap,
description: 'AI-Powered Creation',
badge: 'NEW'
},
{
id: 'users',
label: 'Users',
icon: Users,
description: 'User Management',
badge: null
},
{
id: 'notifications',
label: 'Notifications',
icon: Bell,
description: 'System Alerts',
badge: '3'
},
{
id: 'analytics',
label: 'Analytics',
icon: BarChart3,
description: 'Usage Statistics',
badge: null
},
{
id: 'settings',
label: 'Settings',
icon: Settings,
description: 'System Configuration',
badge: null
}
]
const quickActions = [
{
id: 'create-template',
label: 'New Template',
icon: Plus,
action: () => onViewChange('create-template')
},
{
id: 'add-feature',
label: 'Add Feature',
icon: Layers,
action: () => onViewChange('add-feature')
},
{
id: 'ai-analyze',
label: 'AI Analyze',
icon: Zap,
action: () => onViewChange('ai-analyze')
}
]
return (
<div className={cn(
"bg-gray-900/50 border-r border-white/10 flex flex-col transition-all duration-300",
isCollapsed ? "w-16" : "w-64",
className
)}>
{/* Header */}
<div className="p-4 border-b border-white/10">
<div className="flex items-center justify-between">
{!isCollapsed && (
<div>
<h2 className="text-white font-semibold text-lg">Admin Panel</h2>
<p className="text-white/60 text-sm">CodeNuk Management</p>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setIsCollapsed(!isCollapsed)}
className="text-white/60 hover:text-white hover:bg-white/10"
>
{isCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
</Button>
</div>
</div>
{/* Navigation */}
<div className="flex-1 overflow-y-auto">
<div className="p-2">
{!isCollapsed && (
<div className="text-white/40 text-xs font-medium uppercase tracking-wider px-3 py-2">
Navigation
</div>
)}
<nav className="space-y-1">
{navigationItems.map((item) => {
const Icon = item.icon
const isActive = currentView === item.id
return (
<Button
key={item.id}
variant="ghost"
onClick={() => onViewChange(item.id)}
className={cn(
"w-full justify-start text-left transition-colors",
isActive
? "bg-orange-500/20 text-orange-400 border-r-2 border-orange-400"
: "text-white/70 hover:text-white hover:bg-white/10",
isCollapsed ? "px-2" : "px-3"
)}
>
<Icon className={cn("h-4 w-4", isCollapsed ? "" : "mr-3")} />
{!isCollapsed && (
<>
<div className="flex-1">
<div className="font-medium">{item.label}</div>
<div className="text-xs text-white/50">{item.description}</div>
</div>
{item.badge && (
<Badge
variant="outline"
className={cn(
"text-xs",
item.badge === 'NEW'
? "bg-emerald-500/20 text-emerald-300 border-emerald-500/30"
: "bg-orange-500/20 text-orange-300 border-orange-500/30"
)}
>
{item.badge}
</Badge>
)}
</>
)}
</Button>
)
})}
</nav>
</div>
{/* Quick Actions */}
{!isCollapsed && (
<div className="p-2 border-t border-white/10 mt-4">
<div className="text-white/40 text-xs font-medium uppercase tracking-wider px-3 py-2">
Quick Actions
</div>
<div className="space-y-1">
{quickActions.map((action) => {
const Icon = action.icon
return (
<Button
key={action.id}
variant="outline"
size="sm"
onClick={action.action}
className="w-full justify-start border-white/20 text-white/70 hover:text-white hover:bg-white/10"
>
<Icon className="h-4 w-4 mr-2" />
{action.label}
</Button>
)
})}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-white/10">
{!isCollapsed ? (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-white/60">System Status</span>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-emerald-400 rounded-full"></div>
<span className="text-emerald-400 text-xs">Online</span>
</div>
</div>
<div className="text-xs text-white/40">
Backend: Connected DB: Healthy
</div>
</div>
) : (
<div className="flex justify-center">
<div className="w-2 h-2 bg-emerald-400 rounded-full"></div>
</div>
)}
</div>
</div>
)
}

View File

@ -178,20 +178,35 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
// Get category icon // Get category icon
const getCategoryIcon = (category: string) => { const getCategoryIcon = (category: string) => {
switch (category.toLowerCase()) { switch (category.toLowerCase()) {
case 'marketing': case 'food delivery':
return Zap return ShoppingCart
case 'software': case 'e-commerce':
return Code
case 'seo':
return BarChart3
case 'ecommerce': case 'ecommerce':
return ShoppingCart return ShoppingCart
case 'portfolio': case 'saas platform':
return Code
case 'mobile app':
return Code
case 'dashboard':
return BarChart3
case 'crm system':
return Briefcase return Briefcase
case 'business': case 'learning platform':
return Briefcase
case 'education':
return GraduationCap return GraduationCap
case 'healthcare':
return AlertCircle
case 'real estate':
return Globe
case 'travel':
return Globe
case 'entertainment':
return Zap
case 'finance':
return BarChart3
case 'social media':
return Zap
case 'marketplace':
return ShoppingCart
default: default:
return Globe return Globe
} }
@ -199,37 +214,69 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
// Get category stats for filters // Get category stats for filters
const getCategoryStats = () => { const getCategoryStats = () => {
// Start with "All Templates"
const categoryStats = [ const categoryStats = [
{ id: 'all', name: 'All Templates', count: templates.length, icon: Globe }, { id: 'all', name: 'All Templates', count: templates.length, icon: Globe }
{ id: 'marketing', name: 'Marketing', count: 0, icon: Zap },
{ id: 'software', name: 'Software', count: 0, icon: Code },
{ id: 'seo', name: 'SEO', count: 0, icon: BarChart3 },
{ id: 'ecommerce', name: 'E-commerce', count: 0, icon: ShoppingCart },
{ id: 'portfolio', name: 'Portfolio', count: 0, icon: Briefcase },
{ id: 'business', name: 'Business', count: 0, icon: Briefcase },
{ id: 'education', name: 'Education', count: 0, icon: GraduationCap }
] ]
// Add categories based on your actual categories array
categories.forEach(category => {
if (category !== 'Other') { // Skip 'Other' as it will be handled separately
categoryStats.push({
id: category.toLowerCase().replace(/\s+/g, '-'),
name: category,
count: 0,
icon: getCategoryIcon(category)
})
}
})
// Add 'Other' category at the end
categoryStats.push({
id: 'other',
name: 'Other',
count: 0,
icon: Globe
})
// Count templates by category // Count templates by category
templates.forEach(template => { templates.forEach(template => {
const categoryId = template.category?.toLowerCase() const templateCategory = template.category
const categoryItem = categoryStats.find(cat => cat.id === categoryId) if (templateCategory) {
const categoryItem = categoryStats.find(cat =>
cat.name.toLowerCase() === templateCategory.toLowerCase() ||
cat.id === templateCategory.toLowerCase().replace(/\s+/g, '-')
)
if (categoryItem) { if (categoryItem) {
categoryItem.count++ categoryItem.count++
} else {
// If category doesn't match any predefined category, add to 'Other'
const otherCategory = categoryStats.find(cat => cat.id === 'other')
if (otherCategory) {
otherCategory.count++
}
}
} }
}) })
return categoryStats return categoryStats
} }
// Filter templates based on search // Filter templates based on search and category
const filteredTemplates = templates.filter(template => { const filteredTemplates = templates.filter(template => {
const matchesSearch = !searchQuery || const matchesSearch = !searchQuery ||
template.title?.toLowerCase().includes(searchQuery.toLowerCase()) || template.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.description?.toLowerCase().includes(searchQuery.toLowerCase()) || template.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.type?.toLowerCase().includes(searchQuery.toLowerCase()) template.type?.toLowerCase().includes(searchQuery.toLowerCase())
return matchesSearch const matchesCategory = categoryFilter === 'all' ||
template.category?.toLowerCase() === categoryFilter.toLowerCase() ||
template.category?.toLowerCase().replace(/\s+/g, '-') === categoryFilter ||
(categoryFilter === 'other' && !categories.some(cat =>
cat.toLowerCase() === template.category?.toLowerCase()
))
return matchesSearch && matchesCategory
}) })
const TemplateCard = ({ template }: { template: AdminTemplate }) => ( const TemplateCard = ({ template }: { template: AdminTemplate }) => (
@ -428,30 +475,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
</Select> </Select>
</div> </div>
{/* Category Filters */} {/* Category Filters removed; using only dropdown above */}
<div className="flex flex-wrap gap-4">
{getCategoryStats().map((category) => {
const Icon = category.icon
const active = categoryFilter === category.id
return (
<button
key={category.id}
onClick={() => setCategoryFilter(category.id)}
className={`flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all ${
active
? "bg-orange-500 text-black border-orange-500"
: "bg-white/5 text-white/80 border-white/10 hover:bg-white/10"
}`}
>
<Icon className="h-4 w-4" />
<span className="font-medium">{category.name}</span>
<Badge variant="secondary" className="ml-1 bg-white/10 text-white">
{category.count}
</Badge>
</button>
)
})}
</div>
{/* Create Template Modal */} {/* Create Template Modal */}
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}> <Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>