mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-05-03 10:04:51 +00:00
Generated by Spark: Create seed data templates for different project types (e-commerce, blog, dashboard)
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useSeedTemplates } from '@/hooks/data/use-seed-templates'
|
||||
import { Copy, Download } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function TemplateExplorer() {
|
||||
const { templates } = useSeedTemplates()
|
||||
const [selectedTemplate, setSelectedTemplate] = useState(templates[0]?.id || 'default')
|
||||
|
||||
const currentTemplate = templates.find(t => t.id === selectedTemplate)
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
toast.success('Copied to clipboard')
|
||||
}
|
||||
|
||||
const downloadJSON = () => {
|
||||
if (!currentTemplate) return
|
||||
|
||||
const dataStr = JSON.stringify(currentTemplate.data, null, 2)
|
||||
const blob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${currentTemplate.id}-template.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
toast.success('Template downloaded')
|
||||
}
|
||||
|
||||
const exportCurrentData = async () => {
|
||||
const keys = await window.spark.kv.keys()
|
||||
const data: Record<string, any> = {}
|
||||
|
||||
for (const key of keys) {
|
||||
data[key] = await window.spark.kv.get(key)
|
||||
}
|
||||
|
||||
const dataStr = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'current-project-data.json'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
toast.success('Current project data exported')
|
||||
}
|
||||
|
||||
if (!currentTemplate) return null
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">Template Explorer</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Browse and inspect template structures
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={exportCurrentData} variant="outline">
|
||||
<Download className="mr-2" size={16} />
|
||||
Export Current Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
{templates.map((template) => (
|
||||
<Card
|
||||
key={template.id}
|
||||
className={`cursor-pointer transition-colors ${
|
||||
selectedTemplate === template.id ? 'border-primary bg-accent/50' : 'hover:bg-accent/20'
|
||||
}`}
|
||||
onClick={() => setSelectedTemplate(template.id)}
|
||||
>
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{template.icon}</span>
|
||||
<CardTitle className="text-sm">{template.name}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card className="col-span-3">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-4xl">{currentTemplate.icon}</span>
|
||||
<div>
|
||||
<CardTitle>{currentTemplate.name}</CardTitle>
|
||||
<CardDescription>{currentTemplate.description}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={downloadJSON} variant="outline" size="sm">
|
||||
<Download className="mr-2" size={16} />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="structure">Structure</TabsTrigger>
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Features</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{currentTemplate.features.map((feature, idx) => (
|
||||
<Badge key={idx} variant="secondary">
|
||||
{feature}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{Object.entries(currentTemplate.data).map(([key, value]) => (
|
||||
<Card key={key}>
|
||||
<CardHeader className="p-4">
|
||||
<CardTitle className="text-sm">{key.replace('project-', '')}</CardTitle>
|
||||
<CardDescription>
|
||||
{Array.isArray(value) ? `${value.length} items` : 'Configuration'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="structure" className="space-y-4">
|
||||
<ScrollArea className="h-[500px]">
|
||||
{Object.entries(currentTemplate.data).map(([key, value]) => (
|
||||
<div key={key} className="mb-4 p-4 border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="font-semibold">{key}</h3>
|
||||
<Badge variant="outline">
|
||||
{Array.isArray(value) ? `${value.length} items` : 'object'}
|
||||
</Badge>
|
||||
</div>
|
||||
{Array.isArray(value) && value.length > 0 && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{value.slice(0, 3).map((item: any, idx: number) => (
|
||||
<div key={idx} className="py-1">
|
||||
• {item.name || item.title || item.id}
|
||||
</div>
|
||||
))}
|
||||
{value.length > 3 && (
|
||||
<div className="py-1 italic">... and {value.length - 3} more</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="json">
|
||||
<div className="relative">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute right-2 top-2 z-10"
|
||||
onClick={() => copyToClipboard(JSON.stringify(currentTemplate.data, null, 2))}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</Button>
|
||||
<ScrollArea className="h-[500px]">
|
||||
<pre className="text-xs p-4 bg-muted rounded-lg">
|
||||
{JSON.stringify(currentTemplate.data, null, 2)}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useSeedTemplates, type TemplateType } from '@/hooks/data/use-seed-templates'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { TemplateExplorer } from './TemplateExplorer'
|
||||
import { toast } from 'sonner'
|
||||
import { Download, Package, Plus, Trash } from '@phosphor-icons/react'
|
||||
|
||||
export function TemplateSelector() {
|
||||
const { templates, isLoading, clearAndLoadTemplate, mergeTemplate } = useSeedTemplates()
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateType | null>(null)
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
||||
const [actionType, setActionType] = useState<'replace' | 'merge'>('replace')
|
||||
|
||||
const handleSelectTemplate = (templateId: TemplateType, action: 'replace' | 'merge') => {
|
||||
setSelectedTemplate(templateId)
|
||||
setActionType(action)
|
||||
setShowConfirmDialog(true)
|
||||
}
|
||||
|
||||
const handleConfirmLoad = async () => {
|
||||
if (!selectedTemplate) return
|
||||
|
||||
setShowConfirmDialog(false)
|
||||
|
||||
const success = actionType === 'replace'
|
||||
? await clearAndLoadTemplate(selectedTemplate)
|
||||
: await mergeTemplate(selectedTemplate)
|
||||
|
||||
if (success) {
|
||||
toast.success(`Template loaded successfully!`, {
|
||||
description: `${actionType === 'replace' ? 'Replaced' : 'Merged'} with ${selectedTemplate} template`
|
||||
})
|
||||
window.location.reload()
|
||||
} else {
|
||||
toast.error('Failed to load template', {
|
||||
description: 'Please try again or check the console for errors'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultValue="templates" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="templates">Load Templates</TabsTrigger>
|
||||
<TabsTrigger value="explorer">Explore Templates</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="templates" className="space-y-6 mt-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">Project Templates</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Start your project with pre-configured templates including models, components, and workflows
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{templates.map((template) => (
|
||||
<Card key={template.id} className="relative overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-4xl">{template.icon}</span>
|
||||
<div>
|
||||
<CardTitle className="text-xl">{template.name}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
{template.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{template.features.map((feature, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">
|
||||
{feature}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => handleSelectTemplate(template.id, 'replace')}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
<Download className="mr-2" size={16} />
|
||||
Load Template
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSelectTemplate(template.id, 'merge')}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
<Plus className="mr-2" size={16} />
|
||||
Merge
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Package size={16} />
|
||||
<AlertDescription>
|
||||
<strong>Load Template:</strong> Replaces all existing data with the selected template.
|
||||
<br />
|
||||
<strong>Merge:</strong> Adds template data to your existing project without removing current data.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="explorer" className="mt-6">
|
||||
<TemplateExplorer />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{actionType === 'replace' ? 'Replace Project Data?' : 'Merge Template Data?'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{actionType === 'replace' ? (
|
||||
<>
|
||||
This will <strong className="text-destructive">delete all existing data</strong> and load the{' '}
|
||||
<strong>{selectedTemplate}</strong> template. This action cannot be undone.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This will <strong>add</strong> the <strong>{selectedTemplate}</strong> template data to your
|
||||
existing project without removing current data.
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowConfirmDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant={actionType === 'replace' ? 'destructive' : 'default'}
|
||||
onClick={handleConfirmLoad}
|
||||
>
|
||||
{actionType === 'replace' ? (
|
||||
<>
|
||||
<Trash className="mr-2" size={16} />
|
||||
Replace All Data
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="mr-2" size={16} />
|
||||
Merge Data
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './atoms'
|
||||
export * from './molecules'
|
||||
export * from './organisms'
|
||||
export * from './TemplateSelector'
|
||||
export * from './TemplateExplorer'
|
||||
|
||||
Reference in New Issue
Block a user