320 lines
10 KiB
TypeScript
320 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
import { Loader2, Save, AlertTriangle } from 'lucide-react'
|
|
import { adminApi, AdminApiError } from '@/lib/api/admin'
|
|
import { AdminTemplate } from '@/types/admin.types'
|
|
import { useToast } from '@/components/ui/toast'
|
|
|
|
interface TemplateEditDialogProps {
|
|
template: AdminTemplate
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onUpdate: (templateId: string, updatedTemplate: AdminTemplate) => void
|
|
}
|
|
|
|
export function TemplateEditDialog({
|
|
template,
|
|
open,
|
|
onOpenChange,
|
|
onUpdate
|
|
}: TemplateEditDialogProps) {
|
|
const { show } = useToast()
|
|
const [formData, setFormData] = useState({
|
|
title: '',
|
|
description: '',
|
|
category: '',
|
|
type: '',
|
|
icon: '',
|
|
gradient: '',
|
|
border: '',
|
|
text: '',
|
|
subtext: ''
|
|
})
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [conflictInfo, setConflictInfo] = useState<{ title?: string; type?: string } | null>(null)
|
|
|
|
const categories = [
|
|
"Food Delivery",
|
|
"E-commerce",
|
|
"SaaS Platform",
|
|
"Mobile App",
|
|
"Dashboard",
|
|
"CRM System",
|
|
"Learning Platform",
|
|
"Healthcare",
|
|
"Real Estate",
|
|
"Travel",
|
|
"Entertainment",
|
|
"Finance",
|
|
"Social Media",
|
|
"Marketplace",
|
|
"Other"
|
|
]
|
|
|
|
// Initialize form data when template changes
|
|
useEffect(() => {
|
|
if (template) {
|
|
setFormData({
|
|
title: template.title || '',
|
|
description: template.description || '',
|
|
category: template.category || '',
|
|
type: template.type || '',
|
|
icon: template.icon || '',
|
|
gradient: template.gradient || '',
|
|
border: template.border || '',
|
|
text: template.text || '',
|
|
subtext: template.subtext || ''
|
|
})
|
|
setError(null)
|
|
setConflictInfo(null)
|
|
}
|
|
}, [template])
|
|
|
|
const handleInputChange = (field: string, value: string) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }))
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (!formData.title.trim()) {
|
|
setError('Template title is required')
|
|
return
|
|
}
|
|
|
|
if (!formData.type.trim()) {
|
|
setError('Template type is required')
|
|
return
|
|
}
|
|
|
|
try {
|
|
setLoading(true)
|
|
setError(null)
|
|
setConflictInfo(null)
|
|
|
|
// Prepare update data
|
|
const updateData = {
|
|
title: formData.title.trim(),
|
|
description: formData.description.trim() || undefined,
|
|
category: formData.category || undefined,
|
|
type: formData.type.trim(),
|
|
icon: formData.icon.trim() || undefined,
|
|
gradient: formData.gradient.trim() || undefined,
|
|
border: formData.border.trim() || undefined,
|
|
text: formData.text.trim() || undefined,
|
|
subtext: formData.subtext.trim() || undefined
|
|
}
|
|
|
|
const newTemplate = await adminApi.createTemplateFromEdit(template.id, updateData)
|
|
|
|
// Update the template in the parent component with the new template data
|
|
onUpdate(template.id, { ...template, ...newTemplate, status: 'pending' })
|
|
|
|
// Close dialog
|
|
onOpenChange(false)
|
|
} catch (error) {
|
|
if (error instanceof AdminApiError) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const data: any = (error as any).data || {}
|
|
if (data?.existing_template) {
|
|
setConflictInfo({ title: data.existing_template.title, type: data.existing_template.type })
|
|
}
|
|
const message = data?.message || error.message || 'Failed to create template'
|
|
setError(message)
|
|
show({
|
|
title: 'Template creation failed',
|
|
description: data?.existing_template
|
|
? `${message} — Existing: ${data.existing_template.title} (${data.existing_template.type}).`
|
|
: message,
|
|
variant: 'error',
|
|
})
|
|
} else {
|
|
const message = error instanceof Error ? error.message : 'Failed to create template'
|
|
setError(message)
|
|
show({ title: 'Template creation failed', description: message, variant: 'error' })
|
|
}
|
|
console.error('Error creating template:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Template from Edit</DialogTitle>
|
|
<p className="text-sm text-gray-600">
|
|
We'll help you create a comprehensive template. based on: <strong>{template.title}</strong>
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
The new template will be created with 'pending' status for review.
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="type">Template Type *</Label>
|
|
<Input
|
|
id="type"
|
|
placeholder="e.g., multi_restaurant_food_delivery"
|
|
value={formData.type}
|
|
onChange={(e) => handleInputChange('type', e.target.value)}
|
|
required
|
|
/>
|
|
<p className="text-xs text-gray-500">Unique identifier for the template</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="title">Title *</Label>
|
|
<Input
|
|
id="title"
|
|
placeholder="e.g., Multi-Restaurant Food Delivery App"
|
|
value={formData.title}
|
|
onChange={(e) => handleInputChange('title', e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">Description *</Label>
|
|
<Textarea
|
|
id="description"
|
|
placeholder="Describe your template and its key features..."
|
|
value={formData.description}
|
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
|
className="min-h-[100px]"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="category">Category *</Label>
|
|
<Select value={formData.category} onValueChange={(value) => handleInputChange('category', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a category" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categories.map((category) => (
|
|
<SelectItem key={category} value={category}>
|
|
{category}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="icon">Icon (optional)</Label>
|
|
<Input
|
|
id="icon"
|
|
placeholder="e.g., restaurant, shopping-cart, users"
|
|
value={formData.icon}
|
|
onChange={(e) => handleInputChange('icon', e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="gradient">Gradient (optional)</Label>
|
|
<Input
|
|
id="gradient"
|
|
placeholder="e.g., from-orange-400 to-red-500"
|
|
value={formData.gradient}
|
|
onChange={(e) => handleInputChange('gradient', e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="border">Border (optional)</Label>
|
|
<Input
|
|
id="border"
|
|
placeholder="e.g., border-orange-500"
|
|
value={formData.border}
|
|
onChange={(e) => handleInputChange('border', e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="text">Text Color (optional)</Label>
|
|
<Input
|
|
id="text"
|
|
placeholder="e.g., text-orange-500"
|
|
value={formData.text}
|
|
onChange={(e) => handleInputChange('text', e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="subtext">Subtext (optional)</Label>
|
|
<Input
|
|
id="subtext"
|
|
placeholder="e.g., Perfect for food delivery startups"
|
|
value={formData.subtext}
|
|
onChange={(e) => handleInputChange('subtext', e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Error Display */}
|
|
{error && (
|
|
<Alert>
|
|
<AlertTriangle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
{error}
|
|
{conflictInfo && (
|
|
<span className="block mt-1 text-xs text-gray-600">
|
|
Existing: {conflictInfo.title} ({conflictInfo.type}). Try a different title.
|
|
</span>
|
|
)}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-end space-x-2 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={loading}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={loading || !formData.title.trim() || !formData.type.trim()}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
Creating...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="h-4 w-4 mr-2" />
|
|
Create New Template
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|