add prompt import and export
This commit is contained in:
parent
3b0cc31f27
commit
6e0357f7f5
85
src/app/api/prompts/export/route.ts
Normal file
85
src/app/api/prompts/export/route.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const userId = searchParams.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Missing userId' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Fetch all prompts with their versions, tags, and albums
|
||||
const prompts = await prisma.prompt.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
tags: {
|
||||
select: {
|
||||
name: true,
|
||||
color: true
|
||||
}
|
||||
},
|
||||
versions: {
|
||||
select: {
|
||||
version: true,
|
||||
content: true,
|
||||
changelog: true,
|
||||
createdAt: true
|
||||
},
|
||||
orderBy: {
|
||||
version: 'asc'
|
||||
}
|
||||
},
|
||||
album: {
|
||||
select: {
|
||||
name: true,
|
||||
description: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
// Transform the data for export
|
||||
const exportData = {
|
||||
exportVersion: '1.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
userId,
|
||||
totalPrompts: prompts.length,
|
||||
prompts: prompts.map(prompt => ({
|
||||
id: prompt.id,
|
||||
name: prompt.name,
|
||||
content: prompt.content,
|
||||
description: prompt.description,
|
||||
permissions: prompt.permissions,
|
||||
visibility: prompt.visibility,
|
||||
tags: prompt.tags.map(tag => ({
|
||||
name: tag.name,
|
||||
color: tag.color
|
||||
})),
|
||||
album: prompt.album ? {
|
||||
name: prompt.album.name,
|
||||
description: prompt.album.description
|
||||
} : null,
|
||||
versions: prompt.versions,
|
||||
createdAt: prompt.createdAt.toISOString(),
|
||||
updatedAt: prompt.updatedAt.toISOString()
|
||||
}))
|
||||
}
|
||||
|
||||
const fileName = `prompts-export-${new Date().toISOString().split('T')[0]}.json`
|
||||
|
||||
return new NextResponse(JSON.stringify(exportData, null, 2), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': `attachment; filename="${fileName}"`
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
return NextResponse.json({ error: 'Export failed' }, { status: 500 })
|
||||
}
|
||||
}
|
185
src/app/api/prompts/import/route.ts
Normal file
185
src/app/api/prompts/import/route.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
interface ImportPrompt {
|
||||
id: string
|
||||
name: string
|
||||
content: string
|
||||
description?: string | null
|
||||
permissions?: string
|
||||
visibility?: string | null
|
||||
tags?: Array<{ name: string; color?: string }>
|
||||
album?: { name: string; description?: string | null } | null
|
||||
versions?: Array<{
|
||||
version: number
|
||||
content: string
|
||||
changelog?: string | null
|
||||
createdAt: string
|
||||
}>
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
interface ImportData {
|
||||
exportVersion?: string
|
||||
exportDate?: string
|
||||
userId?: string
|
||||
totalPrompts?: number
|
||||
prompts: ImportPrompt[]
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File
|
||||
const userId = formData.get('userId') as string
|
||||
|
||||
if (!file || !userId) {
|
||||
return NextResponse.json({ error: 'Missing file or userId' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Read and parse the file content
|
||||
const fileContent = await file.text()
|
||||
let importData: ImportData
|
||||
|
||||
try {
|
||||
importData = JSON.parse(fileContent)
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON file' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!importData.prompts || !Array.isArray(importData.prompts)) {
|
||||
return NextResponse.json({ error: 'Invalid file format: missing prompts array' }, { status: 400 })
|
||||
}
|
||||
|
||||
const results = {
|
||||
imported: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
messages: [] as string[]
|
||||
}
|
||||
|
||||
// Process each prompt
|
||||
for (const promptData of importData.prompts) {
|
||||
try {
|
||||
// Check if prompt with this ID already exists for this user
|
||||
const existingPrompt = await prisma.prompt.findUnique({
|
||||
where: { id: promptData.id }
|
||||
})
|
||||
|
||||
if (existingPrompt) {
|
||||
results.skipped++
|
||||
results.messages.push(`Skipped prompt "${promptData.name}" (ID: ${promptData.id}) - already exists`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle tags - create if they don't exist
|
||||
const tagIds: string[] = []
|
||||
if (promptData.tags && promptData.tags.length > 0) {
|
||||
for (const tagData of promptData.tags) {
|
||||
const existingTag = await prisma.promptTag.findUnique({
|
||||
where: { name: tagData.name }
|
||||
})
|
||||
|
||||
if (existingTag) {
|
||||
tagIds.push(existingTag.id)
|
||||
} else {
|
||||
const newTag = await prisma.promptTag.create({
|
||||
data: {
|
||||
name: tagData.name,
|
||||
color: tagData.color || '#3B82F6'
|
||||
}
|
||||
})
|
||||
tagIds.push(newTag.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle album - create if it doesn't exist
|
||||
let albumId: string | null = null
|
||||
if (promptData.album) {
|
||||
const existingAlbum = await prisma.promptAlbum.findFirst({
|
||||
where: { name: promptData.album.name }
|
||||
})
|
||||
|
||||
if (existingAlbum) {
|
||||
albumId = existingAlbum.id
|
||||
} else {
|
||||
const newAlbum = await prisma.promptAlbum.create({
|
||||
data: {
|
||||
name: promptData.album.name,
|
||||
description: promptData.album.description
|
||||
}
|
||||
})
|
||||
albumId = newAlbum.id
|
||||
}
|
||||
}
|
||||
|
||||
// Create the prompt with the original ID
|
||||
const newPrompt = await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptData.id,
|
||||
name: promptData.name,
|
||||
content: promptData.content,
|
||||
description: promptData.description,
|
||||
permissions: promptData.permissions || 'private',
|
||||
visibility: promptData.visibility,
|
||||
userId,
|
||||
albumId,
|
||||
createdAt: promptData.createdAt ? new Date(promptData.createdAt) : undefined,
|
||||
updatedAt: promptData.updatedAt ? new Date(promptData.updatedAt) : undefined,
|
||||
tags: {
|
||||
connect: tagIds.map(id => ({ id }))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Import versions if available
|
||||
if (promptData.versions && promptData.versions.length > 0) {
|
||||
for (const versionData of promptData.versions) {
|
||||
await prisma.promptVersion.create({
|
||||
data: {
|
||||
promptId: newPrompt.id,
|
||||
version: versionData.version,
|
||||
content: versionData.content,
|
||||
changelog: versionData.changelog,
|
||||
createdAt: new Date(versionData.createdAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If no versions are provided, create version 1 with the prompt content
|
||||
await prisma.promptVersion.create({
|
||||
data: {
|
||||
promptId: newPrompt.id,
|
||||
version: 1,
|
||||
content: promptData.content,
|
||||
changelog: 'Initial version'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
results.imported++
|
||||
results.messages.push(`Imported prompt "${promptData.name}" successfully`)
|
||||
|
||||
} catch (error) {
|
||||
results.errors++
|
||||
results.messages.push(`Error importing prompt "${promptData.name}": ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
console.error('Import error for prompt:', promptData.name, error)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
results,
|
||||
message: `Import completed: ${results.imported} imported, ${results.skipped} skipped, ${results.errors} errors`
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Import error:', error)
|
||||
return NextResponse.json({
|
||||
error: 'Import failed',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import { LoadingRing } from '@/components/ui/loading-ring'
|
||||
import { EditPromptModal } from '@/components/studio/EditPromptModal'
|
||||
import { PromptDetailModal } from '@/components/studio/PromptDetailModal'
|
||||
import { BulkAddPromptModal } from '@/components/studio/BulkAddPromptModal'
|
||||
import { ImportPromptModal } from '@/components/studio/ImportPromptModal'
|
||||
import { PermissionToggle } from '@/components/studio/PermissionToggle'
|
||||
import {
|
||||
Plus,
|
||||
@ -22,7 +23,9 @@ import {
|
||||
ChevronDown,
|
||||
Grid,
|
||||
List,
|
||||
FolderPlus
|
||||
FolderPlus,
|
||||
Download,
|
||||
Upload
|
||||
} from 'lucide-react'
|
||||
|
||||
interface Prompt {
|
||||
@ -77,6 +80,9 @@ export default function StudioPage() {
|
||||
// Bulk Add Modal
|
||||
const [isBulkAddModalOpen, setIsBulkAddModalOpen] = useState(false)
|
||||
|
||||
// Import Modal
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false)
|
||||
|
||||
// Pagination
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const itemsPerPage = 12
|
||||
@ -269,6 +275,37 @@ export default function StudioPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportPrompts = async () => {
|
||||
if (!user) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/prompts/export?userId=${user.id}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Export failed')
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = `prompts-export-${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
// Could add a toast notification here
|
||||
}
|
||||
}
|
||||
|
||||
const handleImportComplete = () => {
|
||||
fetchPrompts(false)
|
||||
setIsImportModalOpen(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
@ -303,6 +340,26 @@ export default function StudioPage() {
|
||||
<p className="text-muted-foreground mt-1">{t('myPrompts')}</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleExportPrompts}
|
||||
className="flex items-center justify-center space-x-2 h-10"
|
||||
title="Export all prompts"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Export</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsImportModalOpen(true)}
|
||||
className="flex items-center justify-center space-x-2 h-10"
|
||||
title="Import prompts from file"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Import</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsBulkAddModalOpen(true)}
|
||||
@ -781,6 +838,14 @@ export default function StudioPage() {
|
||||
onSave={handleBulkAdd}
|
||||
userId={user?.id || ''}
|
||||
/>
|
||||
|
||||
{/* Import Modal */}
|
||||
<ImportPromptModal
|
||||
isOpen={isImportModalOpen}
|
||||
onClose={() => setIsImportModalOpen(false)}
|
||||
onImportComplete={handleImportComplete}
|
||||
userId={user?.id || ''}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
296
src/components/studio/ImportPromptModal.tsx
Normal file
296
src/components/studio/ImportPromptModal.tsx
Normal file
@ -0,0 +1,296 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { LoadingRing } from '@/components/ui/loading-ring'
|
||||
import { Upload, FileText, X, CheckCircle, AlertCircle, Info } from 'lucide-react'
|
||||
|
||||
interface ImportPromptModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onImportComplete: () => void
|
||||
userId: string
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
imported: number
|
||||
skipped: number
|
||||
errors: number
|
||||
messages: string[]
|
||||
}
|
||||
|
||||
export function ImportPromptModal({ isOpen, onClose, onImportComplete, userId }: ImportPromptModalProps) {
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [importResult, setImportResult] = useState<ImportResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) {
|
||||
setSelectedFile(file)
|
||||
setError(null)
|
||||
setImportResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (event: React.DragEvent) => {
|
||||
event.preventDefault()
|
||||
const file = event.dataTransfer.files[0]
|
||||
if (file && file.type === 'application/json') {
|
||||
setSelectedFile(file)
|
||||
setError(null)
|
||||
setImportResult(null)
|
||||
} else {
|
||||
setError('Please select a JSON file')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (event: React.DragEvent) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!selectedFile || !userId) return
|
||||
|
||||
setIsUploading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', selectedFile)
|
||||
formData.append('userId', userId)
|
||||
|
||||
const response = await fetch('/api/prompts/import', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Import failed')
|
||||
}
|
||||
|
||||
setImportResult(data.results)
|
||||
if (data.results.imported > 0) {
|
||||
onImportComplete()
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Import failed')
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedFile(null)
|
||||
setImportResult(null)
|
||||
setError(null)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-background rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-primary/10 p-2 rounded-lg">
|
||||
<Upload className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-foreground">Import Prompts</h2>
|
||||
<p className="text-sm text-muted-foreground">Import prompts from a JSON file</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={handleClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 overflow-y-auto">
|
||||
{!importResult ? (
|
||||
<>
|
||||
{/* File Upload Area */}
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||
selectedFile
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50 hover:bg-muted/50'
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
{selectedFile ? (
|
||||
<div className="space-y-3">
|
||||
<div className="bg-primary/10 w-12 h-12 rounded-lg flex items-center justify-center mx-auto">
|
||||
<FileText className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{selectedFile.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{formatFileSize(selectedFile.size)}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedFile(null)}
|
||||
className="mt-2"
|
||||
>
|
||||
Remove file
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="bg-muted w-12 h-12 rounded-lg flex items-center justify-center mx-auto">
|
||||
<Upload className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Drop your JSON file here</p>
|
||||
<p className="text-sm text-muted-foreground">or click to browse</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="mt-2"
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-100 mb-1">Import Instructions:</p>
|
||||
<ul className="text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>• Upload a JSON file exported from Prmbr</li>
|
||||
<li>• Prompts with duplicate IDs will be skipped</li>
|
||||
<li>• Tags and albums will be created if they don't exist</li>
|
||||
<li>• Version history will be preserved</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||
<p className="text-sm font-medium text-red-900 dark:text-red-100">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-3 pt-6">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={!selectedFile || isUploading}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<LoadingRing size="sm" className="mr-2" />
|
||||
Importing...
|
||||
</>
|
||||
) : (
|
||||
'Import'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Import Results */}
|
||||
<div className="space-y-6">
|
||||
{/* Summary */}
|
||||
<div className="bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<div>
|
||||
<p className="font-medium text-green-900 dark:text-green-100">Import Completed</p>
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
{importResult.imported} imported, {importResult.skipped} skipped, {importResult.errors} errors
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-card border border-border rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{importResult.imported}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Imported</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
||||
{importResult.skipped}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Skipped</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-red-600 dark:text-red-400">
|
||||
{importResult.errors}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Errors</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
{importResult.messages.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-foreground">Import Details</h3>
|
||||
<div className="bg-muted rounded-lg p-3 max-h-40 overflow-y-auto">
|
||||
{importResult.messages.map((message, index) => (
|
||||
<div key={index} className="text-sm text-muted-foreground py-1">
|
||||
{message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<Button onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user