frontend changes
This commit is contained in:
parent
298e9c2170
commit
f0cc077b63
@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
@ -84,6 +85,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
|||||||
})
|
})
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
const [showAIModal, setShowAIModal] = useState(false)
|
const [showAIModal, setShowAIModal] = useState(false)
|
||||||
|
const [editingFeature, setEditingFeature] = useState<TemplateFeature | null>(null)
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
@ -169,6 +171,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
|||||||
try {
|
try {
|
||||||
await updateFeature(template.id, f.id, updates)
|
await updateFeature(template.id, f.id, updates)
|
||||||
await load()
|
await load()
|
||||||
|
setEditingFeature(null)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error updating feature:', err)
|
console.error('Error updating feature:', err)
|
||||||
const message = err instanceof Error ? err.message : 'Failed to update feature'
|
const message = err instanceof Error ? err.message : 'Failed to update feature'
|
||||||
@ -178,6 +181,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
|||||||
|
|
||||||
const handleDelete = async (f: TemplateFeature) => {
|
const handleDelete = async (f: TemplateFeature) => {
|
||||||
try {
|
try {
|
||||||
|
if (!confirm(`Delete feature "${f.name}"? This cannot be undone.`)) return
|
||||||
await deleteFeature(template.id, f.id)
|
await deleteFeature(template.id, f.id)
|
||||||
setSelectedIds((prev) => {
|
setSelectedIds((prev) => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
@ -230,13 +234,12 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
|||||||
/>
|
/>
|
||||||
<span>{f.name}</span>
|
<span>{f.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{f.feature_type === 'custom' && (
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-blue-500 text-blue-300 hover:bg-blue-500/10"
|
className="border-blue-500 text-blue-300 hover:bg-blue-500/10"
|
||||||
onClick={() => {/* Edit functionality */}}
|
onClick={(e) => { e.stopPropagation(); setEditingFeature(f) }}
|
||||||
>
|
>
|
||||||
<Edit className="h-3 w-3" />
|
<Edit className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -244,12 +247,11 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-red-500 text-red-300 hover:bg-red-500/10"
|
className="border-red-500 text-red-300 hover:bg-red-500/10"
|
||||||
onClick={() => handleDelete(f)}
|
onClick={(e) => { e.stopPropagation(); handleDelete(f) }}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-white/80 text-sm space-y-2">
|
<CardContent className="text-white/80 text-sm space-y-2">
|
||||||
@ -289,6 +291,27 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
|||||||
|
|
||||||
{section('Your Custom Features', customFeatures)}
|
{section('Your Custom Features', customFeatures)}
|
||||||
|
|
||||||
|
{/* Edit Feature Modal reusing AI-Powered Feature Creator */}
|
||||||
|
{editingFeature && (
|
||||||
|
<AICustomFeatureCreator
|
||||||
|
projectType={template.type || template.title}
|
||||||
|
editingFeature={editingFeature as any}
|
||||||
|
onAdd={async (payload) => {
|
||||||
|
await handleUpdate(editingFeature, {
|
||||||
|
name: payload.name,
|
||||||
|
description: payload.description,
|
||||||
|
complexity: payload.complexity,
|
||||||
|
// Map AI form structure to backend fields
|
||||||
|
business_rules: payload.business_rules,
|
||||||
|
// Flatten logic rules to store if backend supports it on update
|
||||||
|
logic_rules: payload.logic_rules,
|
||||||
|
} as any)
|
||||||
|
setEditingFeature(null)
|
||||||
|
}}
|
||||||
|
onClose={() => setEditingFeature(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{showAIModal && (
|
{showAIModal && (
|
||||||
<AICustomFeatureCreator
|
<AICustomFeatureCreator
|
||||||
projectType={template.type || template.title}
|
projectType={template.type || template.title}
|
||||||
|
|||||||
@ -313,7 +313,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
})
|
})
|
||||||
|
|
||||||
const MAX_TITLE_CHARS = 25
|
const MAX_TITLE_CHARS = 25
|
||||||
const MAX_DESCRIPTION_PREVIEW_CHARS = 220
|
const MAX_DESCRIPTION_PREVIEW_CHARS = 90
|
||||||
|
|
||||||
const TemplateCard = ({ template }: { template: AdminTemplate }) => {
|
const TemplateCard = ({ template }: { template: AdminTemplate }) => {
|
||||||
const [descExpanded, setDescExpanded] = useState(false)
|
const [descExpanded, setDescExpanded] = useState(false)
|
||||||
@ -335,14 +335,14 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleManageFeatures(template) } }}
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleManageFeatures(template) } }}
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="space-y-2 flex-1">
|
<div className="space-y-2 flex-1 min-w-0">
|
||||||
<CardTitle className="text-lg text-white group-hover:text-orange-400 transition-colors">
|
<CardTitle className="text-lg text-white group-hover:text-orange-400 transition-colors">
|
||||||
<Tooltip content={title}>
|
<Tooltip content={title}>
|
||||||
<span title={undefined}>{truncatedTitle}</span>
|
<span title={undefined} className="block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">{truncatedTitle}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Badge variant="outline" className="text-xs bg-white/5 border-white/10 text-white/80">
|
<Badge variant="outline" className="text-xs bg-white/5 border-white/10 text-white/80">
|
||||||
{template.type}
|
{template.type}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -351,7 +351,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center space-x-2 shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -373,7 +373,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{fullDesc && (
|
{fullDesc && (
|
||||||
<div className="text-white/70 text-sm">
|
<div className="text-white/70 text-sm break-words hyphens-auto">
|
||||||
<span>{shownDesc}</span>
|
<span>{shownDesc}</span>
|
||||||
{needsClamp && (
|
{needsClamp && (
|
||||||
<button
|
<button
|
||||||
@ -695,7 +695,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
|
|
||||||
{/* Edit Template Modal */}
|
{/* Edit Template Modal */}
|
||||||
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
||||||
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-xl">
|
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Edit Template</DialogTitle>
|
<DialogTitle className="text-white">Edit Template</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -704,12 +704,18 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const formData = new FormData(e.currentTarget as HTMLFormElement)
|
const form = e.currentTarget as HTMLFormElement
|
||||||
|
const formData = new FormData(form)
|
||||||
const payload = {
|
const payload = {
|
||||||
title: String(formData.get('title') || ''),
|
title: String(formData.get('title') || ''),
|
||||||
description: String(formData.get('description') || ''),
|
description: String(formData.get('description') || ''),
|
||||||
category: String(formData.get('category') || ''),
|
category: String(formData.get('category') || ''),
|
||||||
type: String(formData.get('type') || ''),
|
type: String(formData.get('type') || ''),
|
||||||
|
icon: String(formData.get('icon') || ''),
|
||||||
|
gradient: String(formData.get('gradient') || ''),
|
||||||
|
border: String(formData.get('border') || ''),
|
||||||
|
text: String(formData.get('text') || ''),
|
||||||
|
subtext: String(formData.get('subtext') || ''),
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await adminApi.updateAdminTemplate(editingTemplate.id, payload)
|
await adminApi.updateAdminTemplate(editingTemplate.id, payload)
|
||||||
@ -722,24 +728,68 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm">Title</label>
|
|
||||||
<Input name="title" defaultValue={editingTemplate.title || ''} className="bg-white/5 border-white/10 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm">Description</label>
|
|
||||||
<textarea name="description" defaultValue={editingTemplate.description || ''} className="w-full px-3 py-2 bg-white/5 border border-white/10 text-white rounded-md min-h-[80px]" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm">Category</label>
|
<label className="text-sm font-medium text-white">Title *</label>
|
||||||
<Input name="category" defaultValue={editingTemplate.category || ''} className="bg-white/5 border-white/10 text-white" />
|
<Input name="title" defaultValue={editingTemplate.title || ''} className="bg-white/5 border-white/10 text-white" required />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm">Type</label>
|
<label className="text-sm font-medium text-white">Type *</label>
|
||||||
<Input name="type" defaultValue={editingTemplate.type || ''} className="bg-white/5 border-white/10 text-white" />
|
<Input name="type" defaultValue={editingTemplate.type || ''} className="bg-white/5 border-white/10 text-white" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-white">Description *</label>
|
||||||
|
<textarea name="description" defaultValue={editingTemplate.description || ''} className="w-full px-3 py-2 bg-white/5 border border-white/10 text-white rounded-md min-h-[100px]" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-white">Category *</label>
|
||||||
|
<Select defaultValue={editingTemplate.category || ''} onValueChange={(v) => {
|
||||||
|
const input = document.querySelector<HTMLInputElement>('input[name=\'category\']')
|
||||||
|
if (input) input.value = v
|
||||||
|
}}>
|
||||||
|
<SelectTrigger className="bg-white/5 border-white/10 text-white">
|
||||||
|
<SelectValue placeholder="Select a category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-gray-900 border-white/10">
|
||||||
|
{categories.map((category, index) => (
|
||||||
|
<SelectItem key={`edit-category-${index}`} value={category} className="text-white">
|
||||||
|
{category}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<input type="hidden" name="category" defaultValue={editingTemplate.category || ''} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-white">Icon</label>
|
||||||
|
<Input name="icon" defaultValue={editingTemplate.icon || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-white">Gradient</label>
|
||||||
|
<Input name="gradient" defaultValue={editingTemplate.gradient || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-white">Border</label>
|
||||||
|
<Input name="border" defaultValue={editingTemplate.border || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-white">Text Color</label>
|
||||||
|
<Input name="text" defaultValue={editingTemplate.text || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-white">Subtext</label>
|
||||||
|
<Input name="subtext" defaultValue={editingTemplate.subtext || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 pt-2">
|
<div className="flex justify-end space-x-2 pt-2">
|
||||||
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)} className="border-white/20 text-white">Cancel</Button>
|
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)} className="border-white/20 text-white">Cancel</Button>
|
||||||
<Button type="submit" className="bg-orange-500 text-black hover:bg-orange-600">Save</Button>
|
<Button type="submit" className="bg-orange-500 text-black hover:bg-orange-600">Save</Button>
|
||||||
|
|||||||
@ -411,7 +411,8 @@ export const adminApi = {
|
|||||||
subtext?: string;
|
subtext?: string;
|
||||||
}
|
}
|
||||||
): Promise<AdminTemplate> => {
|
): Promise<AdminTemplate> => {
|
||||||
const response = await apiCall<AdminTemplate>(`/api/admin/templates/${templateId}`, {
|
// Backend update endpoint is /api/templates/:id (not under /api/admin)
|
||||||
|
const response = await apiCall<AdminTemplate>(`/api/templates/${templateId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(updateData),
|
body: JSON.stringify(updateData),
|
||||||
});
|
});
|
||||||
@ -420,7 +421,8 @@ export const adminApi = {
|
|||||||
|
|
||||||
// Delete an admin template (main templates table)
|
// Delete an admin template (main templates table)
|
||||||
deleteAdminTemplate: async (templateId: string): Promise<{ success: boolean }> => {
|
deleteAdminTemplate: async (templateId: string): Promise<{ success: boolean }> => {
|
||||||
const response = await apiCall<{ success: boolean }>(`/api/admin/templates/${templateId}`, {
|
// Backend delete endpoint is /api/templates/:id
|
||||||
|
const response = await apiCall<{ success: boolean }>(`/api/templates/${templateId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user