Skip to content

Commit fb90e1f

Browse files
committed
feat: add Canvas Backups feature
- Implemented CanvasBackups component to display recent canvas backups. (!backups) - Added API hooks for fetching backups and restoring selected backups. - Enhanced the pad module to include CanvasBackups and its styles.
1 parent 8dac137 commit fb90e1f

File tree

7 files changed

+276
-1
lines changed

7 files changed

+276
-1
lines changed

src/frontend/src/CustomEmbeddableRenderer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
StateIndicator,
77
ControlButton,
88
HtmlEditor,
9-
Editor
9+
Editor,
10+
CanvasBackups
1011
} from './pad';
1112
import { ActionButton } from './pad/buttons';
1213

@@ -37,6 +38,8 @@ export const renderCustomEmbeddable = (
3738
/>;
3839
case 'dashboard':
3940
return <Dashboard element={element} appState={appState} excalidrawAPI={excalidrawAPI} />;
41+
case 'backups':
42+
return <CanvasBackups element={element} appState={appState} excalidrawAPI={excalidrawAPI} />;
4043
default:
4144
return null;
4245
}

src/frontend/src/api/hooks.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ export interface CanvasData {
2828
files: any;
2929
}
3030

31+
export interface CanvasBackup {
32+
id: number;
33+
timestamp: string;
34+
data: CanvasData;
35+
}
36+
37+
export interface CanvasBackupsResponse {
38+
backups: CanvasBackup[];
39+
}
40+
3141
// API functions
3242
export const api = {
3343
// Authentication
@@ -112,6 +122,16 @@ export const api = {
112122
throw error;
113123
}
114124
},
125+
126+
// Canvas Backups
127+
getCanvasBackups: async (limit: number = 10): Promise<CanvasBackupsResponse> => {
128+
try {
129+
const result = await fetchApi(`/api/canvas/recent?limit=${limit}`);
130+
return result;
131+
} catch (error) {
132+
throw error;
133+
}
134+
},
115135
};
116136

117137
// Query hooks
@@ -162,6 +182,14 @@ export function useDefaultCanvas(options?: UseQueryOptions<CanvasData>) {
162182
});
163183
}
164184

