diff --git a/src/app/api/prompts/export/route.ts b/src/app/api/prompts/export/route.ts new file mode 100644 index 0000000..4031905 --- /dev/null +++ b/src/app/api/prompts/export/route.ts @@ -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 }) + } +} \ No newline at end of file diff --git a/src/app/api/prompts/import/route.ts b/src/app/api/prompts/import/route.ts new file mode 100644 index 0000000..0c1ba91 --- /dev/null +++ b/src/app/api/prompts/import/route.ts @@ -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 }) + } +} \ No newline at end of file diff --git a/src/app/studio/page.tsx b/src/app/studio/page.tsx index 001074b..2f080ef 100644 --- a/src/app/studio/page.tsx +++ b/src/app/studio/page.tsx @@ -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() {
{t('myPrompts')}
Import prompts from a JSON file
+{selectedFile.name}
+{formatFileSize(selectedFile.size)}
+Drop your JSON file here
+or click to browse
+Import Instructions:
+{error}
+Import Completed
++ {importResult.imported} imported, {importResult.skipped} skipped, {importResult.errors} errors +
+