Skip to content

Commit 7e7ed25

Browse files
feat: add backup import and export features
1 parent a273740 commit 7e7ed25

File tree

3 files changed

+260
-42
lines changed

3 files changed

+260
-42
lines changed

encra-client/src/components/chat/chat/AccountModal.jsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Tabs,
1212
Tab,
1313
} from "@mui/material";
14+
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
1415
import LogoutOutlinedIcon from "@mui/icons-material/LogoutOutlined";
1516
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
1617
import { motion } from "framer-motion";
@@ -61,6 +62,26 @@ const AccountModal = ({ onClose }) => {
6162
setCurrentTab(newValue);
6263
};
6364

65+
const handleBackupExport = () => {
66+
const backupData = {};
67+
68+
for (let i = 0; i < localStorage.length; i++) {
69+
const key = localStorage.key(i);
70+
backupData[key] = localStorage.getItem(key);
71+
}
72+
73+
const blob = new Blob([JSON.stringify(backupData, null, 2)], {
74+
type: "application/json",
75+
});
76+
77+
const url = URL.createObjectURL(blob);
78+
const a = document.createElement("a");
79+
a.href = url;
80+
a.download = "encra-backup.json";
81+
a.click();
82+
URL.revokeObjectURL(url);
83+
};
84+
6485
return (
6586
<Box
6687
sx={{
@@ -142,6 +163,21 @@ const AccountModal = ({ onClose }) => {
142163
<Box
143164
sx={{ mt: 2, display: "flex", flexDirection: "column", gap: 2 }}
144165
>
166+
<Button
167+
variant="outlined"
168+
startIcon={<FileDownloadOutlinedIcon />}
169+
onClick={handleBackupExport}
170+
sx={{
171+
justifyContent: "flex-start",
172+
px: 2,
173+
borderRadius: 3,
174+
color: theme.palette.primary.main,
175+
borderColor: theme.palette.primary.main,
176+
}}
177+
>
178+
Export Backup
179+
</Button>
180+
145181
<Button
146182
variant="outlined"
147183
startIcon={<LogoutOutlinedIcon />}

encra-client/src/components/layout/Header.jsx

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useState } from "react";
22
import {
33
AppBar,
44
Toolbar,
@@ -11,55 +11,66 @@ import {
1111
import { Link as RouterLink } from "react-router-dom";
1212
import Brightness4Icon from "@mui/icons-material/Brightness4";
1313
import Brightness7Icon from "@mui/icons-material/Brightness7";
14+
import LocalStorageBackupModal from "./LocalStorageBackupModal";
1415

1516
const Header = ({ toggleTheme }) => {
1617
const theme = useTheme();
18+
const [backupModalOpen, setBackupModalOpen] = useState(false);
19+
20+
const handleOpenBackup = () => setBackupModalOpen(true);
21+
const handleCloseBackup = () => setBackupModalOpen(false);
1722

1823
return (
19-
<AppBar
20-
position="static"
21-
elevation={0}
22-
sx={{
23-
bgcolor: theme.palette.background.paper,
24-
}}
25-
>
26-
<Toolbar
27-
sx={{
28-
display: "flex",
29-
justifyContent: "space-between",
30-
}}
24+
<>
25+
<AppBar
26+
position="static"
27+
elevation={0}
28+
sx={{ bgcolor: theme.palette.background.paper }}
3129
>
32-
<Typography
33-
variant="h6"
34-
component={RouterLink}
35-
to="/"
36-
color="primary"
37-
sx={{
38-
textDecoration: "none",
39-
fontWeight: "bold",
40-
}}
41-
>
42-
ENCRA
43-
</Typography>
30+
<Toolbar sx={{ display: "flex", justifyContent: "space-between" }}>
31+
<Typography
32+
variant="h6"
33+
component={RouterLink}
34+
to="/"
35+
color="primary"
36+
sx={{ textDecoration: "none", fontWeight: "bold" }}
37+
>
38+
ENCRA
39+
</Typography>
40+
41+
<Box sx={{ display: "flex", gap: 2 }}>
42+
<Button component={RouterLink} to="/" color="inherit">
43+
Home
44+
</Button>
45+
<Button component={RouterLink} to="/login" color="inherit">
46+
Login
47+
</Button>
48+
<Button component={RouterLink} to="/register" color="inherit">
49+
Register
50+
</Button>
51+
<Button onClick={handleOpenBackup} color="inherit">
52+
Backup
53+
</Button>
54+
</Box>
4455

45-
<Box sx={{ display: "flex", gap: 2 }}>
46-
<Button component={RouterLink} to="/" color="inherit">
47-
Home
48-
</Button>
49-
<Button component={RouterLink} to="/chat" color="inherit">
50-
Chat
51-
</Button>
52-
</Box>
56+
<IconButton
57+
onClick={toggleTheme}
58+
sx={{ color: theme.palette.primary.main }}
59+
>
60+
{theme.palette.mode === "dark" ? (
61+
<Brightness7Icon />
62+
) : (
63+
<Brightness4Icon />
64+
)}
65+
</IconButton>
66+
</Toolbar>
67+
</AppBar>
5368

54-
<IconButton onClick={toggleTheme} sx={{ color: theme.palette.primary.main }}>
55-
{theme.palette.mode === "dark" ? (
56-
<Brightness7Icon />
57-
) : (
58-
<Brightness4Icon />
59-
)}
60-
</IconButton>
61-
</Toolbar>
62-
</AppBar>
69+
<LocalStorageBackupModal
70+
open={backupModalOpen}
71+
onClose={handleCloseBackup}
72+
/>
73+
</>
6374
);
6475
};
6576

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import React, { useRef, useState } from "react";
2+
import {
3+
Paper,
4+
Typography,
5+
Box,
6+
Button,
7+
ClickAwayListener,
8+
Alert,
9+
} from "@mui/material";
10+
import BackupOutlinedIcon from "@mui/icons-material/BackupOutlined";
11+
import RestorePageOutlinedIcon from "@mui/icons-material/RestorePageOutlined";
12+
import { motion } from "framer-motion";
13+
14+
const LocalStorageBackupModal = ({ open, onClose }) => {
15+
const fileInputRef = useRef(null);
16+
const [error, setError] = useState(null);
17+
const [success, setSuccess] = useState(null);
18+
19+
const handleDownload = () => {
20+
try {
21+
const backupData = {};
22+
for (let i = 0; i < localStorage.length; i++) {
23+
const key = localStorage.key(i);
24+
backupData[key] = localStorage.getItem(key);
25+
}
26+
27+
const blob = new Blob([JSON.stringify(backupData, null, 2)], {
28+
type: "application/json",
29+
});
30+
31+
const url = URL.createObjectURL(blob);
32+
const a = document.createElement("a");
33+
34+
a.href = url;
35+
a.download = "encra-backup.json";
36+
a.click();
37+
URL.revokeObjectURL(url);
38+
} catch {
39+
setError("Failed to create backup file.");
40+
}
41+
};
42+
43+
const handleFileChange = (event) => {
44+
const file = event.target.files?.[0];
45+
if (!file) return;
46+
47+
const reader = new FileReader();
48+
reader.onload = (e) => {
49+
try {
50+
const importedData = JSON.parse(e.target.result);
51+
if (typeof importedData !== "object" || Array.isArray(importedData)) {
52+
throw new Error("Invalid backup format");
53+
}
54+
55+
for (const [key, value] of Object.entries(importedData)) {
56+
localStorage.setItem(key, value);
57+
}
58+
59+
setSuccess("Backup imported successfully. Reloading...");
60+
setTimeout(() => window.location.reload(), 1000);
61+
} catch {
62+
setError("Invalid backup file. Please try again.");
63+
}
64+
};
65+
reader.readAsText(file);
66+
};
67+
68+
const triggerFileInput = () => {
69+
fileInputRef.current?.click();
70+
};
71+
72+
if (!open) return null;
73+
74+
return (
75+
<Box
76+
sx={{
77+
position: "fixed",
78+
top: 0,
79+
left: 0,
80+
height: "100vh",
81+
width: "100vw",
82+
display: "flex",
83+
alignItems: "center",
84+
justifyContent: "center",
85+
zIndex: 1500,
86+
}}
87+
>
88+
<Box
89+
sx={{
90+
position: "absolute",
91+
inset: 0,
92+
backdropFilter: "blur(2px)",
93+
backgroundColor: "rgba(0, 0, 0, 0.2)",
94+
zIndex: -1,
95+
}}
96+
/>
97+
98+
<ClickAwayListener onClickAway={onClose}>
99+
<motion.div
100+
initial={{ opacity: 0, scale: 0.95 }}
101+
animate={{ opacity: 1, scale: 1 }}
102+
exit={{ opacity: 0, scale: 0.95 }}
103+
transition={{ duration: 0.2 }}
104+
>
105+
<Paper
106+
elevation={4}
107+
sx={{
108+
p: 4,
109+
borderRadius: 3,
110+
minWidth: 320,
111+
maxWidth: "90vw",
112+
textAlign: "center",
113+
}}
114+
>
115+
<Typography variant="h6" gutterBottom>
116+
LocalStorage Backup
117+
</Typography>
118+
119+
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
120+
Download a JSON backup of your encrypted localStorage data or
121+
restore it from a previous backup.
122+
</Typography>
123+
124+
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
125+
<Button
126+
variant="contained"
127+
color="primary"
128+
startIcon={<BackupOutlinedIcon />}
129+
onClick={handleDownload}
130+
sx={{ borderRadius: 3 }}
131+
>
132+
Download Backup
133+
</Button>
134+
135+
<Button
136+
variant="outlined"
137+
color="secondary"
138+
startIcon={<RestorePageOutlinedIcon />}
139+
onClick={triggerFileInput}
140+
sx={{ borderRadius: 3 }}
141+
>
142+
Import Backup
143+
</Button>
144+
145+
<input
146+
type="file"
147+
accept="application/json"
148+
ref={fileInputRef}
149+
style={{ display: "none" }}
150+
onChange={handleFileChange}
151+
/>
152+
153+
{error && (
154+
<Alert severity="error" onClose={() => setError(null)}>
155+
{error}
156+
</Alert>
157+
)}
158+
{success && <Alert severity="success">{success}</Alert>}
159+
</Box>
160+
161+
<Button onClick={onClose} sx={{ mt: 3, borderRadius: 3 }} fullWidth>
162+
Close
163+
</Button>
164+
</Paper>
165+
</motion.div>
166+
</ClickAwayListener>
167+
</Box>
168+
);
169+
};
170+
171+
export default LocalStorageBackupModal;

0 commit comments

Comments
 (0)