Skip to content

Commit 39a3854

Browse files
Copilothuangyiirene
andcommitted
Address code review feedback - add UUID generation, toast notifications, and confirmation dialog
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent b76f864 commit 39a3854

6 files changed

Lines changed: 164 additions & 36 deletions

File tree

apps/playground/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
"@object-ui/designer": "workspace:*",
1717
"@object-ui/plugin-charts": "workspace:*",
1818
"@object-ui/plugin-editor": "workspace:*",
19+
"@object-ui/plugin-kanban": "workspace:*",
20+
"@object-ui/plugin-markdown": "workspace:*",
1921
"@object-ui/react": "workspace:*",
2022
"lucide-react": "^0.469.0",
2123
"react": "^18.3.1",
2224
"react-dom": "^18.3.1",
2325
"react-router-dom": "^7.12.0",
24-
"@object-ui/plugin-markdown": "workspace:*",
25-
"@object-ui/plugin-kanban": "workspace:*"
26+
"sonner": "^2.0.7"
2627
},
2728
"devDependencies": {
2829
"@eslint/js": "^9.39.1",

apps/playground/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
2+
import { Toaster } from 'sonner';
23
import { Home } from './pages/Home';
34
import { Studio } from './pages/Studio';
45
import { MyDesigns } from './pages/MyDesigns';
@@ -16,6 +17,7 @@ import './index.css';
1617
export default function App() {
1718
return (
1819
<Router>
20+
<Toaster position="top-right" richColors />
1921
<Routes>
2022
<Route path="/" element={<Home />} />
2123
<Route path="/my-designs" element={<MyDesigns />} />

apps/playground/src/pages/MyDesigns.tsx

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
3+
import { toast } from 'sonner';
34
import { designStorage, Design } from '../services/designStorage';
45
import {
56
Plus,
@@ -25,50 +26,68 @@ export const MyDesigns = () => {
2526
const [showImportModal, setShowImportModal] = useState(false);
2627
const [importJson, setImportJson] = useState('');
2728
const [importName, setImportName] = useState('');
29+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
30+
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
31+
const [deleteTargetName, setDeleteTargetName] = useState<string>('');
2832

2933
const loadDesigns = () => {
3034
setDesigns(designStorage.getAllDesigns());
3135
};
3236

33-
const handleDelete = (id: string) => {
34-
if (confirm('Are you sure you want to delete this design?')) {
35-
designStorage.deleteDesign(id);
37+
const handleDelete = (id: string, name: string) => {
38+
setDeleteTargetId(id);
39+
setDeleteTargetName(name);
40+
setShowDeleteConfirm(true);
41+
};
42+
43+
const confirmDelete = () => {
44+
if (deleteTargetId) {
45+
designStorage.deleteDesign(deleteTargetId);
3646
loadDesigns();
47+
toast.success('Design deleted successfully');
48+
setShowDeleteConfirm(false);
49+
setDeleteTargetId(null);
50+
setDeleteTargetName('');
3751
}
3852
};
3953

4054
const handleClone = (id: string) => {
4155
try {
4256
const cloned = designStorage.cloneDesign(id);
4357
loadDesigns();
58+
toast.success('Design cloned successfully');
4459
navigate(`/studio/${cloned.id}`);
4560
} catch (error) {
46-
alert('Error cloning design: ' + (error as Error).message);
61+
const message = error instanceof Error ? error.message : 'Unknown error';
62+
toast.error(`Failed to clone design: ${message}`);
4763
}
4864
};
4965

5066
const handleShare = (id: string) => {
5167
try {
5268
const shareUrl = designStorage.shareDesign(id);
5369
navigator.clipboard.writeText(shareUrl);
54-
alert('Share link copied to clipboard!');
70+
toast.success('Share link copied to clipboard!');
5571
} catch (error) {
56-
alert('Error sharing design: ' + (error as Error).message);
72+
const message = error instanceof Error ? error.message : 'Unknown error';
73+
toast.error(`Failed to share design: ${message}`);
5774
}
5875
};
5976

6077
const handleExport = (id: string) => {
6178
try {
62-
const json = designStorage.exportDesign(id);
79+
const { json, filename } = designStorage.exportDesign(id);
6380
const blob = new Blob([json], { type: 'application/json' });
6481
const url = URL.createObjectURL(blob);
6582
const a = document.createElement('a');
6683
a.href = url;
67-
a.download = `design-${id}.json`;
84+
a.download = filename;
6885
a.click();
6986
URL.revokeObjectURL(url);
87+
toast.success('Design exported successfully');
7088
} catch (error) {
71-
alert('Error exporting design: ' + (error as Error).message);
89+
const message = error instanceof Error ? error.message : 'Unknown error';
90+
toast.error(`Failed to export design: ${message}`);
7291
}
7392
};
7493

@@ -79,9 +98,11 @@ export const MyDesigns = () => {
7998
setImportJson('');
8099
setImportName('');
81100
loadDesigns();
101+
toast.success('Design imported successfully');
82102
navigate(`/studio/${imported.id}`);
83103
} catch (error) {
84-
alert('Error importing design: ' + (error as Error).message);
104+
const message = error instanceof Error ? error.message : 'Unknown error';
105+
toast.error(`Failed to import design: ${message}`);
85106
}
86107
};
87108

@@ -258,7 +279,7 @@ export const MyDesigns = () => {
258279
<button
259280
onClick={(e) => {
260281
e.stopPropagation();
261-
handleDelete(design.id);
282+
handleDelete(design.id, design.name);
262283
}}
263284
className="p-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
264285
title="Delete"
@@ -331,7 +352,7 @@ export const MyDesigns = () => {
331352
<button
332353
onClick={(e) => {
333354
e.stopPropagation();
334-
handleDelete(design.id);
355+
handleDelete(design.id, design.name);
335356
}}
336357
className="p-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
337358
title="Delete"
@@ -400,6 +421,41 @@ export const MyDesigns = () => {
400421
</div>
401422
</div>
402423
)}
424+
425+
{/* Delete Confirmation Modal */}
426+
{showDeleteConfirm && (
427+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
428+
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full">
429+
<div className="p-6 border-b border-gray-200">
430+
<h2 className="text-2xl font-bold text-gray-900">Delete Design</h2>
431+
<p className="text-sm text-gray-600 mt-1">This action cannot be undone</p>
432+
</div>
433+
<div className="p-6">
434+
<p className="text-gray-700">
435+
Are you sure you want to delete <strong className="text-gray-900">"{deleteTargetName}"</strong>? This will permanently remove the design and cannot be recovered.
436+
</p>
437+
</div>
438+
<div className="p-6 border-t border-gray-200 flex items-center justify-end gap-3">
439+
<button
440+
onClick={() => {
441+
setShowDeleteConfirm(false);
442+
setDeleteTargetId(null);
443+
setDeleteTargetName('');
444+
}}
445+
className="px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50 border-2 border-gray-200 rounded-xl transition-all"
446+
>
447+
Cancel
448+
</button>
449+
<button
450+
onClick={confirmDelete}
451+
className="px-4 py-2 text-sm font-bold text-white bg-gradient-to-r from-red-600 to-pink-600 hover:from-red-700 hover:to-pink-700 rounded-xl shadow-lg shadow-red-300/50 transition-all"
452+
>
453+
Delete Design
454+
</button>
455+
</div>
456+
</div>
457+
</div>
458+
)}
403459
</div>
404460
);
405461
};

apps/playground/src/pages/Studio.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@ import {
1818
Save,
1919
Share2
2020
} from 'lucide-react';
21+
import { toast } from 'sonner';
2122
import '@object-ui/components';
2223
import { examples, ExampleKey } from '../data/examples';
2324
import { designStorage } from '../services/designStorage';
2425

26+
// Helper function to format design titles
27+
function formatDesignTitle(exampleId: string): string {
28+
if (exampleId === 'new') return 'New Design';
29+
if (typeof exampleId !== 'string') return 'Untitled';
30+
return exampleId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
31+
}
32+
2533
type ViewportSize = 'desktop' | 'tablet' | 'mobile';
2634
type ViewMode = 'code' | 'design' | 'preview';
2735

@@ -92,14 +100,19 @@ const StudioToolbarContext = ({
92100
};
93101

94102
const handleExport = () => {
95-
const json = JSON.stringify(schema, null, 2);
96-
const blob = new Blob([json], { type: 'application/json' });
97-
const url = URL.createObjectURL(blob);
98-
const a = document.createElement('a');
99-
a.href = url;
100-
a.download = 'schema.json';
101-
a.click();
102-
URL.revokeObjectURL(url);
103+
try {
104+
const { json, filename } = designStorage.exportDesign(schema.id || 'temp');
105+
const blob = new Blob([json], { type: 'application/json' });
106+
const url = URL.createObjectURL(blob);
107+
const a = document.createElement('a');
108+
a.href = url;
109+
a.download = filename;
110+
a.click();
111+
URL.revokeObjectURL(url);
112+
toast.success('Design exported successfully');
113+
} catch (error) {
114+
toast.error('Failed to export design');
115+
}
103116
};
104117

105118
return (
@@ -310,8 +323,10 @@ const StudioEditor = ({ exampleId, initialJson, isUserDesign, currentDesignId }:
310323
designStorage.updateDesign(currentDesignId, {
311324
schema: JSON.parse(code)
312325
});
326+
toast.success('Design saved successfully');
313327
} catch (error) {
314-
alert('Error saving design: ' + (error as Error).message);
328+
const message = error instanceof Error ? error.message : 'Unknown error';
329+
toast.error(`Failed to save design: ${message}`);
315330
}
316331
} else {
317332
// Show save modal for new design
@@ -330,9 +345,11 @@ const StudioEditor = ({ exampleId, initialJson, isUserDesign, currentDesignId }:
330345
setShowSaveModal(false);
331346
setSaveName('');
332347
setSaveDescription('');
348+
toast.success('Design saved successfully');
333349
navigate(`/studio/${saved.id}`);
334350
} catch (error) {
335-
alert('Error saving design: ' + (error as Error).message);
351+
const message = error instanceof Error ? error.message : 'Unknown error';
352+
toast.error(`Failed to save design: ${message}`);
336353
}
337354
};
338355

@@ -341,12 +358,15 @@ const StudioEditor = ({ exampleId, initialJson, isUserDesign, currentDesignId }:
341358
try {
342359
const shareUrl = designStorage.shareDesign(currentDesignId);
343360
navigator.clipboard.writeText(shareUrl);
344-
alert('Share link copied to clipboard!');
361+
toast.success('Share link copied to clipboard!');
345362
} catch (error) {
346-
alert('Error sharing design: ' + (error as Error).message);
363+
const message = error instanceof Error ? error.message : 'Unknown error';
364+
toast.error(`Failed to share design: ${message}`);
347365
}
348366
} else {
349-
alert('Please save the design first before sharing.');
367+
// Automatically trigger save modal instead of showing alert
368+
setShowSaveModal(true);
369+
toast.info('Please save the design first before sharing');
350370
}
351371
};
352372

@@ -361,7 +381,7 @@ const StudioEditor = ({ exampleId, initialJson, isUserDesign, currentDesignId }:
361381
<div className="flex h-screen w-screen bg-background text-foreground flex-col">
362382
{/* Top Header injected into Designer Context */}
363383
<StudioToolbarContext
364-
exampleTitle={exampleId === 'new' ? 'New Design' : (typeof exampleId === 'string' ? exampleId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') : 'Untitled')}
384+
exampleTitle={formatDesignTitle(exampleId)}
365385
jsonError={jsonError}
366386
viewMode={viewMode}
367387
setViewMode={setViewMode}

apps/playground/src/services/designStorage.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@
44
* Currently uses localStorage, can be extended to use cloud storage
55
*/
66

7+
/**
8+
* Sanitize a filename by removing invalid characters
9+
*/
10+
function sanitizeFilename(name: string): string {
11+
return name
12+
.replace(/[^a-zA-Z0-9-_\s]/g, '') // Remove invalid characters
13+
.replace(/\s+/g, '-') // Replace spaces with hyphens
14+
.substring(0, 100); // Limit length
15+
}
16+
717
export interface Design {
818
id: string;
919
name: string;
1020
description?: string;
11-
schema: Record<string, unknown>;
21+
schema: unknown;
1222
createdAt: string;
1323
updatedAt: string;
1424
isTemplate?: boolean;
@@ -131,12 +141,14 @@ class DesignStorageService {
131141
}
132142

133143
/**
134-
* Export a design as JSON
144+
* Export a design as JSON with user-friendly filename
135145
*/
136-
exportDesign(id: string): string {
146+
exportDesign(id: string): { json: string; filename: string } {
137147
const design = this.getDesign(id);
138148
if (!design) throw new Error('Design not found');
139-
return JSON.stringify(design.schema, null, 2);
149+
const json = JSON.stringify(design.schema, null, 2);
150+
const filename = `${sanitizeFilename(design.name) || 'design'}.json`;
151+
return { json, filename };
140152
}
141153

142154
/**
@@ -155,12 +167,46 @@ class DesignStorageService {
155167
}
156168

157169
// Private helper methods
170+
private generateUUID(): string {
171+
// Prefer native crypto.randomUUID when available
172+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
173+
return crypto.randomUUID();
174+
}
175+
176+
// Fallback: generate RFC4122 v4 UUID using crypto.getRandomValues
177+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
178+
const bytes = new Uint8Array(16);
179+
crypto.getRandomValues(bytes);
180+
181+
// Per RFC4122 section 4.4
182+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
183+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
184+
185+
const toHex = (n: number) => n.toString(16).padStart(2, '0');
186+
const segments = [
187+
Array.from(bytes.slice(0, 4)).map(toHex).join(''),
188+
Array.from(bytes.slice(4, 6)).map(toHex).join(''),
189+
Array.from(bytes.slice(6, 8)).map(toHex).join(''),
190+
Array.from(bytes.slice(8, 10)).map(toHex).join(''),
191+
Array.from(bytes.slice(10, 16)).map(toHex).join(''),
192+
];
193+
194+
return segments.join('-');
195+
}
196+
197+
// Last-resort fallback for environments without Web Crypto
198+
return `fallback_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
199+
}
200+
158201
private generateId(): string {
159-
return `design_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
202+
return `design_${this.generateUUID()}`;
160203
}
161204

162205
private generateShareId(): string {
163-
return Math.random().toString(36).substring(2, 14);
206+
// Generate a compact share ID derived from a UUID for security
207+
const uuid = this.generateUUID();
208+
// Remove hyphens and take first 12 characters for a shorter share link
209+
return uuid.replace(/-/g, '').substring(0, 12);
164210
}
165211

166212
private getSharedDesigns(): Record<string, Design> {

0 commit comments

Comments
 (0)