Skip to content

Commit a9a9c01

Browse files
committed
feat: implement BackupsModal component and styles
- Added BackupsModal component to display and manage canvas backups. - Integrated state management for modal visibility and exit animations. - Created corresponding SCSS styles for the modal, ensuring a cohesive design. - Updated MainMenu to include the BackupsModal, enhancing user experience with backup management options.
1 parent d9fc23f commit a9a9c01

File tree

3 files changed

+368
-15
lines changed

3 files changed

+368
-15
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
.backups-modal {
2+
&__content {
3+
padding: 20px;
4+
max-height: 80vh;
5+
overflow-y: auto;
6+
}
7+
8+
&__header {
9+
display: flex;
10+
justify-content: space-between;
11+
align-items: center;
12+
margin-bottom: 1rem;
13+
}
14+
15+
&__close-button {
16+
background: none;
17+
border: none;
18+
color: #ffffff;
19+
font-size: 1.5rem;
20+
cursor: pointer;
21+
padding: 0;
22+
width: 30px;
23+
height: 30px;
24+
display: flex;
25+
align-items: center;
26+
justify-content: center;
27+
border-radius: 50%;
28+
transition: background-color 0.2s ease;
29+
30+
&:hover {
31+
background-color: rgba(255, 255, 255, 0.1);
32+
}
33+
}
34+
35+
&__title {
36+
margin: 0 0 1rem;
37+
font-size: 1.2rem;
38+
font-weight: 500;
39+
color: #ffffff;
40+
text-align: center;
41+
opacity: 0.9;
42+
}
43+
44+
&__loading,
45+
&__error,
46+
&__empty {
47+
display: flex;
48+
align-items: center;
49+
justify-content: center;
50+
padding: 2rem;
51+
color: #ffffff;
52+
font-style: italic;
53+
opacity: 0.9;
54+
animation: fadeIn 0.5s ease-in-out;
55+
}
56+
57+
&__error {
58+
color: #f44336;
59+
}
60+
61+
&__list {
62+
list-style: none;
63+
padding: 0;
64+
margin: 0;
65+
max-height: 60vh;
66+
overflow-y: auto;
67+
}
68+
69+
&__item {
70+
display: flex;
71+
align-items: center;
72+
justify-content: space-between;
73+
padding: 12px 15px;
74+
margin-bottom: 8px;
75+
background-color: #32373c;
76+
border-radius: 10px;
77+
cursor: pointer;
78+
transition: all 0.3s ease;
79+
position: relative;
80+
overflow: hidden;
81+
82+
&::after {
83+
content: '';
84+
position: absolute;
85+
top: 0;
86+
left: 0;
87+
right: 0;
88+
bottom: 0;
89+
background-color: rgba(255, 255, 255, 0);
90+
transition: background-color 0.3s ease;
91+
pointer-events: none;
92+
border-radius: 10px;
93+
}
94+
95+
&:hover::after {
96+
background-color: rgba(255, 255, 255, 0.1);
97+
}
98+
99+
&:active::after {
100+
background-color: rgba(255, 255, 255, 0.05);
101+
}
102+
103+
&:last-child {
104+
margin-bottom: 0;
105+
}
106+
}
107+
108+
&__item-content {
109+
display: flex;
110+
align-items: center;
111+
gap: 10px;
112+
}
113+
114+
&__number {
115+
font-size: 0.9rem;
116+
font-weight: 600;
117+
color: #cc6d24;
118+
background-color: rgba(106, 122, 255, 0.1);
119+
padding: 4px 8px;
120+
border-radius: 4px;
121+
min-width: 28px;
122+
text-align: center;
123+
}
124+
125+
&__timestamp {
126+
font-size: 0.9rem;
127+
color: #ffffff;
128+
opacity: 0.9;
129+
}
130+
131+
&__restore-button {
132+
background-color: transparent;
133+
border: none;
134+
color: #cc6d24;
135+
font-size: 0.9rem;
136+
cursor: pointer;
137+
padding: 0.25rem 0.5rem;
138+
border-radius: 4px;
139+
transition: all 0.2s ease;
140+
141+
&:hover {
142+
background-color: rgba(106, 122, 255, 0.1);
143+
}
144+
}
145+
146+
&__confirmation {
147+
display: flex;
148+
flex-direction: column;
149+
align-items: center;
150+
justify-content: center;
151+
padding: 20px;
152+
background-color: #32373c;
153+
border-radius: 10px;
154+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
155+
text-align: center;
156+
color: #ffffff;
157+
animation: fadeIn 0.4s ease-in-out;
158+
}
159+
160+
&__warning {
161+
color: #f44336;
162+
font-weight: 500;
163+
margin: 0.5rem 0 1rem;
164+
}
165+
166+
&__actions {
167+
display: flex;
168+
gap: 1rem;
169+
}
170+
171+
&__button {
172+
padding: 10px 16px;
173+
border: none;
174+
border-radius: 7px;
175+
font-weight: 500;
176+
cursor: pointer;
177+
transition: all 0.3s ease;
178+
position: relative;
179+
overflow: hidden;
180+
181+
&::after {
182+
content: '';
183+
position: absolute;
184+
top: 0;
185+
left: 0;
186+
right: 0;
187+
bottom: 0;
188+
background-color: rgba(255, 255, 255, 0);
189+
transition: background-color 0.3s ease;
190+
pointer-events: none;
191+
border-radius: 7px;
192+
}
193+
194+
&:hover::after {
195+
background-color: rgba(255, 255, 255, 0.1);
196+
}
197+
198+
&:active::after {
199+
background-color: rgba(255, 255, 255, 0.05);
200+
}
201+
202+
&--restore {
203+
background-color: #cc6d24;
204+
border: 1px solid #cecece00;
205+
color: white;
206+
207+
&:hover {
208+
border: 1px solid #cecece;
209+
}
210+
}
211+
212+
&--cancel {
213+
background-color: #4a4a54;
214+
color: #ffffff;
215+
216+
&:hover {
217+
background-color: #3a3a44;
218+
}
219+
}
220+
}
221+
}
222+
223+
@keyframes fadeIn {
224+
from { opacity: 0; transform: translateY(10px); }
225+
to { opacity: 1; transform: translateY(0); }
226+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useState } from "react";
2+
import Modal from "./Modal";
3+
import { useCanvasBackups, CanvasBackup } from "../api/hooks";
4+
import "../styles/BackupsModal.scss";
5+
6+
interface BackupsModalProps {
7+
excalidrawAPI?: any;
8+
isExiting?: boolean;
9+
onExitComplete?: () => void;
10+
onClose?: () => void;
11+
}
12+
13+
const BackupsModal: React.FC<BackupsModalProps> = ({
14+
excalidrawAPI,
15+
isExiting = false,
16+
onExitComplete,
17+
onClose,
18+
}) => {
19+
const { data, isLoading, error } = useCanvasBackups();
20+
const [selectedBackup, setSelectedBackup] = useState<CanvasBackup | null>(null);
21+
22+
// Functions from CanvasBackups.tsx
23+
const handleBackupSelect = (backup: CanvasBackup) => {
24+
setSelectedBackup(backup);
25+
};
26+
27+
const handleRestoreBackup = () => {
28+
if (selectedBackup && excalidrawAPI) {
29+
// Load the backup data into the canvas
30+
excalidrawAPI.updateScene(selectedBackup.data);
31+
setSelectedBackup(null);
32+
}
33+
};
34+
35+
const handleCancel = () => {
36+
setSelectedBackup(null);
37+
};
38+
39+
// Format date function from CanvasBackups.tsx
40+
const formatDate = (dateString: string): string => {
41+
const date = new Date(dateString);
42+
return date.toLocaleString(undefined, {
43+
year: 'numeric',
44+
month: 'short',
45+
day: 'numeric',
46+
hour: '2-digit',
47+
minute: '2-digit'
48+
});
49+
};
50+
51+
return (
52+
<Modal
53+
logoSrc="/assets/images/favicon.png"
54+
logoAlt="pad.ws logo"
55+
className="backups-modal"
56+
isExiting={isExiting}
57+
onExitComplete={onExitComplete}
58+
>
59+
<div className="backups-modal__content">
60+
<div className="backups-modal__header">
61+
<h2 className="backups-modal__title">Canvas Backups</h2>
62+
<button
63+
className="backups-modal__close-button"
64+
onClick={onClose}
65+
aria-label="Close"
66+
>
67+
×
68+
</button>
69+
</div>
70+
71+
{isLoading ? (
72+
<div className="backups-modal__loading">Loading backups...</div>
73+
) : error ? (
74+
<div className="backups-modal__error">Error loading backups</div>
75+
) : !data || data.backups.length === 0 ? (
76+
<div className="backups-modal__empty">No backups available</div>
77+
) : selectedBackup ? (
78+
<div className="backups-modal__confirmation">
79+
<p>Restore canvas from backup #{data.backups.findIndex(b => b.id === selectedBackup.id) + 1} created on {formatDate(selectedBackup.timestamp)}?</p>
80+
<p className="backups-modal__warning">This will replace your current canvas!</p>
81+
<div className="backups-modal__actions">
82+
<button
83+
className="backups-modal__button backups-modal__button--restore"
84+
onClick={handleRestoreBackup}
85+
>
86+
Restore
87+
</button>
88+
<button
89+
className="backups-modal__button backups-modal__button--cancel"
90+
onClick={handleCancel}
91+
>
92+
Cancel
93+
</button>
94+
</div>
95+
</div>
96+
) : (
97+
<ul className="backups-modal__list">
98+
{data.backups.map((backup, index) => (
99+
<li
100+
key={backup.id}
101+
className="backups-modal__item"
102+
onClick={() => handleBackupSelect(backup)}
103+
>
104+
<div className="backups-modal__item-content">
105+
<span className="backups-modal__number">#{index + 1}</span>
106+
<span className="backups-modal__timestamp">{formatDate(backup.timestamp)}</span>
107+
</div>
108+
<button className="backups-modal__restore-button">Restore</button>
109+
</li>
110+
))}
111+
</ul>
112+
)}
113+
</div>
114+
</Modal>
115+
);
116+
};
117+
118+
export default BackupsModal;

0 commit comments

Comments
 (0)