Skip to content

Commit acdec9e

Browse files
committed
add server/routes and client/src/components/ImportExportDialog.tsx
1 parent 1ae02b3 commit acdec9e

2 files changed

Lines changed: 220 additions & 24 deletions

File tree

client/src/components/ImportExportDialog.tsx

Lines changed: 138 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Label } from "@/components/ui/label";
88
import { useToast } from "@/hooks/use-toast";
99
import { queryClient } from "@/lib/queryClient";
1010
import { UploadCloud, Download, AlertCircle } from "lucide-react";
11+
import { getAuth } from "firebase/auth";
12+
import { useAuthContext } from "@/contexts/AuthContext";
1113

1214
interface ImportExportDialogProps {
1315
open: boolean;
@@ -20,6 +22,7 @@ export default function ImportExportDialog({
2022
}: ImportExportDialogProps) {
2123
const { toast } = useToast();
2224
const queryClient = useQueryClient();
25+
const { user } = useAuthContext();
2326
const [activeTab, setActiveTab] = useState("import");
2427
const [importData, setImportData] = useState("");
2528
const [isLoading, setIsLoading] = useState(false);
@@ -31,6 +34,37 @@ export default function ImportExportDialog({
3134
setIsLoading(true);
3235
setError(null);
3336

37+
// Ensure user is authenticated
38+
if (!user) {
39+
setError("You must be logged in to import snippets.");
40+
setIsLoading(false);
41+
return;
42+
}
43+
44+
// Get Firebase auth instance and current user
45+
const auth = getAuth();
46+
const firebaseUser = auth.currentUser;
47+
48+
// Check if Firebase user exists
49+
if (!firebaseUser) {
50+
console.error("Firebase user not found but local user exists");
51+
setError("Authentication error. Please try logging in again.");
52+
setIsLoading(false);
53+
return;
54+
}
55+
56+
// Get id token
57+
let token;
58+
try {
59+
token = await firebaseUser.getIdToken(true);
60+
console.log("Got Firebase ID token for API request");
61+
} catch (tokenError) {
62+
console.error("Failed to get ID token:", tokenError);
63+
setError("Authentication token error. Please refresh the page and try again.");
64+
setIsLoading(false);
65+
return;
66+
}
67+
3468
// Validate JSON format
3569
let snippetsData;
3670
try {
@@ -42,25 +76,51 @@ export default function ImportExportDialog({
4276
}
4377

4478
// Ensure snippets is an array
45-
const snippets = Array.isArray(snippetsData) ? snippetsData : [snippetsData];
79+
const rawSnippets = Array.isArray(snippetsData) ? snippetsData : [snippetsData];
4680

47-
// Send to API
81+
// Clean the snippets to remove any ID fields and auto-generated fields
82+
const cleanSnippets = rawSnippets.map(snippet => {
83+
// Only keep the fields we want to import
84+
return {
85+
title: snippet.title || "Untitled Snippet",
86+
code: snippet.code || "",
87+
language: snippet.language || "text",
88+
description: snippet.description || "",
89+
tags: Array.isArray(snippet.tags) ? snippet.tags : [],
90+
// User ID will be set on the server side from the auth token
91+
};
92+
});
93+
94+
console.log(`Importing ${cleanSnippets.length} snippets...`);
95+
96+
// Send to API with auth token
4897
const response = await fetch("/api/snippets/import", {
4998
method: "POST",
5099
headers: {
51-
"Content-Type": "application/json"
100+
"Content-Type": "application/json",
101+
"Authorization": `Bearer ${token}`
52102
},
53-
body: JSON.stringify({ snippets })
103+
body: JSON.stringify({ snippets: cleanSnippets })
54104
});
55105

106+
// Handle non-2xx responses
56107
if (!response.ok) {
57-
throw new Error("Failed to import snippets");
108+
const errorText = await response.text();
109+
console.error("Import response error:", response.status, errorText);
110+
throw new Error(`Server returned ${response.status}: ${response.statusText || errorText}`);
58111
}
59112

60113
const result = await response.json();
114+
console.log("Import successful:", result);
115+
116+
// Refresh snippets data - try different query keys
117+
queryClient.invalidateQueries({ queryKey: ['snippets'] });
118+
queryClient.invalidateQueries({ queryKey: ['/api/snippets'] });
61119

62-
// Refresh snippets data
63-
queryClient.invalidateQueries({ queryKey: ["/api/snippets"] });
120+
// Force a complete refresh after a short delay
121+
setTimeout(() => {
122+
window.location.reload();
123+
}, 1000);
64124

65125
// Show success message
66126
toast({
@@ -70,25 +130,75 @@ export default function ImportExportDialog({
70130

71131
// Close dialog
72132
onOpenChange(false);
73-
} catch (err) {
74-
setError("Failed to import snippets. Please try again.");
133+
} catch (err: any) {
134+
const errorMessage = err.message || "Failed to import snippets";
135+
setError(`${errorMessage}. Please try again.`);
75136
console.error("Import error:", err);
76137
} finally {
77138
setIsLoading(false);
78139
}
79140
};
80141

81142
// Handle Export
82-
const handleExport = () => {
143+
const handleExport = async () => {
83144
try {
84-
// Create a download link and trigger it
145+
setIsLoading(true);
146+
147+
// Ensure user is authenticated
148+
if (!user) {
149+
setError("You must be logged in to export snippets.");
150+
setIsLoading(false);
151+
return;
152+
}
153+
154+
// Get Firebase auth instance and current user
155+
const auth = getAuth();
156+
const firebaseUser = auth.currentUser;
157+
158+
if (!firebaseUser) {
159+
setError("Authentication error. Please try logging in again.");
160+
setIsLoading(false);
161+
return;
162+
}
163+
164+
// Get id token
165+
const token = await firebaseUser.getIdToken(true);
166+
167+
// Create a download with authenticated request
85168
const exportUrl = "/api/snippets/export";
169+
170+
// For authenticated downloads, we need to use fetch with credentials
171+
// and then create a blob URL from the response
172+
const response = await fetch(exportUrl, {
173+
method: "GET",
174+
headers: {
175+
"Authorization": `Bearer ${token}`
176+
}
177+
});
178+
179+
if (!response.ok) {
180+
throw new Error(`Export failed: ${response.statusText}`);
181+
}
182+
183+
// Get the response data as JSON
184+
const snippetsData = await response.json();
185+
186+
// Create a blob URL
187+
const blob = new Blob([JSON.stringify(snippetsData, null, 2)], {
188+
type: "application/json"
189+
});
190+
const blobUrl = URL.createObjectURL(blob);
191+
192+
// Create a download link and trigger it
86193
const link = document.createElement("a");
87-
link.href = exportUrl;
88-
link.download = "codepatchwork-snippets.json";
194+
link.href = blobUrl;
195+
link.download = `codepatchwork-snippets-${new Date().toISOString().slice(0, 10)}.json`;
89196
document.body.appendChild(link);
90197
link.click();
198+
199+
// Clean up
91200
document.body.removeChild(link);
201+
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
92202

93203
// Show success toast
94204
toast({
@@ -98,9 +208,12 @@ export default function ImportExportDialog({
98208

99209
// Close dialog
100210
onOpenChange(false);
101-
} catch (err) {
102-
setError("Failed to export snippets. Please try again.");
211+
} catch (err: any) {
212+
const errorMessage = err.message || "Failed to export snippets";
213+
setError(`${errorMessage}. Please try again.`);
103214
console.error("Export error:", err);
215+
} finally {
216+
setIsLoading(false);
104217
}
105218
};
106219

@@ -177,6 +290,13 @@ export default function ImportExportDialog({
177290
Download all your snippets as a JSON file that you can import later or share with others.
178291
</p>
179292
</div>
293+
294+
{error && (
295+
<div className="bg-destructive/10 text-destructive p-3 rounded-md flex items-start">
296+
<AlertCircle className="h-5 w-5 mr-2 mt-0.5 flex-shrink-0" />
297+
<p className="text-sm">{error}</p>
298+
</div>
299+
)}
180300
</div>
181301
</TabsContent>
182302
</Tabs>
@@ -191,12 +311,12 @@ export default function ImportExportDialog({
191311
{isLoading ? "Importing..." : "Import Snippets"}
192312
</Button>
193313
) : (
194-
<Button onClick={handleExport}>
195-
Export Snippets
314+
<Button onClick={handleExport} disabled={isLoading}>
315+
{isLoading ? "Exporting..." : "Export Snippets"}
196316
</Button>
197317
)}
198318
</DialogFooter>
199319
</DialogContent>
200320
</Dialog>
201321
);
202-
}
322+
}

server/routes.ts

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,22 +140,98 @@ export async function registerRoutes(app: Express): Promise<Server> {
140140
}
141141
});
142142

143-
app.get("/api/snippets/export", async (req, res) => {
143+
// ENHANCED EXPORT ENDPOINT
144+
app.get("/api/snippets/export", authMiddleware, async (req, res) => {
144145
try {
145-
let list;
146+
// Get all snippets for this user
147+
let allSnippets;
146148
try {
147-
list = await simpleStorage.getSnippets({});
149+
allSnippets = await simpleStorage.getSnippets({});
148150
} catch {
149-
list = await storage.getSnippets({});
151+
allSnippets = await storage.getSnippets({});
150152
}
153+
154+
const userSnippets = allSnippets.filter(s => s.userId === (req as any).user.id);
155+
156+
// Format for export (only include relevant fields that would be needed for import)
157+
const exportSnippets = userSnippets.map(snippet => ({
158+
title: snippet.title,
159+
code: snippet.code,
160+
language: snippet.language,
161+
description: snippet.description,
162+
tags: snippet.tags,
163+
isFavorite: snippet.isFavorite,
164+
isPublic: snippet.isPublic
165+
// Exclude id, userId, viewCount, shareId, timestamps
166+
}));
167+
151168
res.setHeader("Content-Type", "application/json");
152-
res.setHeader("Content-Disposition", `attachment; filename="snippets.json"`);
153-
res.json(list);
169+
res.setHeader("Content-Disposition", `attachment; filename="snippets-${new Date().toISOString().slice(0, 10)}.json"`);
170+
res.json(exportSnippets);
154171
} catch (err) {
155172
console.error("[EXPORT] GET /api/snippets/export error:", err);
156173
res.status(500).json({ message: "Failed to export snippets", error: err.toString() });
157174
}
158175
});
176+
177+
// NEW IMPORT ENDPOINT
178+
app.post("/api/snippets/import", authMiddleware, async (req, res) => {
179+
try {
180+
const { snippets } = req.body;
181+
182+
if (!Array.isArray(snippets)) {
183+
return res.status(400).json({
184+
message: "Invalid input: snippets must be an array"
185+
});
186+
}
187+
188+
const importedSnippets = [];
189+
const userId = (req as any).user.id;
190+
191+
for (const snippetData of snippets) {
192+
try {
193+
// Ensure required fields are present
194+
if (!snippetData.title || !snippetData.code) {
195+
console.error("[IMPORT] Skipping snippet due to missing required fields:", snippetData.title || "Untitled");
196+
continue;
197+
}
198+
199+
// Format the snippet to match our database schema
200+
const formattedSnippet = {
201+
title: snippetData.title,
202+
code: snippetData.code,
203+
language: snippetData.language || null,
204+
description: snippetData.description || null,
205+
tags: Array.isArray(snippetData.tags) ? snippetData.tags : null,
206+
userId: userId,
207+
isFavorite: typeof snippetData.isFavorite === 'boolean' ? snippetData.isFavorite : false,
208+
isPublic: typeof snippetData.isPublic === 'boolean' ? snippetData.isPublic : false
209+
};
210+
211+
// Validate with schema
212+
const validatedSnippet = insertSnippetSchema.parse(formattedSnippet);
213+
214+
// Create the snippet
215+
const createdSnippet = await storage.createSnippet(validatedSnippet);
216+
importedSnippets.push(createdSnippet);
217+
} catch (snippetError) {
218+
console.error("[IMPORT] Error importing snippet:", snippetError);
219+
// Continue with other snippets even if one fails
220+
}
221+
}
222+
223+
res.status(201).json({
224+
message: `Successfully imported ${importedSnippets.length} snippets.`,
225+
snippets: importedSnippets
226+
});
227+
} catch (err: any) {
228+
console.error("[IMPORT] POST /api/snippets/import error:", err);
229+
res.status(500).json({
230+
message: "Failed to import snippets",
231+
error: err.message
232+
});
233+
}
234+
});
159235

160236
app.get("/api/snippets/:id", async (req, res) => {
161237
try {

0 commit comments

Comments
 (0)