├── server
├── .gitignore
├── routes
│ ├── UserRoute.js
│ ├── MatchRoute.js
│ ├── ProtectedRoute.js
│ ├── CloudinaryRoute.js
│ ├── ReportRoute.js
│ ├── FoundRoute.js
│ ├── AuthRoutes.js
│ └── Mailroute.js
├── config
│ ├── firebaseConfig.js
│ └── CloudinaryConfig.js
├── middleware
│ └── authmiddle.js
├── package.json
├── app.js
└── controllers
│ ├── nearby.js
│ ├── UserControl.js
│ └── matchControl.js
├── client
├── versal.json
├── src
│ ├── assets
│ │ ├── ReturnIt.png
│ │ └── badges.jsx
│ ├── App.css
│ ├── main.jsx
│ ├── components
│ │ ├── loading.jsx
│ │ ├── spin.jsx
│ │ ├── theme.jsx
│ │ └── MapComponent.jsx
│ ├── firebaseConfig.js
│ ├── App.jsx
│ └── pages
│ │ ├── Login.jsx
│ │ ├── welcome.jsx
│ │ ├── SignUp.jsx
│ │ ├── Report.jsx
│ │ ├── Recent.jsx
│ │ ├── Found.jsx
│ │ ├── Nearby.jsx
│ │ ├── Home.jsx
│ │ ├── Profile.jsx
│ │ ├── UpdateProfile.jsx
│ │ ├── Forum.jsx
│ │ └── Adminpage.jsx
├── vite.config.js
├── .gitignore
├── index.html
├── eslint.config.js
├── package.json
└── README.md
└── README.md
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | /config/*.json
4 | .env
--------------------------------------------------------------------------------
/client/versal.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | { "source": "/(.*)", "destination": "/index.html" }
4 | ]
5 | }
--------------------------------------------------------------------------------
/client/src/assets/ReturnIt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Srinanth/Lost_AND_Found/HEAD/client/src/assets/ReturnIt.png
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "leaflet/dist/leaflet.css";
3 |
4 | .dark body {
5 | background-color: #121212;
6 | color: white;
7 | }
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App.jsx'
4 |
5 | createRoot(document.getElementById('root')).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/server/routes/UserRoute.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getUserProfile, updateUserProfile } from "../controllers/UserControl.js";
3 |
4 | const ProfileRouter = express.Router();
5 |
6 | ProfileRouter.get("/profile/:userId", getUserProfile);
7 | ProfileRouter.put("/update/:userId", updateUserProfile);
8 |
9 | export default ProfileRouter;
10 |
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import tailwindcss from '@tailwindcss/vite'
4 |
5 |
6 | // https://vite.dev/config/
7 | export default defineConfig({
8 | plugins: [ tailwindcss(),react()],
9 | base: './',
10 | server: {
11 | historyApiFallback: true, // Fixes 404 errors on refresh
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 |
27 | .env
28 | package-lock.json
29 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
105 |
106 |
107 |
108 |
109 | Potential Matches
110 |
111 |
navigate("/Home")}
113 | className="text-blue-600 hover:text-blue-800 font-medium flex items-center"
114 | >
115 |
116 | Back to Home
117 |
118 |
119 |
120 | {emailStatus === "sending" && (
121 |
122 |
123 | Sending email...
124 |
125 | )}
126 | {emailStatus === "success" && (
127 |
128 |
129 | Email sent successfully!
130 |
131 | )}
132 | {emailStatus === "error" && (
133 |
134 |
135 | Failed to send email. Please try again.
136 |
137 | )}
138 |
139 | {loading && (
140 |
141 |
142 |
148 |
{loadingText}
149 |
150 |
151 | )}
152 |
153 | {!loading && matches.length === 0 && (
154 |
155 |
156 |
157 |
158 |
No matches found
159 |
We'll notify you when we find potential matches for your lost items.
160 |
161 | )}
162 |
163 | {!loading && matches.length > 0 && (
164 |
165 | {matches.map(({ lostItem, topMatches }) => (
166 |
167 |
168 |
169 |
170 |
171 | Your Lost Item
172 |
173 |
{lostItem.description}
174 | {lostItem.imageUrl && (
175 |
176 |
177 |
183 |
184 |
185 | )}
186 |
187 |
188 |
189 |
190 | Potential Matches ({topMatches.length})
191 |
192 |
193 | {topMatches.length > 0 ? (
194 |
195 | {topMatches.map((match) => (
196 |
200 |
201 |
202 |
{match.foundItem.description}
203 |
204 |
205 | {match.similarity ? `${(match.similarity * 100).toFixed(1)}% match` : "Possible match"}
206 |
207 |
208 | {match.foundItem.imageUrl && (
209 |
210 |
215 |
216 | )}
217 |
218 | handleSendEmail(
220 | match.foundItem.userId,
221 | lostItem.description,
222 | lostItem.type
223 | )}
224 | className={`flex-1 ${isDarkMode ? "bg-blue-700 hover:bg-blue-600 text-white" : "bg-blue-600 text-white hover:bg-blue-700"} px-3 py-2 rounded-md transition flex items-center justify-center text-sm`}
225 | >
226 |
227 | Send Email
228 |
229 |
230 |
231 |
232 | ))}
233 |
234 | ) : (
235 |
236 | No close matches found for this item
237 |
238 | )}
239 |
240 |
241 | ))}
242 |
243 | )}
244 |
245 |
246 | );
247 | };
248 |
249 | export default RecentUpdates;
--------------------------------------------------------------------------------
/client/src/pages/Found.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { getAuth } from "firebase/auth";
3 | import { useNavigate } from "react-router-dom";
4 | import { app } from "../firebaseConfig";
5 | import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
6 | import "leaflet/dist/leaflet.css";
7 | import L from "leaflet";
8 | import icon from "leaflet/dist/images/marker-icon.png";
9 | import iconShadow from "leaflet/dist/images/marker-shadow.png";
10 | import LinearProgress from '@mui/material/LinearProgress';
11 |
12 | const DefaultIcon = L.icon({
13 | iconUrl: icon,
14 | shadowUrl: iconShadow,
15 | iconSize: [25, 41],
16 | iconAnchor: [12, 41],
17 | popupAnchor: [1, -34],
18 | shadowSize: [41, 41],
19 | });
20 |
21 | L.Marker.prototype.options.icon = DefaultIcon;
22 |
23 | export default function FoundPage() {
24 | const [category, setCategory] = useState("");
25 | const [description, setDescription] = useState("");
26 | const [location, setLocation] = useState({ lat: null, lng: null });
27 | const [imageFile, setImageFile] = useState(null);
28 | const [error, setError] = useState(null);
29 | const [loading, setLoading] = useState(false);
30 | const [success, setSuccess] = useState(false);
31 | const [isDarkMode, setIsDarkMode] = useState(false);
32 |
33 | const navigate = useNavigate();
34 | const auth = getAuth(app);
35 | const categories = ["Electronics", "Clothing", "Home Appliances", "Books", "Automotive", "Animals/Pets"];
36 |
37 | useEffect(() => {
38 | const storedTheme = localStorage.getItem("darkMode");
39 | setIsDarkMode(storedTheme === "true");
40 |
41 | navigator.geolocation.getCurrentPosition(
42 | (pos) => {
43 | setLocation({ lat: pos.coords.latitude, lng: pos.coords.longitude });
44 | },
45 | (err) => console.error("Error getting location:", err),
46 | { enableHighAccuracy: true }
47 | );
48 | }, []);
49 |
50 | useEffect(() => {
51 | if (success) {
52 | const timer = setTimeout(() => {
53 | navigate("/Home");
54 | }, 1500);
55 | return () => clearTimeout(timer);
56 | }
57 | }, [success, navigate]);
58 |
59 | function LocationMarker() {
60 | useMapEvents({
61 | click(e) {
62 | setLocation({ lat: e.latlng.lat, lng: e.latlng.lng });
63 | },
64 | });
65 | return location.lat && location.lng ? (
66 |
150 |
151 |
152 |
153 | Map Search
154 |
155 | navigate("/Home")}
157 | className="text-blue-600 hover:text-blue-800 font-medium flex items-center"
158 | >
159 |
160 | Back
161 |
162 |
163 |
164 |
165 | {position ? (
166 |
setMapInstance(map)}
172 | >
173 | {mapView === "normal" ? (
174 |
178 | ) : (
179 |
183 | )}
184 |
185 | {userLocation && (
186 |
187 |
188 | You are here
189 |
190 |
191 | )}
192 |
193 | {reports.map((report, index) => (
194 |
199 |
200 |
201 |
{report.description}
202 |
Type: {report.type}
203 | {report.userId && (
204 |
handleSendEmail(report.userId, report.description, report.type)}
206 | className={`mt-2 w-full ${isDarkMode ? "bg-blue-700 hover:bg-blue-600 text-white" : "bg-blue-600 text-white hover:bg-blue-700"} px-3 py-1 rounded-md transition flex items-center justify-center`}
207 | disabled={emailLoading}
208 | >
209 | {emailLoading ? (
210 | <>
211 |
212 | Sending...
213 | >
214 | ) : (
215 | <>
216 |
217 | Contact Owner
218 | >
219 | )}
220 |
221 | )}
222 |
223 |
224 |
225 | ))}
226 |
227 |
228 |
229 |
234 | {mapView === "normal" ? : }
235 |
236 |
237 |
238 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | ) : (
250 |
251 |
252 |
253 | )}
254 |
255 |
256 |
257 | {selectedLocation && (
258 |
259 | Selected: {selectedLocation[0].toFixed(5)}, {selectedLocation[1].toFixed(5)}
260 |
261 | )}
262 |
263 |
264 |
269 | {loading ? (
270 | <>
271 |
272 | Searching...
273 | >
274 | ) : (
275 | <>
276 |
277 | Search This Area
278 | >
279 | )}
280 |
281 |
282 |
283 | {error && (
284 |
{error}
285 | )}
286 |
287 | {reports.length === 0 && !loading && (
288 |
289 | {selectedLocation ? "No items found in this area" : "Select a location on the map"}
290 |
291 | )}
292 |
293 |
294 | );
295 | };
296 |
297 | export default MapComponent;
--------------------------------------------------------------------------------
/client/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useNavigate } from "react-router-dom";
2 | import { useState, useEffect } from "react";
3 | import { Menu, X, Moon, Sun,HelpCircle } from "lucide-react";
4 | import { getAuth } from "firebase/auth";
5 | import { getFirestore, collection, query, where, getDocs } from "firebase/firestore";
6 | import { app } from "../firebaseConfig";
7 | import LinearProgress from '@mui/material/LinearProgress';
8 | import YourBadgesSection from "../assets/badges";
9 |
10 |
11 | export default function HomePage() {
12 | const navigate = useNavigate();
13 | const [hovered, setHovered] = useState(null);
14 | const [isSidebarOpen, setIsSidebarOpen] = useState(false);
15 | const [reportedItems, setReportedItems] = useState([]);
16 | const [foundItems, setFoundItems] = useState([]);
17 | const [loading, setLoading] = useState(true);
18 | const [error, setError] = useState(null);
19 | const [userId, setUserId] = useState(null);
20 |
21 | const auth = getAuth(app);
22 | const db = getFirestore(app);
23 |
24 | const [isDarkMode, setIsDarkMode] = useState(() => {
25 | const storedTheme = localStorage.getItem("darkMode");
26 | return storedTheme ? storedTheme === "true" : false;
27 | });
28 | useEffect(() => {
29 | localStorage.setItem("darkMode", isDarkMode);
30 | }, [isDarkMode]);
31 | useEffect(() => {
32 | const fetchReports = async () => {
33 | try {
34 | const user = auth.currentUser;
35 | if (!user) {
36 | navigate("/login");
37 | return;
38 | }
39 | setUserId(user.uid);
40 | const lostQuery = query(collection(db, "reportedItems"), where("userId", "==", user.uid));
41 | const lostSnapshot = await getDocs(lostQuery);
42 | const lostData = lostSnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
43 |
44 | const foundQuery = query(collection(db, "foundItems"), where("userId", "==", user.uid));
45 | const foundSnapshot = await getDocs(foundQuery);
46 | const foundData = foundSnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
47 |
48 | setReportedItems(lostData);
49 | setFoundItems(foundData);
50 | } catch (err) {
51 | setError("Failed to fetch reported items");
52 | console.error(err);
53 | } finally {
54 | setLoading(false);
55 | }
56 | };
57 |
58 | fetchReports();
59 | }, [navigate, auth, db]);
60 |
61 | const handleLogout = () => {
62 | auth.signOut();
63 | navigate("/login");
64 | };
65 |
66 | const darkClass = isDarkMode ? "dark" : "";
67 |
68 | return (
69 |
70 |
71 |
72 | {[
73 | { name: "Report Lost", path: "/report" },
74 | { name: "Report Found", path: "/found" },
75 | { name: "Forum", path: "/forum" },
76 | { name: "Recent Posts", path: "/recent" },
77 | { name: "Map", path: "/map" },
78 | { name: "Your Profile", path: `/profile/${userId}` },
79 | ].map((item, index) => (
80 | setHovered(index)}
85 | onMouseLeave={() => setHovered(null)}
86 | >
87 | {item.name}
88 |
89 | ))}
90 |
91 |
92 | setIsDarkMode(!isDarkMode)}
94 | className={`p-2 rounded-full ${isDarkMode ? "bg-gray-700 hover:bg-gray-600" : "bg-gray-200 hover:bg-gray-300"} transition`}
95 | >
96 | {isDarkMode ? : }
97 |
98 |
102 | Logout
103 |
104 |
105 |
106 |
setIsSidebarOpen(true)}
109 | >
110 |
111 |
112 |
113 |
setIsSidebarOpen(false)}>
114 |
115 |
116 |
117 | ReturnIt
118 |
119 |
120 |
121 | {[
122 | { name: "Report Lost", path: "/report" },
123 | { name: "Report Found", path: "/found" },
124 | { name: "Forum", path: "/forum" },
125 | { name: "Recent Posts", path: "/recent" },
126 | { name: "Map", path: "/map" },
127 | { name: "Your Profile", path: `/profile/${userId}` },
128 | ].map((item, index) => (
129 | setIsSidebarOpen(false)}
133 | className={`text-lg hover:text-blue-500 p-2 rounded hover:bg-blue-100 transition ${isDarkMode ? "text-white hover:bg-gray-700" : "text-gray-700"}`}
134 | >
135 | {item.name}
136 |
137 | ))}
138 |
139 |
140 | setIsDarkMode(!isDarkMode)}
142 | className={`p-2 rounded-full ${isDarkMode ? "bg-gray-700 hover:bg-gray-600" : "bg-gray-200 hover:bg-gray-300"} transition`}
143 | >
144 | {isDarkMode ? : }
145 |
146 |
150 | Logout
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
Your Lost Items
159 |
160 | + Report New Item
161 |
162 |
163 | {loading ? (
164 |
165 | ) : (
166 |
167 | {reportedItems.length > 0 ? (
168 | reportedItems.map((item) => (
169 |
170 | {item.imageUrl && (
171 |
172 |
173 |
174 | )}
175 |
176 |
177 |
{item.category}
178 | Lost
179 |
180 |
{item.description}
181 |
182 |
183 | ))
184 | ) : (
185 |
186 |
No lost items reported yet
187 |
188 | Report Lost Item
189 |
190 |
191 | )}
192 |
193 | )}
194 |
195 |
196 |
197 |
Your Found Items
198 |
199 | + Report Found Item
200 |
201 |
202 | {loading ? (
203 |
204 | ) : (
205 |
206 | {foundItems.length > 0 ? (
207 | foundItems.map((item) => (
208 |
209 | {item.imageUrl && (
210 |
211 |
212 |
213 | )}
214 |
215 |
216 |
{item.category}
217 | Found
218 |
219 |
{item.description}
220 |
221 |
222 | ))
223 | ) : (
224 |
225 |
No found items reported yet
226 |
227 | Report Found Item
228 |
229 |
230 | )}
231 |
232 | )}
233 |
234 |
235 | Your Badges
236 |
237 |
242 |
243 |
244 |
245 |
246 |
247 | );
248 | }
249 |
--------------------------------------------------------------------------------
/client/src/pages/Profile.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { FaUserEdit } from "react-icons/fa";
3 | import { Menu, X, Moon, Sun } from "lucide-react";
4 | import { Link, useNavigate } from "react-router-dom";
5 | import axios from "axios";
6 | import { getAuth, onAuthStateChanged } from "firebase/auth";
7 | import AccountCircleIcon from '@mui/icons-material/AccountCircle';
8 | import Spinner from '../components/spin';
9 | import { getFirestore, collection, query, where, getDocs } from "firebase/firestore";
10 | import { app } from "../firebaseConfig";
11 |
12 | export default function ProfilePage() {
13 | const [user, setUser] = useState(null);
14 | const [userId, setUserId] = useState(null);
15 | const [error, setError] = useState(null);
16 | const [loading, setLoading] = useState(true);
17 | const [isSidebarOpen, setIsSidebarOpen] = useState(false);
18 | const [hovered, setHovered] = useState(null);
19 | const [reportedItems, setReportedItems] = useState([]);
20 | const [foundItems, setFoundItems] = useState([]);
21 | const [isDarkMode, setIsDarkMode] = useState(() => {
22 | const storedTheme = localStorage.getItem("darkMode");
23 | return storedTheme ? storedTheme === "true" : false;
24 | });
25 |
26 | const navigate = useNavigate();
27 | const auth = getAuth(app);
28 | const db = getFirestore(app);
29 |
30 | useEffect(() => {
31 | localStorage.setItem("darkMode", isDarkMode);
32 | }, [isDarkMode]);
33 |
34 | useEffect(() => {
35 | const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
36 | if (currentUser) {
37 | setUserId(currentUser.uid);
38 | fetchUserProfile(currentUser.uid);
39 | fetchReports(currentUser.uid);
40 | } else {
41 | setError("User not authenticated. Please log in.");
42 | setLoading(false);
43 | }
44 | });
45 |
46 | return () => unsubscribe();
47 | }, [navigate, auth]);
48 |
49 | const fetchUserProfile = async (id) => {
50 | try {
51 | const response = await axios.get(`https://los-n-found.onrender.com/api/profile/${id}`);
52 | setUser(response.data);
53 | setError(null);
54 | } catch (error) {
55 | console.error("Error fetching profile:", error);
56 | setError("Failed to load profile. Please try again later.");
57 | } finally {
58 | setLoading(false);
59 | }
60 | };
61 |
62 | const fetchReports = async (userId) => {
63 | try {
64 | const lostQuery = query(collection(db, "reportedItems"), where("userId", "==", userId));
65 | const lostSnapshot = await getDocs(lostQuery);
66 | const lostData = lostSnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
67 |
68 | const foundQuery = query(collection(db, "foundItems"), where("userId", "==", userId));
69 | const foundSnapshot = await getDocs(foundQuery);
70 | const foundData = foundSnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
71 |
72 | setReportedItems(lostData);
73 | setFoundItems(foundData);
74 | } catch (err) {
75 | console.error("Failed to fetch reported items", err);
76 | }
77 | };
78 |
79 | const handleLogout = () => {
80 | auth.signOut();
81 | navigate("/login");
82 | };
83 |
84 | if (loading) {
85 | return (
86 |
110 | {/* Desktop Navigation */}
111 |
112 |
113 |
114 | ReturnIt
115 |
116 |
117 | {[
118 | { name: "Report Lost", path: "/report" },
119 | { name: "Report Found", path: "/found" },
120 | { name: "Forum", path: "/forum" },
121 | { name: "Recent Posts", path: "/recent" },
122 | { name: "Map", path: "/map" },
123 | ].map((item, index) => (
124 | setHovered(index)}
129 | onMouseLeave={() => setHovered(null)}
130 | >
131 | {item.name}
132 |
133 | ))}
134 |
135 |
136 |
137 | setIsDarkMode(!isDarkMode)}
139 | className={`p-2 rounded-full ${isDarkMode ? "bg-gray-700 hover:bg-gray-600" : "bg-gray-200 hover:bg-gray-300"} transition`}
140 | >
141 | {isDarkMode ? : }
142 |
143 |
147 | Logout
148 |
149 |
150 |
151 |
152 | {/* Mobile Sidebar Toggle */}
153 |
154 | setIsSidebarOpen(true)}
157 | >
158 |
159 |
160 |
161 | ReturnIt
162 |
163 |
164 |
165 | {/* Mobile Sidebar */}
166 |
167 |
setIsSidebarOpen(false)}>
168 |
169 |
170 |
171 | ReturnIt
172 |
173 |
174 |
175 | {[
176 | { name: "Report Lost", path: "/report" },
177 | { name: "Report Found", path: "/found" },
178 | { name: "Forum", path: "/forum" },
179 | { name: "Recent Posts", path: "/recent" },
180 | { name: "Map", path: "/map" },
181 | ].map((item, index) => (
182 | setIsSidebarOpen(false)}
186 | className={`text-lg hover:text-blue-500 p-2 rounded hover:bg-blue-100 transition ${isDarkMode ? "text-white hover:bg-gray-700" : "text-gray-700"}`}
187 | >
188 | {item.name}
189 |
190 | ))}
191 |
192 |
193 | setIsDarkMode(!isDarkMode)}
195 | className={`p-2 rounded-full ${isDarkMode ? "bg-gray-700 hover:bg-gray-600" : "bg-gray-200 hover:bg-gray-300"} transition`}
196 | >
197 | {isDarkMode ? : }
198 |
199 |
203 | Logout
204 |
205 |
206 |
207 |
208 | {/* Main Content */}
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | {user.profileImage ? (
218 |
223 | ) : (
224 |
225 | )}
226 |
227 |
228 |
229 |
{user.name || "No name"}
230 |
231 |
232 | {user.email || "Not provided"}
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
Phone
241 |
242 | {user.phone || "Not provided"}
243 |
244 |
245 |
246 |
247 |
Reports Made
248 |
249 | {reportedItems.length || 0} items reported
250 |
251 |
252 |
253 |
254 |
Items Found
255 |
256 | {foundItems.length || 0} items found
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 | Edit Profile
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 | );
273 | }
--------------------------------------------------------------------------------
/client/src/pages/UpdateProfile.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import axios from "axios";
3 | import { getAuth, onAuthStateChanged } from "firebase/auth";
4 | import { useNavigate } from "react-router-dom";
5 | import AccountCircleIcon from '@mui/icons-material/AccountCircle';
6 | import { FaArrowLeft } from "react-icons/fa";
7 | import { toast } from 'react-hot-toast';
8 |
9 | export default function UpdateProfile() {
10 | const [userId, setUserId] = useState(null);
11 | const [initialData, setInitialData] = useState(null);
12 | const [formData, setFormData] = useState({
13 | name: "",
14 | email: "",
15 | phone: "",
16 | profileImage: "",
17 | });
18 | const [imageFile, setImageFile] = useState(null);
19 | const [loading, setLoading] = useState(false);
20 | const [error, setError] = useState(null);
21 | const [success, setSuccess] = useState(null);
22 | const [isDarkMode] = useState(() => {
23 | return localStorage.getItem("darkMode") === "true";
24 | });
25 |
26 | const navigate = useNavigate();
27 |
28 | useEffect(() => {
29 | const auth = getAuth();
30 | const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
31 | if (currentUser) {
32 | setUserId(currentUser.uid);
33 | fetchUserProfile(currentUser.uid);
34 | }
35 | });
36 |
37 | return () => unsubscribe();
38 | }, []);
39 |
40 | const fetchUserProfile = async (id) => {
41 | try {
42 | const response = await axios.get(`https://los-n-found.onrender.com/api/profile/${id}`);
43 | setInitialData(response.data);
44 | setFormData({
45 | name: response.data.name || "",
46 | email: response.data.email || "",
47 | phone: response.data.phone || "",
48 | profileImage: response.data.profileImage || "",
49 | });
50 | } catch (error) {
51 | console.error("Error fetching profile:", error);
52 | setError("Failed to load profile data");
53 | }
54 | };
55 |
56 | const handleChange = (e) => {
57 | setFormData({ ...formData, [e.target.name]: e.target.value });
58 | };
59 |
60 | const handleImageChange = (e) => {
61 | if (e.target.files && e.target.files[0]) {
62 | const file = e.target.files[0];
63 | if (file.size > 5 * 1024 * 1024) {
64 | setError("Image size should be less than 5MB");
65 | return;
66 | }
67 |
68 | setImageFile(file);
69 | setError(null);
70 | const reader = new FileReader();
71 | reader.onload = (event) => {
72 | setFormData(prev => ({ ...prev, profileImage: event.target.result }));
73 | };
74 | reader.readAsDataURL(file);
75 | }
76 | };
77 |
78 | const handleSubmit = async (e) => {
79 | e.preventDefault();
80 | setLoading(true);
81 | setError(null);
82 | setSuccess(null);
83 |
84 | if (!formData.name.trim()) {
85 | setError("Name is required");
86 | setLoading(false);
87 | return;
88 | }
89 |
90 | if (!formData.email.trim()) {
91 | setError("Email is required");
92 | setLoading(false);
93 | return;
94 | }
95 |
96 | if (initialData &&
97 | formData.name === initialData.name &&
98 | formData.email === initialData.email &&
99 | formData.phone === initialData.phone &&
100 | !imageFile) {
101 | setLoading(false);
102 | setSuccess("No changes detected");
103 | setTimeout(() => navigate("/profile"), 1500);
104 | return;
105 | }
106 |
107 | let imageUrl = formData.profileImage;
108 |
109 | if (imageFile) {
110 | const formDataImage = new FormData();
111 | formDataImage.append("image", imageFile);
112 |
113 | try {
114 | const uploadResponse = await fetch("https://los-n-found.onrender.com/api/cloudinary/upload", {
115 | method: "POST",
116 | body: formDataImage,
117 | });
118 |
119 | const uploadData = await uploadResponse.json();
120 | if (uploadResponse.ok) {
121 | imageUrl = uploadData.imageUrl;
122 | } else {
123 | throw new Error("Image upload failed.");
124 | }
125 | } catch (error) {
126 | setError("Image upload failed. Please try again.");
127 | setLoading(false);
128 | return;
129 | }
130 | }
131 |
132 | try {
133 | const updatePayload = {
134 | name: formData.name,
135 | phone: formData.phone,
136 | profileImage: imageUrl,
137 | };
138 | if (initialData?.email !== formData.email) {
139 | updatePayload.email = formData.email;
140 | }
141 |
142 | const response = await axios.put(
143 |
144 | `https://los-n-found.onrender.com/api/update/${userId}`,
145 | updatePayload
146 | );
147 |
148 | setSuccess("Profile updated successfully!");
149 | toast.success("Profile updated successfully!");
150 | setTimeout(() => navigate(`/profile/${userId}`), 1500);
151 | } catch (error) {
152 | console.error("Error updating profile:", error);
153 | setError(error.response?.data?.error ||
154 | error.response?.data?.details ||
155 | "Failed to update profile!");
156 | toast.error(errorMessage);
157 | } finally {
158 | setLoading(false);
159 | }
160 | };
161 |
162 | const darkClass = isDarkMode ? "dark" : "";
163 |
164 | return (
165 |
128 |
129 |
130 |
Community Forum
131 | navigate("/Home")}
133 | className={`${isDarkMode ? "text-blue-400 hover:text-blue-300" : "text-blue-600 hover:text-blue-800"} font-medium`}
134 | >
135 | ← Back to Home
136 |
137 |
138 |
139 |
140 |
141 |
142 | setSearchQuery(e.target.value)}
150 | />
151 |
152 |
setActivePost("new")}
157 | >
158 | Create New Post
159 |
160 |
161 |
162 | {!activePost && (
163 |
164 | {filteredDiscussions.length > 0 ? (
165 | filteredDiscussions.map((discussion) => (
166 |
setActivePost(discussion.id)}
172 | >
173 |
174 |
175 |
{discussion.title}
176 |
177 |
{discussion.content}
178 |
181 |
182 |
183 | {discussion.author}
184 | •
185 | {discussion.date}
186 |
187 |
188 |
189 |
190 | {discussion.likes}
191 |
192 |
193 |
194 | {discussion.replies}
195 |
196 |
197 |
198 |
199 |
200 | ))
201 | ) : (
202 |
205 |
No discussions found matching your search.
206 |
207 | )}
208 |
209 | )}
210 | {activePost && activePost !== "new" && (
211 |
214 | {discussions.filter(d => d.id === activePost).map(discussion => (
215 |
216 |
217 |
setActivePost(null)}
219 | className={`font-medium mb-4 ${
220 | isDarkMode ? "text-blue-400 hover:text-blue-300" : "text-blue-600 hover:text-blue-800"
221 | }`}
222 | >
223 | ← Back to discussions
224 |
225 |
226 |
{discussion.title}
229 |
232 |
233 | {discussion.author}
234 | •
235 | {discussion.date}
236 |
237 |
238 |
{discussion.content}
241 |
242 |
243 |
handleLike(discussion.id)}
248 | >
249 |
250 | Like ({discussion.likes})
251 |
252 |
255 |
256 | {discussion.replies} {discussion.replies === 1 ? 'reply' : 'replies'}
257 |
258 |
259 |
260 |
261 | {discussion.comments.map(comment => (
262 |
265 |
268 |
269 | {comment.author}
270 | •
271 | {comment.date}
272 |
273 |
{comment.content}
276 |
handleLike(discussion.id, comment.id)}
281 | >
282 |
283 | Like ({comment.likes})
284 |
285 |
286 | ))}
287 |
288 |
289 |
290 |
Add your comment
293 |
311 |
312 |
313 | ))}
314 |
315 | )}
316 | {activePost === "new" && (
317 |
320 |
setActivePost(null)}
322 | className={`font-medium mb-4 ${
323 | isDarkMode ? "text-blue-400 hover:text-blue-300" : "text-blue-600 hover:text-blue-800"
324 | }`}
325 | >
326 | ← Back to discussions
327 |
328 |
329 |
Create New Post
332 |
333 |
334 |
335 | Title
338 |
345 |
346 |
347 |
348 | Content
351 |
358 |
359 |
360 |
361 | setActivePost(null)}
363 | className={`px-4 py-2 border rounded-lg hover:bg-gray-700 transition ${
364 | isDarkMode ? "border-gray-600 text-white" : "border-gray-300 hover:bg-gray-50"
365 | }`}
366 | >
367 | Cancel
368 |
369 |
374 | Post Discussion
375 |
376 |
377 |
378 |
379 | )}
380 |
381 |
382 | );
383 | };
384 |
385 | export default ForumPage;
--------------------------------------------------------------------------------
/client/src/pages/Adminpage.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { Menu, X, Moon, Sun, Trash2, MoreVertical, Edit } from "lucide-react";
4 | import { getAuth } from "firebase/auth";
5 | import { app } from "../firebaseConfig";
6 |
7 | export default function AdminPage() {
8 | const navigate = useNavigate();
9 | const auth = getAuth(app);
10 | const [isSidebarOpen, setIsSidebarOpen] = useState(false);
11 | const [hovered, setHovered] = useState(null);
12 | const [reportedItems, setReportedItems] = useState([
13 | {
14 | id: "1",
15 | category: "Wallet",
16 | description: "Black leather wallet with two credit cards, driver's license, and college ID. Slight scratch on the front corner.",
17 | imageUrl: "https://media.istockphoto.com/id/180756294/photo/wallet.jpg?s=2048x2048&w=is&k=20&c=LsLYeSiy8Kk8XGXCmWBD-vukSQWJMNVbhIyVghEEXxM=&auto=format&fit=crop",
18 | location: "Main Campus - Building A",
19 | status: "Lost"
20 | },
21 | {
22 | id: "2",
23 | category: "Laptop",
24 | description: "13-inch MacBook Pro in a silver case with a 'Code Like a Pro' sticker on the back. Slight dent on the top left corner.",
25 | imageUrl: "https://images.unsplash.com/photo-1593642632823-8f785ba67e45?w=500&auto=format&fit=crop",
26 | location: "Library - 2nd Floor",
27 | status: "Lost"
28 | },
29 | {
30 | id: "3",
31 | category: "Keys",
32 | description: "Bunch of 5 metal keys on a ring with a blue plastic tag labeled 'B-Block 23'.",
33 | imageUrl: "https://images.pexels.com/photos/842528/pexels-photo-842528.jpeg?w=500&auto=format&fit=crop",
34 | location: "Cafeteria",
35 | status: "Lost"
36 | },
37 | {
38 | id: "4",
39 | category: "Phone",
40 | description: "iPhone 12 with a matte black case. The lock screen has a dog wallpaper. May have low battery.",
41 | imageUrl: "https://media.istockphoto.com/id/1197063359/photo/woman-using-apple-iphone-11-pro-smartphone.jpg?s=2048x2048&w=is&k=20&c=Q8nQ4II_VJlaukvKvCl4kix6LUGczuFTQAmr9jEOs6s=&auto=format&fit=crop",
42 | location: "Sports Complex",
43 | status: "Lost"
44 | },
45 | {
46 | id: "9",
47 | category: "Watch",
48 | description: "Black Fossil smartwatch with a leather strap and a cracked screen protector.",
49 | imageUrl: "https://images.unsplash.com/photo-1523170335258-f5ed11844a49?w=500&auto=format&fit=crop",
50 | location: "Men's Restroom - Admin Block",
51 | status: "Lost"
52 | },
53 | {
54 | id: "11",
55 | category: "Headphones",
56 | description: "Black wireless Sony headphones in a soft case with minor scratches on the left ear cup.",
57 | imageUrl: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=500&auto=format&fit=crop",
58 | location: "Music Room",
59 | status: "Lost"
60 | },
61 | {
62 | id: "12",
63 | category: "Calculator",
64 | description: "Casio FX-991ES PLUS scientific calculator with name written on the back in black ink.",
65 | imageUrl: "https://images.unsplash.com/photo-1587145820266-a5951ee6f620?w=500&auto=format&fit=crop",
66 | location: "Exam Hall 2",
67 | status: "Lost"
68 | }
69 | ]);
70 |
71 | const [foundItems, setFoundItems] = useState([
72 | {
73 | id: "5",
74 | category: "Backpack",
75 | description: "Blue Jansport backpack containing 3 textbooks, a pencil pouch, and a laptop charger.",
76 | imageUrl: "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=500&auto=format&fit=crop",
77 | location: "Lecture Hall B",
78 | status: "Found"
79 | },
80 | {
81 | id: "6",
82 | category: "Glasses",
83 | description: "Black rectangular prescription glasses inside a brown leather case with initials 'RK'.",
84 | imageUrl: "https://images.unsplash.com/photo-1511499767150-a48a237f0083?w=500&auto=format&fit=crop",
85 | location: "Computer Lab",
86 | status: "Found"
87 | },
88 | {
89 | id: "7",
90 | category: "Notebook",
91 | description: "Spiral-bound notebook with 'Organic Chemistry' written on the first page. Green plastic cover.",
92 | imageUrl: "https://images.unsplash.com/photo-1544947950-fa07a98d237f?w=500&auto=format&fit=crop",
93 | location: "Chemistry Department",
94 | status: "Found"
95 | },
96 | {
97 | id: "8",
98 | category: "Water Bottle",
99 | description: "Silver stainless steel bottle with several cartoon character stickers. Slightly dented at the bottom.",
100 | imageUrl: "https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=500&auto=format&fit=crop",
101 | location: "Gym",
102 | status: "Found"
103 | },
104 | {
105 | id: "14",
106 | category: "Earphones",
107 | description: "White Apple earphones found tangled on a bench. Minor dirt on wires.",
108 | imageUrl: "https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=500&auto=format&fit=crop",
109 | location: "Bus Stop",
110 | status: "Found"
111 | },
112 | {
113 | id: "15",
114 | category: "Cap",
115 | description: "Black Nike cap with white logo, slightly faded. Found on a stair railing.",
116 | imageUrl: "https://images.unsplash.com/photo-1575428652377-a2d80e2277fc?w=500&auto=format&fit=crop",
117 | location: "Stadium Entrance",
118 | status: "Found"
119 | },
120 | {
121 | id: "16",
122 | category: "Umbrella",
123 | description: "Blue foldable umbrella with a wooden handle. Wet and placed on a bench near the entrance.",
124 | imageUrl: "https://media.istockphoto.com/id/866721702/photo/blue-umbrella.jpg?s=2048x2048&w=is&k=20&c=YWt75t-vdqUoAXyRVnE6l5mVEpQ1UfSZlsnyOzEEDNs=&auto=format&fit=crop",
125 | location: "Library Gate",
126 | status: "Found"
127 | },
128 | {
129 | id: "17",
130 | category: "Power Bank",
131 | description: "Red Mi power bank with 20000mAh capacity. Found unplugged near a charging station.",
132 | imageUrl: "https://media.istockphoto.com/id/1159079715/photo/blue-external-battery.jpg?s=2048x2048&w=is&k=20&c=igmq2ibBU-isQPN732s1IuOjQ5MX8lW4N8zD4HLy_9g=&auto=format&fit=crop",
133 | location: "Student Lounge",
134 | status: "Found"
135 | }
136 | ]);
137 |
138 | const [users, setUsers] = useState([
139 | {
140 | id: "u1",
141 | username: "Kevin Jose",
142 | email: "kevin@gmail.com"
143 | },
144 | {
145 | id: "u2",
146 | username: "Gopika",
147 | email: "Gopzz@gmail.com"
148 | },
149 | {
150 | id: "u3",
151 | username: "Cinol Samson",
152 | email: "cinol@gmail.com"
153 | },
154 | {
155 | id: "u4",
156 | username: "Ardra",
157 | email: "Ardra@gmail.com"
158 | },
159 | {
160 | id: "u5",
161 | username: "Shwetha",
162 | email: "shwetha@gmail.com"
163 | },
164 | {
165 | id: "u6",
166 | username: "Pranav",
167 | email: "pranav@gmail.com"
168 | },
169 | {
170 | id: "u7",
171 | username: "Srinanth",
172 | email: "srinanth@gmail.com"
173 | }
174 | ]);
175 |
176 | const [isDarkMode, setIsDarkMode] = useState(() => {
177 | const storedTheme = localStorage.getItem("darkMode");
178 | return storedTheme ? storedTheme === "true" : false;
179 | });
180 |
181 | const [activeMenu, setActiveMenu] = useState(null);
182 |
183 | useEffect(() => {
184 | localStorage.setItem("darkMode", isDarkMode);
185 | }, [isDarkMode]);
186 |
187 | const handleDeleteItem = (collectionName, id, setItems) => {
188 | if (!window.confirm("Are you sure you want to delete this item?")) return;
189 | setItems(prev => prev.filter(item => item.id !== id));
190 | alert("Item deleted successfully");
191 | };
192 |
193 | const handleDeleteUser = (userId) => {
194 | if (!window.confirm("Are you sure you want to delete this user?")) return;
195 | setUsers(prev => prev.filter(user => user.id !== userId));
196 | setActiveMenu(null);
197 | alert("User deleted successfully");
198 | };
199 |
200 | const handleLogout = () => {
201 | auth.signOut();
202 | navigate("/login");
203 | };
204 |
205 | const toggleMenu = (userId) => {
206 | setActiveMenu(activeMenu === userId ? null : userId);
207 | };
208 |
209 | return (
210 |