diff --git a/apps/playground/package.json b/apps/playground/package.json index c0e39e1d3..b21c4b5c3 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -16,13 +16,14 @@ "@object-ui/designer": "workspace:*", "@object-ui/plugin-charts": "workspace:*", "@object-ui/plugin-editor": "workspace:*", + "@object-ui/plugin-kanban": "workspace:*", + "@object-ui/plugin-markdown": "workspace:*", "@object-ui/react": "workspace:*", "lucide-react": "^0.469.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.12.0", - "@object-ui/plugin-markdown": "workspace:*", - "@object-ui/plugin-kanban": "workspace:*" + "sonner": "^2.0.7" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/apps/playground/src/App.tsx b/apps/playground/src/App.tsx index 61d543189..6c2015256 100644 --- a/apps/playground/src/App.tsx +++ b/apps/playground/src/App.tsx @@ -1,6 +1,8 @@ -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { Toaster } from 'sonner'; import { Home } from './pages/Home'; import { Studio } from './pages/Studio'; +import { MyDesigns } from './pages/MyDesigns'; import '@object-ui/components'; // Import lazy-loaded plugins @@ -15,9 +17,13 @@ import './index.css'; export default function App() { return ( + } /> - } /> + } /> + } /> + {/* Default redirect to first example if typed manually specifically */} + } /> ); diff --git a/apps/playground/src/pages/Home.tsx b/apps/playground/src/pages/Home.tsx index 55b7f3c05..aa75e2a17 100644 --- a/apps/playground/src/pages/Home.tsx +++ b/apps/playground/src/pages/Home.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { exampleCategories } from '../data/examples'; -import { LayoutTemplate, ArrowRight, Component, Layers, Database, Shield, Box } from 'lucide-react'; +import { LayoutTemplate, ArrowRight, Component, Layers, Database, Shield, Box, FolderOpen, Plus } from 'lucide-react'; const CategoryIcon = ({ name }: { name: string }) => { switch (name) { @@ -29,16 +29,32 @@ export const Home = () => { Object UI Studio - - - - - GitHub - +
+ + + + + + + GitHub + +
@@ -62,11 +78,28 @@ export const Home = () => { Build Stunning Interfaces,
Purely from JSON. -

+

Object UI transforms JSON schemas into fully functional, accessible, and responsive React applications.
- Select a template below to start building. + Select a template below or start from scratch.

+ +
+ + +
{/* Category Filter */} diff --git a/apps/playground/src/pages/MyDesigns.tsx b/apps/playground/src/pages/MyDesigns.tsx new file mode 100644 index 000000000..0d6a8d049 --- /dev/null +++ b/apps/playground/src/pages/MyDesigns.tsx @@ -0,0 +1,461 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { designStorage, Design } from '../services/designStorage'; +import { + Plus, + FileJson, + Trash2, + Copy, + Share2, + Download, + ArrowLeft, + Clock, + Tag, + Search, + Grid3x3, + List +} from 'lucide-react'; + +export const MyDesigns = () => { + const navigate = useNavigate(); + // Load initial designs from storage to avoid useEffect cascading render + const [designs, setDesigns] = useState(() => designStorage.getAllDesigns()); + const [searchQuery, setSearchQuery] = useState(''); + const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); + const [showImportModal, setShowImportModal] = useState(false); + const [importJson, setImportJson] = useState(''); + const [importName, setImportName] = useState(''); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + const [deleteTargetName, setDeleteTargetName] = useState(''); + + const loadDesigns = () => { + setDesigns(designStorage.getAllDesigns()); + }; + + const handleDelete = (id: string, name: string) => { + setDeleteTargetId(id); + setDeleteTargetName(name); + setShowDeleteConfirm(true); + }; + + const confirmDelete = () => { + if (deleteTargetId) { + designStorage.deleteDesign(deleteTargetId); + loadDesigns(); + toast.success('Design deleted successfully'); + setShowDeleteConfirm(false); + setDeleteTargetId(null); + setDeleteTargetName(''); + } + }; + + const handleClone = (id: string) => { + try { + const cloned = designStorage.cloneDesign(id); + loadDesigns(); + toast.success('Design cloned successfully'); + navigate(`/studio/${cloned.id}`); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + toast.error(`Failed to clone design: ${message}`); + } + }; + + const handleShare = (id: string) => { + try { + const shareUrl = designStorage.shareDesign(id); + navigator.clipboard.writeText(shareUrl); + toast.success('Share link copied to clipboard!'); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + toast.error(`Failed to share design: ${message}`); + } + }; + + const handleExport = (id: string) => { + try { + const { json, filename } = designStorage.exportDesign(id); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + toast.success('Design exported successfully'); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + toast.error(`Failed to export design: ${message}`); + } + }; + + const handleImport = () => { + try { + const imported = designStorage.importDesign(importJson, importName || undefined); + setShowImportModal(false); + setImportJson(''); + setImportName(''); + loadDesigns(); + toast.success('Design imported successfully'); + navigate(`/studio/${imported.id}`); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + toast.error(`Failed to import design: ${message}`); + } + }; + + const filteredDesigns = designs.filter(d => + d.name.toLowerCase().includes(searchQuery.toLowerCase()) || + d.description?.toLowerCase().includes(searchQuery.toLowerCase()) || + d.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) + ); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); + }; + + return ( +
+ {/* Header */} +
+
+
+ + + My Designs + +
+
+ + +
+
+
+ + {/* Main Content */} +
+ {/* Search and View Controls */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:outline-none transition-colors" + /> +
+
+ + +
+
+ + {/* Designs Display */} + {filteredDesigns.length === 0 ? ( +
+
+ +
+

+ {searchQuery ? 'No designs found' : 'No designs yet'} +

+

+ {searchQuery ? 'Try a different search term' : 'Create your first design or import a template'} +

+ {!searchQuery && ( + + )} +
+ ) : viewMode === 'grid' ? ( +
+ {filteredDesigns.map((design) => ( +
+
navigate(`/studio/${design.id}`)} + className="p-6 border-b border-gray-100" + > +

+ {design.name} +

+ {design.description && ( +

+ {design.description} +

+ )} +
+ + {formatDate(design.updatedAt)} +
+ {design.tags && design.tags.length > 0 && ( +
+ {design.tags.map((tag) => ( + + + {tag} + + ))} +
+ )} +
+
+ + + + +
+
+ ))} +
+ ) : ( +
+ {filteredDesigns.map((design, index) => ( +
navigate(`/studio/${design.id}`)} + > +
+

{design.name}

+ {design.description && ( +

{design.description}

+ )} +
+ + + {formatDate(design.updatedAt)} + + {design.tags && design.tags.length > 0 && ( + + + {design.tags.join(', ')} + + )} +
+
+
+ + + + +
+
+ ))} +
+ )} +
+ + {/* Import Modal */} + {showImportModal && ( +
+
+
+

Import Design

+

Paste your JSON schema below

+
+
+
+ + setImportName(e.target.value)} + placeholder="My Imported Design" + className="w-full px-4 py-2 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:outline-none transition-colors" + /> +
+
+ +