185+
export function useCanvasBackups(limit: number = 10, options?: UseQueryOptions<CanvasBackupsResponse>) {
186+
return useQuery({
187+
queryKey: ['canvasBackups', limit],
188+
queryFn: () => api.getCanvasBackups(limit),
189+
...options,
190+
});
191+
}
192+
165193
// Mutation hooks
166194
export function useStartWorkspace(options?: UseMutationOptions) {
167195
return useMutation({
@@ -188,6 +216,10 @@ export function useStopWorkspace(options?: UseMutationOptions) {
188216
export function useSaveCanvas(options?: UseMutationOptions<any, Error, CanvasData>) {
189217
return useMutation({
190218
mutationFn: api.saveCanvas,
219+
onSuccess: () => {
220+
// Invalidate canvas backups query to trigger refetch
221+
queryClient.invalidateQueries({ queryKey: ['canvasBackups'] });
222+
},
191223
...options,
192224
});
193225
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { useState } from 'react';
2+
import type { NonDeleted, ExcalidrawEmbeddableElement } from '@atyrode/excalidraw/element/types';
3+
import type { AppState } from '@atyrode/excalidraw/types';
4+
import { useCanvasBackups, CanvasBackup } from '../../api/hooks';
5+
import '../styles/CanvasBackups.scss';
6+
7+
interface CanvasBackupsProps {
8+
element: NonDeleted<ExcalidrawEmbeddableElement>;
9+
appState: AppState;
10+
excalidrawAPI?: any;
11+
}
12+
13+
const formatDate = (dateString: string): string => {
14+
const date = new Date(dateString);
15+
return date.toLocaleString(undefined, {
16+
year: 'numeric',
17+
month: 'short',
18+
day: 'numeric',
19+
hour: '2-digit',
20+
minute: '2-digit'
21+
});
22+
};
23+
24+
export const CanvasBackups: React.FC<CanvasBackupsProps> = ({
25+
element,
26+
appState,
27+
excalidrawAPI
28+
}) => {
29+
const { data, isLoading, error } = useCanvasBackups();
30+
const [selectedBackup, setSelectedBackup] = useState<CanvasBackup | null>(null);
31+
32+
const handleBackupSelect = (backup: CanvasBackup) => {
33+
setSelectedBackup(backup);
34+
};
35+
36+
const handleRestoreBackup = () => {
37+
if (selectedBackup && excalidrawAPI) {
38+
// Load the backup data into the canvas
39+
excalidrawAPI.updateScene(selectedBackup.data);
40+
setSelectedBackup(null);
41+
}
42+
};
43+
44+
const handleCancel = () => {
45+
setSelectedBackup(null);
46+
};
47+
48+
if (isLoading) {
49+
return <div className="canvas-backups canvas-backups--loading">Loading backups...</div>;
50+
}
51+
52+
if (error) {
53+
return <div className="canvas-backups canvas-backups--error">Error loading backups</div>;
54+
}
55+
56+
if (!data || data.backups.length === 0) {
57+
return <div className="canvas-backups canvas-backups--empty">No backups available</div>;
58+
}
59+
60+
return (
61+
<div className="canvas-backups">
62+
<h2 className="canvas-backups__title">Recent Canvas Backups</h2>
63+
64+
{selectedBackup ? (
65+
<div className="canvas-backups__confirmation">
66+
<p>Restore canvas from backup created on {formatDate(selectedBackup.timestamp)}?</p>
67+
<p className="canvas-backups__warning">This will replace your current canvas!</p>
68+
<div className="canvas-backups__actions">
69+
<button
70+
className="canvas-backups__button canvas-backups__button--restore"
71+
onClick={handleRestoreBackup}
72+
>
73+
Restore
74+
</button>
75+
<button
76+
className="canvas-backups__button canvas-backups__button--cancel"
77+
onClick={handleCancel}
78+
>
79+
Cancel
80+
</button>
81+
</div>
82+
</div>
83+
) : (
84+
<ul className="canvas-backups__list">
85+
{data.backups.map((backup) => (
86+
<li
87+
key={backup.id}
88+
className="canvas-backups__item"
89+
onClick={() => handleBackupSelect(backup)}
90+
>
91+
<span className="canvas-backups__timestamp">{formatDate(backup.timestamp)}</span>
92+
<button className="canvas-backups__restore-button">Restore</button>
93+
</li>
94+
))}
95+
</ul>
96+
)}
97+
</div>
98+
);
99+
};
100+
101+
export default CanvasBackups;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './CanvasBackups';
2+
export { default as CanvasBackups } from './CanvasBackups';

src/frontend/src/pad/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ export * from './controls/StateIndicator';
44
export * from './containers/Dashboard';
55
export * from './buttons';
66
export * from './editors';
7+
export * from './backups';
78

89
// Default exports
910
export { default as ControlButton } from './controls/ControlButton';
1011
export { default as StateIndicator } from './controls/StateIndicator';
1112
export { default as Dashboard } from './containers/Dashboard';
13+
export { default as CanvasBackups } from './backups/CanvasBackups';
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
.canvas-backups {
2+
display: flex;
3+
flex-direction: column;
4+
height: 100%;
5+
width: 100%;
6+
padding: 1rem;
7+
overflow-y: auto;
8+
background-color: #f5f5f5;
9+
border-radius: 4px;
10+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
11+
font-family: 'Roboto', sans-serif;
12+
13+
&--loading,
14+
&--error,
15+
&--empty {
16+
display: flex;
17+
align-items: center;
18+
justify-content: center;
19+
height: 100%;
20+
color: #666;
21+
font-style: italic;
22+
}
23+
24+
&--error {
25+
color: #d32f2f;
26+
}
27+
28+
&__title {
29+
margin: 0 0 1rem;
30+
font-size: 1.2rem;
31+
font-weight: 500;
32+
color: #333;
33+
text-align: center;
34+
}
35+
36+
&__list {
37+
list-style: none;
38+
padding: 0;
39+
margin: 0;
40+
flex-grow: 1;
41+
overflow-y: auto;
42+
}
43+
44+
&__item {
45+
display: flex;
46+
align-items: center;
47+
justify-content: space-between;
48+
padding: 0.75rem;
49+
margin-bottom: 0.5rem;
50+
background-color: white;
51+
border-radius: 4px;
52+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
53+
cursor: pointer;
54+
transition: background-color 0.2s ease;
55+
56+
&:hover {
57+
background-color: #f0f0f0;
58+
}
59+
60+
&:last-child {
61+
margin-bottom: 0;
62+
}
63+
}
64+
65+
&__timestamp {
66+
font-size: 0.9rem;
67+
color: #555;
68+
}
69+
70+
&__restore-button {
71+
background-color: transparent;
72+
border: none;
73+
color: #2196f3;
74+
font-size: 0.9rem;
75+
cursor: pointer;
76+
padding: 0.25rem 0.5rem;
77+
border-radius: 4px;
78+
transition: background-color 0.2s ease;
79+
80+
&:hover {
81+
background-color: rgba(33, 150, 243, 0.1);
82+
}
83+
}
84+
85+
&__confirmation {
86+
display: flex;
87+
flex-direction: column;
88+
align-items: center;
89+
justify-content: center;
90+
padding: 1rem;
91+
background-color: white;
92+
border-radius: 4px;
93+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
94+
text-align: center;
95+
}
96+
97+
&__warning {
98+
color: #f44336;
99+
font-weight: 500;
100+
margin: 0.5rem 0 1rem;
101+
}
102+
103+
&__actions {
104+
display: flex;
105+
gap: 1rem;
106+
}
107+
108+
&__button {
109+
padding: 0.5rem 1rem;
110+
border: none;
111+
border-radius: 4px;
112+
font-weight: 500;
113+
cursor: pointer;
114+
transition: background-color 0.2s ease;
115+
116+
&--restore {
117+
background-color: #2196f3;
118+
color: white;
119+
120+
&:hover {
121+
background-color: #1976d2;
122+
}
123+
}
124+
125+
&--cancel {
126+
background-color: #e0e0e0;
127+
color: #333;
128+
129+
&:hover {
130+
background-color: #d5d5d5;
131+
}
132+
}
133+
}
134+
}

src/frontend/src/pad/styles/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
@import './ControlButton.scss';
44
@import './StateIndicator.scss';
55
@import './Dashboard.scss';
6+
@import './CanvasBackups.scss';

0 commit comments

Comments
 (0)