Skip to content

Commit dc20dc9

Browse files
author
Lauren Pothuru
committed
Implement frontend for the folder system
- Implement folder page to display user folders - Add folder card component for folder representation - Added 'add to folder' button in apartment page - Debugged authentication issues in folder operations
1 parent 9799fa1 commit dc20dc9

File tree

8 files changed

+1137
-9
lines changed

8 files changed

+1137
-9
lines changed

backend/src/app.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,7 +1764,6 @@ app.get('/api/folders', authenticate, async (req, res) => {
17641764
try {
17651765
if (!req.user) throw new Error('Not authenticated');
17661766
const { uid } = req.user;
1767-
17681767
// Fetch all folders for this user
17691768
const folderSnapshot = await folderCollection.where('userId', '==', uid).get();
17701769

@@ -1834,11 +1833,12 @@ app.put('/api/folders/:folderId', authenticate, async (req, res) => {
18341833
});
18351834

18361835
// Endpoint to add an apartment to a folder
1837-
app.post('/api/folders/:id/apartments/:apartmentId', authenticate, async (req, res) => {
1836+
app.post('/api/folders/:folderId/apartments', authenticate, async (req, res) => {
18381837
try {
18391838
if (!req.user) throw new Error('Not authenticated');
18401839
const { uid } = req.user;
1841-
const { folderId, aptId } = req.body;
1840+
const { folderId } = req.params;
1841+
const { aptId } = req.body;
18421842

18431843
const folderRef = folderCollection.doc(folderId);
18441844
const folderDoc = await folderRef.get();
@@ -1866,11 +1866,11 @@ app.post('/api/folders/:id/apartments/:apartmentId', authenticate, async (req, r
18661866
});
18671867

18681868
// Endpoint to remove an apartment from a folder
1869-
app.post('/api/folders/:id/apartments/:apartmentId', authenticate, async (req, res) => {
1869+
app.delete('/api/folders/:folderId/apartments/:apartmentId', authenticate, async (req, res) => {
18701870
try {
18711871
if (!req.user) throw new Error('Not authenticated');
18721872
const { uid } = req.user;
1873-
const { folderId, aptId } = req.body;
1873+
const { folderId, apartmentId } = req.params;
18741874

18751875
const folderRef = folderCollection.doc(folderId);
18761876
const folderDoc = await folderRef.get();
@@ -1884,7 +1884,7 @@ app.post('/api/folders/:id/apartments/:apartmentId', authenticate, async (req, r
18841884
}
18851885

18861886
let apartments = folderDoc.data()?.apartments || [];
1887-
apartments = apartments.filter((id: string) => id !== aptId);
1887+
apartments = apartments.filter((id: string) => id !== apartmentId);
18881888
await folderRef.update({ apartments });
18891889
return res.status(200).send('Apartment removed from folder successfully');
18901890
} catch (err) {
@@ -1893,4 +1893,30 @@ app.post('/api/folders/:id/apartments/:apartmentId', authenticate, async (req, r
18931893
}
18941894
});
18951895

1896+
// Endpoint to get all apartments in a folder
1897+
app.get('/api/folders/:folderId/apartments', authenticate, async (req, res) => {
1898+
try {
1899+
if (!req.user) throw new Error('Not authenticated');
1900+
const { uid } = req.user;
1901+
const { folderId } = req.params;
1902+
1903+
const folderRef = folderCollection.doc(folderId);
1904+
const folderDoc = await folderRef.get();
1905+
1906+
if (!folderDoc.exists) {
1907+
return res.status(404).send('Folder not found');
1908+
}
1909+
1910+
if (folderDoc.data()?.userId !== uid) {
1911+
return res.status(403).send('Unauthorized to access this folder');
1912+
}
1913+
1914+
const apartments = folderDoc.data()?.apartments || [];
1915+
return res.status(200).json(apartments);
1916+
} catch (err) {
1917+
console.error(err);
1918+
return res.status(500).send('Error fetching apartments from folder');
1919+
}
1920+
});
1921+
18961922
export default app;

frontend/src/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import FAQPage from './pages/FAQPage';
66
import ReviewPage from './pages/ReviewPage';
77
import LandlordPage from './pages/LandlordPage';
88
import ProfilePage from './pages/ProfilePage';
9+
import FolderPage from './pages/FolderPage';
10+
import FolderDetailPage from './pages/FolderDetailPage';
911
import BookmarksPage from './pages/BookmarksPage';
1012
import { ThemeProvider } from '@material-ui/core';
1113
import { createTheme } from '@material-ui/core/styles';
@@ -137,6 +139,15 @@ const App = (): ReactElement => {
137139
path="/bookmarks"
138140
component={() => <BookmarksPage user={user} setUser={setUser} />}
139141
/>
142+
<Route
143+
exact
144+
path="/folders"
145+
component={() => <FolderPage user={user} setUser={setUser} />}
146+
/>
147+
<Route
148+
path="/folders/:folderId"
149+
component={() => <FolderDetailPage user={user} setUser={setUser} />}
150+
/>
140151
<Route
141152
path="/apartment/:aptId"
142153
component={() => <ApartmentPage user={user} setUser={setUser} />}
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import React, { useState, useEffect } from 'react';
2+
import {
3+
Dialog,
4+
DialogTitle,
5+
DialogContent,
6+
DialogActions,
7+
Button,
8+
List,
9+
ListItem,
10+
ListItemText,
11+
Checkbox,
12+
TextField,
13+
Typography,
14+
Box,
15+
CircularProgress,
16+
} from '@material-ui/core';
17+
import { makeStyles } from '@material-ui/core/styles';
18+
import AddIcon from '@material-ui/icons/Add';
19+
import axios from 'axios';
20+
import { getUser, createAuthHeaders } from '../../utils/firebase';
21+
import { colors } from '../../colors';
22+
23+
const useStyles = makeStyles({
24+
dialogContent: {
25+
minHeight: '300px',
26+
maxHeight: '400px',
27+
},
28+
folderItem: {
29+
border: `1px solid ${colors.gray3}`,
30+
borderRadius: '8px',
31+
marginBottom: '8px',
32+
'&:hover': {
33+
backgroundColor: colors.gray3,
34+
},
35+
},
36+
createFolderSection: {
37+
padding: '16px',
38+
backgroundColor: colors.gray3,
39+
borderRadius: '8px',
40+
marginTop: '16px',
41+
},
42+
addButton: {
43+
backgroundColor: colors.red1,
44+
color: 'white',
45+
'&:hover': {
46+
backgroundColor: colors.red2,
47+
},
48+
},
49+
});
50+
51+
type Folder = {
52+
id: string;
53+
name: string;
54+
userId: string;
55+
createdAt: any;
56+
apartments?: string[];
57+
};
58+
59+
type Props = {
60+
open: boolean;
61+
onClose: () => void;
62+
apartmentId: string;
63+
apartmentName: string;
64+
user: firebase.User | null;
65+
setUser: React.Dispatch<React.SetStateAction<firebase.User | null>>;
66+
onSuccess: () => void;
67+
};
68+
69+
const AddToFolderModal = ({
70+
open,
71+
onClose,
72+
apartmentId,
73+
apartmentName,
74+
user,
75+
setUser,
76+
onSuccess,
77+
}: Props) => {
78+
const classes = useStyles();
79+
const [folders, setFolders] = useState<Folder[]>([]);
80+
const [selectedFolders, setSelectedFolders] = useState<Set<string>>(new Set());
81+
const [loading, setLoading] = useState(false);
82+
const [showCreateNew, setShowCreateNew] = useState(false);
83+
const [newFolderName, setNewFolderName] = useState('');
84+
const [error, setError] = useState('');
85+
86+
useEffect(() => {
87+
if (open) {
88+
fetchFolders();
89+
}
90+
}, [open]);
91+
92+
const fetchFolders = async () => {
93+
try {
94+
setLoading(true);
95+
if (!user) {
96+
const loggedInUser = await getUser(true);
97+
setUser(loggedInUser);
98+
if (!loggedInUser) return;
99+
}
100+
const token = await user!.getIdToken(true);
101+
const response = await axios.get('/api/folders', createAuthHeaders(token));
102+
setFolders(response.data);
103+
104+
// Pre-select folders that already contain this apartment
105+
const preSelected = new Set<string>();
106+
response.data.forEach((folder: Folder) => {
107+
if (folder.apartments?.includes(apartmentId)) {
108+
preSelected.add(folder.id);
109+
}
110+
});
111+
setSelectedFolders(preSelected);
112+
} catch (err) {
113+
console.error('Error fetching folders:', err);
114+
setError('Failed to load folders');
115+
} finally {
116+
setLoading(false);
117+
}
118+
};
119+
120+
const handleToggleFolder = (folderId: string) => {
121+
setSelectedFolders((prev) => {
122+
const newSet = new Set(prev);
123+
if (newSet.has(folderId)) {
124+
newSet.delete(folderId);
125+
} else {
126+
newSet.add(folderId);
127+
}
128+
return newSet;
129+
});
130+
};
131+
132+
const handleCreateFolder = async () => {
133+
if (!newFolderName.trim()) {
134+
setError('Folder name cannot be empty');
135+
return;
136+
}
137+
try {
138+
if (!user) {
139+
const loggedInUser = await getUser(true);
140+
setUser(loggedInUser);
141+
if (!loggedInUser) return;
142+
}
143+
const token = await user!.getIdToken(true);
144+
const response = await axios.post(
145+
'/api/folders',
146+
{ folderName: newFolderName },
147+
createAuthHeaders(token)
148+
);
149+
const newFolder = response.data;
150+
setFolders([...folders, newFolder]);
151+
setSelectedFolders((prev) => new Set(prev).add(newFolder.id));
152+
setNewFolderName('');
153+
setShowCreateNew(false);
154+
} catch (err) {
155+
console.error('Error creating folder:', err);
156+
setError('Failed to create folder');
157+
}
158+
};
159+
160+
const handleSave = async () => {
161+
try {
162+
setLoading(true);
163+
if (!user) {
164+
const loggedInUser = await getUser(true);
165+
setUser(loggedInUser);
166+
if (!loggedInUser) return;
167+
}
168+
const token = await user!.getIdToken(true);
169+
170+
// Determine which folders need to be added/removed
171+
const currentFolders = folders.filter((f) => f.apartments?.includes(apartmentId));
172+
const currentFolderIds = new Set(currentFolders.map((f) => f.id));
173+
174+
// Add apartment to newly selected folders
175+
for (const folderId of selectedFolders) {
176+
if (!currentFolderIds.has(folderId)) {
177+
await axios.post(
178+
`/api/folders/${folderId}/apartments`,
179+
{ aptId: apartmentId },
180+
createAuthHeaders(token)
181+
);
182+
}
183+
}
184+
185+
// Remove apartment from deselected folders
186+
for (const folder of currentFolders) {
187+
if (!selectedFolders.has(folder.id)) {
188+
await axios.delete(
189+
`/api/folders/${folder.id}/apartments/${apartmentId}`,
190+
createAuthHeaders(token)
191+
);
192+
}
193+
}
194+
195+
onSuccess();
196+
onClose();
197+
} catch (err) {
198+
console.error('Error updating folders:', err);
199+
setError('Failed to update folders');
200+
} finally {
201+
setLoading(false);
202+
}
203+
};
204+
205+
return (
206+
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
207+
<DialogTitle>Add "{apartmentName}" to Folder</DialogTitle>
208+
<DialogContent className={classes.dialogContent}>
209+
{loading && folders.length === 0 ? (
210+
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
211+
<CircularProgress />
212+
</Box>
213+
) : folders.length === 0 ? (
214+
<Box textAlign="center" py={4}>
215+
<Typography color="textSecondary">No folders yet. Create one below!</Typography>
216+
</Box>
217+
) : (
218+
<List>
219+
{folders.map((folder) => (
220+
<ListItem
221+
key={folder.id}
222+
button
223+
onClick={() => handleToggleFolder(folder.id)}
224+
className={classes.folderItem}
225+
>
226+
<Checkbox
227+
checked={selectedFolders.has(folder.id)}
228+
onChange={() => handleToggleFolder(folder.id)}
229+
color="primary"
230+
/>
231+
<ListItemText
232+
primary={folder.name}
233+
secondary={`${folder.apartments?.length || 0} apartments`}
234+
/>
235+
</ListItem>
236+
))}
237+
</List>
238+
)}
239+
240+
{error && (
241+
<Typography color="error" variant="body2" style={{ marginTop: '8px' }}>
242+
{error}
243+
</Typography>
244+
)}
245+
246+
<Box className={classes.createFolderSection}>
247+
{!showCreateNew ? (
248+
<Button
249+
startIcon={<AddIcon />}
250+
onClick={() => setShowCreateNew(true)}
251+
fullWidth
252+
variant="outlined"
253+
>
254+
Create New Folder
255+
</Button>
256+
) : (
257+
<Box>
258+
<TextField
259+
autoFocus
260+
fullWidth
261+
label="New Folder Name"
262+
value={newFolderName}
263+
onChange={(e) => setNewFolderName(e.target.value)}
264+
onKeyPress={(e) => {
265+
if (e.key === 'Enter') {
266+
handleCreateFolder();
267+
}
268+
}}
269+
variant="outlined"
270+
size="small"
271+
/>
272+
<Box display="flex" mt={1} style={{ gap: 8 }}>
273+
<Button onClick={handleCreateFolder} className={classes.addButton} size="small">
274+
Create
275+
</Button>
276+
<Button onClick={() => setShowCreateNew(false)} size="small">
277+
Cancel
278+
</Button>
279+
</Box>
280+
</Box>
281+
)}
282+
</Box>
283+
</DialogContent>
284+
<DialogActions>
285+
<Button onClick={onClose}>Cancel</Button>
286+
<Button onClick={handleSave} color="primary" disabled={loading}>
287+
{loading ? <CircularProgress size={24} /> : 'Save'}
288+
</Button>
289+
</DialogActions>
290+
</Dialog>
291+
);
292+
};
293+
294+
export default AddToFolderModal;

0 commit comments

Comments
 (0)