├── 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 | 5 | 6 | 7 | ReturnIt 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/config/firebaseConfig.js: -------------------------------------------------------------------------------- 1 | import admin from "firebase-admin"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | // Parse Firebase credentials from the environment variable 7 | const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT); 8 | 9 | admin.initializeApp({ 10 | credential: admin.credential.cert(serviceAccount), 11 | }); 12 | 13 | 14 | export const db = admin.firestore(); 15 | export const auth = admin.auth(); 16 | 17 | console.log("🔥 Firebase connected successfully!"); 18 | -------------------------------------------------------------------------------- /server/routes/MatchRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { matchUserItems } from "../controllers/matchControl.js"; 3 | 4 | const Matchrouter = express.Router(); 5 | 6 | Matchrouter.get("/matches/:userId", async (req, res) => { 7 | try { 8 | const userId = req.params.userId; 9 | const matches = await matchUserItems(userId); 10 | res.status(200).json(matches); 11 | } catch (error) { 12 | console.error("Error fetching matches:", error); 13 | res.status(500).json({ error: "Failed to fetch matches" }); 14 | } 15 | }); 16 | 17 | export default Matchrouter -------------------------------------------------------------------------------- /client/src/components/loading.jsx: -------------------------------------------------------------------------------- 1 | const LoadingScreen = ({ isDarkMode }) => { 2 | return ( 3 |
8 |

9 | ReturnIt 10 |

11 |
12 | ); 13 | }; 14 | 15 | export default LoadingScreen; 16 | -------------------------------------------------------------------------------- /server/middleware/authmiddle.js: -------------------------------------------------------------------------------- 1 | import { auth } from "../config/firebaseConfig.js"; 2 | 3 | export const verifyUser = async (req, res, next) => { 4 | try { 5 | const token = req.headers.authorization?.split("Bearer ")[1]; 6 | if (!token) return res.status(401).json({ message: "No token provided" }); 7 | 8 | const decodedToken = await auth.verifyIdToken(token); 9 | req.user = decodedToken; 10 | next(); 11 | } catch (error) { 12 | console.error("Token Verification Error:", error.message); 13 | return res.status(403).json({ message: "Unauthorized" }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/spin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Spinner = ({ size = "md", color = "blue" }) => { 4 | const sizeClasses = { 5 | sm: 'w-6 h-6 border-2', 6 | md: 'w-10 h-10 border-4', 7 | lg: 'w-14 h-14 border-6', 8 | }; 9 | 10 | const colorClasses = { 11 | blue: 'border-blue-500 border-t-transparent', 12 | white: 'border-white border-t-transparent', 13 | }; 14 | 15 | const selectedSizeClass = sizeClasses[size] || sizeClasses.md; 16 | const selectedColorClass = colorClasses[color] || colorClasses.blue; 17 | 18 | return ( 19 |
20 | ); 21 | }; 22 | 23 | export default Spinner; -------------------------------------------------------------------------------- /server/routes/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | // import express from "express"; 2 | // import { verifyUser } from "../middleware/authmiddle.js"; 3 | // import { db } from "../config/firebaseConfig.js"; 4 | 5 | // const ProtectedRouter = express.Router(); 6 | 7 | 8 | // ProtectedRouter.get("/profile", verifyUser, async (req, res) => { 9 | // try { 10 | // const userDoc = await db.collection("users").doc(req.user.email).get(); 11 | // if (!userDoc.exists) return res.status(404).json({ error: "User not found" }); 12 | 13 | // return res.json({ user: userDoc.data() }); 14 | // } catch (error) { 15 | // return res.status(500).json({ error: error.message }); 16 | // } 17 | // }); 18 | 19 | 20 | 21 | // export default ProtectedRouter; 22 | -------------------------------------------------------------------------------- /client/src/firebaseConfig.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getStorage } from "firebase/storage"; 3 | import { getAuth, GoogleAuthProvider } from "firebase/auth"; 4 | 5 | 6 | 7 | const firebaseConfig = { 8 | apiKey: "AIzaSyAeH1zRH8nrNxP5QxFadiyJhzoVT-j1qSk", 9 | authDomain: "match-making-a1c79.firebaseapp.com", 10 | projectId: "match-making-a1c79", 11 | storageBucket: "match-making-a1c79.firebasestorage.app", 12 | messagingSenderId: "1003305083842", 13 | appId: "1:1003305083842:web:63ffb634836d41780c2c5b", 14 | measurementId: "G-0GKW8800LZ" 15 | }; 16 | 17 | // Initialize Firebase 18 | export const app = initializeApp(firebaseConfig); 19 | export const auth = getAuth(app); 20 | export const googleProvider = new GoogleAuthProvider(); 21 | export const storage = getStorage(app); -------------------------------------------------------------------------------- /client/src/components/theme.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useContext } from "react"; 2 | import { useEffect } from "react"; 3 | 4 | const ThemeContext = createContext(); 5 | 6 | export const useTheme = () => { 7 | return useContext(ThemeContext); 8 | }; 9 | 10 | export const ThemeProvider = ({ children }) => { 11 | const [darkMode, setDarkMode] = useState(false); 12 | 13 | const toggleDarkMode = () => { 14 | setDarkMode(prev => !prev); 15 | }; 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export const useThemeEffect = () => { 25 | useEffect(() => { 26 | const darkTheme = localStorage.getItem("theme") === "dark"; 27 | if (darkTheme) { 28 | document.documentElement.classList.add("dark"); 29 | } else { 30 | document.documentElement.classList.remove("dark"); 31 | } 32 | }, []); 33 | }; -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Srinanth/Los_N_Foun-.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Srinanth/Los_N_Foun-/issues" 17 | }, 18 | "homepage": "https://github.com/Srinanth/Los_N_Foun-#readme", 19 | "description": "", 20 | "dependencies": { 21 | "axios": "^1.8.3", 22 | "bcrypt": "^5.1.1", 23 | "bcryptjs": "^3.0.2", 24 | "cloudinary": "^2.6.0", 25 | "cors": "^2.8.5", 26 | "dotenv": "^16.4.7", 27 | "express": "^4.21.2", 28 | "firebase": "^11.5.0", 29 | "firebase-admin": "^13.2.0", 30 | "geofire-common": "^6.0.0", 31 | "jsonwebtoken": "^9.0.2", 32 | "multer": "^2.0.1", 33 | "nodemailer": "^6.10.0", 34 | "streamifier": "^0.1.1", 35 | "uuid": "^11.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/routes/CloudinaryRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import multer from "multer"; 3 | import { uploadToCloudinary, deleteFromCloudinary } from "../config/CloudinaryConfig.js"; 4 | 5 | const Cloudrouter = express.Router(); 6 | const upload = multer(); 7 | 8 | 9 | Cloudrouter.post("/upload", upload.single("image"), async (req, res) => { 10 | if (!req.file) { 11 | return res.status(400).json({ error: "No file uploaded" }); 12 | } 13 | 14 | try { 15 | const result = await uploadToCloudinary(req.file.buffer); 16 | res.json({ imageUrl: result.secure_url }); 17 | } catch (error) { 18 | res.status(500).json({ error: "Image upload failed" }); 19 | } 20 | }); 21 | 22 | Cloudrouter.delete("/delete-image", async (req, res) => { 23 | const { publicId } = req.body; 24 | 25 | try { 26 | const result = await deleteFromCloudinary(publicId); 27 | res.json({ message: "Image deleted successfully", result }); 28 | } catch (error) { 29 | res.status(500).json({ error: "Image deletion failed" }); 30 | } 31 | }); 32 | 33 | export default Cloudrouter; -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | import UserRouter from "./routes/AuthRoutes.js"; 4 | import cors from "cors"; 5 | // import ProtectedRouter from "./routes/ProtectedRoute.js"; 6 | import Cloudrouter from "./routes/CloudinaryRoute.js"; 7 | import Reportrouter from "./routes/ReportRoute.js"; 8 | import FoundRouter from "./routes/FoundRoute.js"; 9 | import Matchrouter from "./routes/MatchRoute.js"; 10 | import ProfileRouter from "./routes/UserRoute.js"; 11 | import NearbyRoute from "./controllers/nearby.js"; 12 | import MailRouter from "./routes/Mailroute.js"; 13 | 14 | dotenv.config(); 15 | const app = express(); 16 | app.use(cors({ origin: "https://los-n-found-p783.onrender.com", credentials: true })); 17 | app.use(express.json()); 18 | 19 | // Routes 20 | app.use("/api/auth", UserRouter); 21 | // app.use("/api/protected", ProtectedRouter); 22 | app.use("/api/cloudinary", Cloudrouter); 23 | app.use("/api", Reportrouter); 24 | app.use("/api", FoundRouter); 25 | app.use("/api", Matchrouter); 26 | app.use("/api",ProfileRouter); 27 | app.use("/api",NearbyRoute); 28 | app.use("/api",MailRouter) 29 | 30 | const PORT = process.env.PORT || 5000; 31 | app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); 32 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.14.0", 14 | "@emotion/styled": "^11.14.0", 15 | "@mui/icons-material": "^7.0.0", 16 | "@mui/material": "^7.0.0", 17 | "@tailwindcss/vite": "^4.0.14", 18 | "axios": "^1.8.4", 19 | "cloudinary": "^2.6.0", 20 | "dotenv": "^16.4.7", 21 | "firebase": "^11.5.0", 22 | "leaflet": "^1.9.4", 23 | "lucide-react": "^0.483.0", 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0", 26 | "react-hot-toast": "^2.5.2", 27 | "react-icons": "^5.5.0", 28 | "react-leaflet": "^5.0.0", 29 | "react-router-dom": "^7.4.0", 30 | "streamifier": "^0.1.1", 31 | "tailwindcss": "^4.0.14" 32 | }, 33 | "devDependencies": { 34 | "@eslint/js": "^9.21.0", 35 | "@types/react": "^19.0.10", 36 | "@types/react-dom": "^19.0.4", 37 | "@vitejs/plugin-react-swc": "^3.8.0", 38 | "eslint": "^9.21.0", 39 | "eslint-plugin-react-hooks": "^5.1.0", 40 | "eslint-plugin-react-refresh": "^0.4.19", 41 | "globals": "^15.15.0", 42 | "vite": "^6.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/config/CloudinaryConfig.js: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | import streamifier from "streamifier"; 3 | import dotenv from "dotenv" 4 | dotenv.config(); 5 | // Configure Cloudinary 6 | cloudinary.config({ 7 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 8 | api_key: process.env.CLOUDINARY_API_KEY, 9 | api_secret: process.env.CLOUDINARY_API_SECRET, 10 | }); 11 | 12 | // Function to upload an image to Cloudinary 13 | export const uploadToCloudinary = (fileBuffer, folder = "lost_items") => { 14 | return new Promise((resolve, reject) => { 15 | const stream = cloudinary.uploader.upload_stream( 16 | { folder }, // Optional: Organize images into folders 17 | (error, result) => { 18 | if (error) { 19 | reject(error); 20 | } else { 21 | resolve(result); 22 | } 23 | } 24 | ); 25 | 26 | // Convert file buffer to a stream and upload 27 | streamifier.createReadStream(fileBuffer).pipe(stream); 28 | }); 29 | }; 30 | 31 | // Function to delete an image from Cloudinary 32 | export const deleteFromCloudinary = (publicId) => { 33 | return new Promise((resolve, reject) => { 34 | cloudinary.uploader.destroy(publicId, (error, result) => { 35 | if (error) { 36 | reject(error); 37 | } else { 38 | resolve(result); 39 | } 40 | }); 41 | }); 42 | }; -------------------------------------------------------------------------------- /server/routes/ReportRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { db, auth } from "../config/firebaseConfig.js"; 3 | 4 | const Reportrouter = express.Router(); 5 | 6 | 7 | Reportrouter.post("/report", async (req, res) => { 8 | const { category, description, location, imageUrl } = req.body; 9 | const token = req.headers.authorization?.split(" ")[1]; 10 | 11 | if (!token) { 12 | return res.status(401).json({ error: "Unauthorized" }); 13 | } 14 | 15 | if (!category || !description || !location || !location.lat || !location.lng) { 16 | return res.status(400).json({ error: "Missing required fields" }); 17 | } 18 | 19 | try { 20 | 21 | const user = await auth.verifyIdToken(token); 22 | const userId = user.uid; 23 | 24 | const reportData = { 25 | userId, 26 | category, 27 | description, 28 | location: { 29 | lat: location.lat, 30 | lng: location.lng, 31 | }, 32 | imageUrl: imageUrl || null, 33 | createdAt: new Date().toISOString(), 34 | }; 35 | 36 | const docRef = await db.collection("reportedItems").add(reportData); 37 | res.status(201).json({ id: docRef.id, message: "Report submitted successfully" }); 38 | } catch (error) { 39 | console.error("Error saving report:", error.message || error); 40 | res.status(500).json({ error: "Failed to submit report" }); 41 | } 42 | }); 43 | 44 | export default Reportrouter; 45 | -------------------------------------------------------------------------------- /server/routes/FoundRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { db, auth } from "../config/firebaseConfig.js"; 3 | 4 | const FoundRouter = express.Router(); 5 | 6 | 7 | FoundRouter.post("/found", async (req, res) => { 8 | const { category, description, location, imageUrl } = req.body; 9 | const token = req.headers.authorization?.split(" ")[1]; 10 | 11 | if (!token) { 12 | return res.status(401).json({ error: "Unauthorized" }); 13 | } 14 | 15 | if (!category || !description || !location || !location.lat || !location.lng) { 16 | return res.status(400).json({ error: "Missing required fields" }); 17 | } 18 | 19 | try { 20 | 21 | const user = await auth.verifyIdToken(token); 22 | const userId = user.uid; 23 | 24 | const foundData = { 25 | userId, 26 | category, 27 | description, 28 | location: { 29 | lat: location.lat, 30 | lng: location.lng, 31 | }, 32 | imageUrl: imageUrl || null, 33 | createdAt: new Date().toISOString(), 34 | }; 35 | 36 | const docRef = await db.collection("foundItems").add(foundData); 37 | res.status(201).json({ id: docRef.id, message: "Found item reported successfully" }); 38 | } catch (error) { 39 | console.error("Error saving found report:", error.message || error); 40 | res.status(500).json({ error: "Failed to submit found item report" }); 41 | } 42 | }); 43 | 44 | export default FoundRouter; 45 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Setting Up a React + Vite Project 2 | 3 | This guide provides the necessary steps to set up and run a React project using Vite. 4 | 5 | --- 6 | 7 | ## **1️⃣ Install Node.js and npm** 8 | Ensure **Node.js** is installed, as it includes `npm` (Node Package Manager). 9 | 10 | ### **🔹 Windows/macOS/Linux:** 11 | - Download **Node.js LTS** from: [https://nodejs.org/](https://nodejs.org/) 12 | - Verify installation by running: 13 | ```sh 14 | node -v # Check Node.js version 15 | npm -v # Check npm version 16 | ``` 17 | 18 | --- 19 | 20 | ## **2️⃣ Clone the Repository** 21 | Clone the repository to get the project files locally. 22 | 23 | ```sh 24 | git clone https://github.com/your-username/your-repo.git 25 | ``` 26 | 27 | Replace `your-username` and `your-repo` with the actual values. 28 | 29 | --- 30 | 31 | ## **3️⃣ Navigate to the Frontend Directory** 32 | If the React frontend is in a subfolder (`frontend`), move into it: 33 | 34 | ```sh 35 | cd client 36 | ``` 37 | 38 | --- 39 | 40 | ## **4️⃣ Install Dependencies** 41 | Run the following command to install all required dependencies: 42 | 43 | ```sh 44 | npm install 45 | ``` 46 | 47 | This will install all packages listed in `package.json`. 48 | 49 | --- 50 | 51 | ## **5️⃣ Run the Development Server** 52 | To start the project in development mode with **Vite**, use: 53 | 54 | ```sh 55 | npm run dev 56 | ``` 57 | 58 | This will launch the project on a local server. The terminal will display a local development URL (e.g., `http://localhost:5173/`). 59 | 60 | --- 61 | 62 | ## **7️⃣ Stopping the Development Server** 63 | To stop the server, press: 64 | 65 | ``` 66 | CTRL + C 67 | ``` 68 | 69 | --- 70 | 71 | Now the project is ready for development! 🚀 72 | 73 | -------------------------------------------------------------------------------- /server/routes/AuthRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import bcrypt from "bcryptjs"; 3 | import jwt from "jsonwebtoken"; 4 | import { db } from "../config/firebaseConfig.js"; 5 | import dotenv from "dotenv"; 6 | import cors from "cors"; 7 | import { verifyUser } from "../middleware/authmiddle.js"; 8 | dotenv.config(); 9 | 10 | const UserRouter = express.Router(); 11 | UserRouter.use(cors()); 12 | 13 | const generateToken = (email) => { 14 | return jwt.sign({ email }, process.env.JWT_SECRET, { expiresIn: "7d" }); 15 | }; 16 | 17 | UserRouter.post("/signup", verifyUser, async (req, res) => { 18 | try { 19 | const { uid, username, email } = req.user; 20 | const newUser = { uid, username, email }; 21 | console.log("User signed up:", newUser); 22 | 23 | res.status(201).json({ message: "User registered successfully", user: newUser }); 24 | } catch (error) { 25 | res.status(500).json({ error: "Signup failed", details: error.message }); 26 | } 27 | }); 28 | 29 | UserRouter.post("/login", async (req, res) => { 30 | try { 31 | const { email, password } = req.body; 32 | const userDoc = await db.collection("users").doc(email).get(); 33 | 34 | if (!userDoc.exists) return res.status(404).json({ error: "User not found" }); 35 | 36 | const userData = userDoc.data(); 37 | const isValidPassword = await bcrypt.compare(password, userData.password); 38 | 39 | if (!isValidPassword) return res.status(401).json({ error: "Invalid credentials" }); 40 | 41 | 42 | const token = generateToken(email); 43 | 44 | return res.status(200).json({ message: "Login successful", token }); 45 | } catch (error) { 46 | return res.status(500).json({ error: error.message }); 47 | } 48 | }); 49 | 50 | export default UserRouter; -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2 | import "./App.css"; 3 | import { ThemeProvider, useTheme } from "./components/theme"; 4 | import LoginPage from "./pages/Login"; 5 | import LandingPage from "./pages/welcome"; 6 | import SignUp from "./pages/SignUp"; 7 | import UpdateProfile from "./pages/UpdateProfile"; 8 | import ProfilePage from "./pages/Profile"; 9 | import HomePage from "./pages/Home"; 10 | import ReportPage from "./pages/Report"; 11 | import FoundPage from "./pages/Found"; 12 | import RecentUpdates from "./pages/Recent"; 13 | import MapComponent from "./pages/Nearby"; 14 | import ForumPage from "./pages/Forum"; 15 | import { Toaster } from "react-hot-toast"; 16 | import AdminPage from './pages/Adminpage.jsx'; 17 | 18 | function App() { 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /client/src/components/MapComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from "react-leaflet"; 3 | import "leaflet/dist/leaflet.css"; 4 | import L from "leaflet"; 5 | import icon from "leaflet/dist/images/marker-icon.png"; 6 | import iconShadow from "leaflet/dist/images/marker-shadow.png"; 7 | 8 | const DefaultIcon = L.icon({ 9 | iconUrl: icon, 10 | shadowUrl: iconShadow, 11 | iconSize: [25, 41], 12 | iconAnchor: [12, 41], 13 | popupAnchor: [1, -34], 14 | shadowSize: [41, 41], 15 | }); 16 | 17 | L.Marker.prototype.options.icon = DefaultIcon; 18 | 19 | export default function MapComponent({ setLocation }) { 20 | const [position, setPosition] = useState(null); 21 | 22 | function LocationMarker() { 23 | useMapEvents({ 24 | click(e) { 25 | setPosition(e.latlng); 26 | setLocation(e.latlng); 27 | }, 28 | }); 29 | 30 | return position ? ( 31 | 32 | Selected Location: {position.lat.toFixed(5)}, {position.lng.toFixed(5)} 33 | 34 | ) : null; 35 | } 36 | 37 | useEffect(() => { 38 | navigator.geolocation.getCurrentPosition( 39 | (position) => { 40 | setPosition({ lat: position.coords.latitude, lng: position.coords.longitude }); 41 | setLocation({ lat: position.coords.latitude, lng: position.coords.longitude }); 42 | }, 43 | () => console.log("Location access denied") 44 | ); 45 | }, []); 46 | 47 | return ( 48 | 54 | 55 | 56 | 57 | ); 58 | } -------------------------------------------------------------------------------- /server/routes/Mailroute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import nodemailer from "nodemailer"; 3 | import dotenv from "dotenv"; 4 | import admin from "firebase-admin"; 5 | 6 | dotenv.config(); 7 | const MailRouter = express.Router(); 8 | 9 | // Nodemailer Configuration 10 | const transporter = nodemailer.createTransport({ 11 | service: "gmail", 12 | auth: { 13 | user: process.env.EMAIL_USER, 14 | pass: process.env.EMAIL_PASS, 15 | }, 16 | }); 17 | 18 | 19 | MailRouter.post("/send-email", async (req, res) => { 20 | const { ownerUid, senderEmail, itemDetails, title } = req.body; 21 | 22 | try { 23 | // Fetch the owner's email from Firebase Auth 24 | const ownerRecord = await admin.auth().getUser(ownerUid); 25 | const ownerEmail = ownerRecord.email; 26 | 27 | 28 | const mailOptions = { 29 | from: senderEmail, 30 | to: ownerEmail, 31 | subject: `Lost & Found: Inquiry about Reported Item`, 32 | text: `Dear User, 33 | 34 | 👋 We hope this message finds you well. 35 | 36 | We are reaching out through our Lost & Found service regarding an item that may match something you’ve reported as missing. 37 | 38 | 📦 Item Details: 39 | ${itemDetails} 40 | 41 | If this item appears to be yours, we kindly request you to contact the person who found it in order to verify ownership and arrange for its return. 42 | 43 | 📧 Please reply directly to the following email address: 44 | ${senderEmail} 45 | 46 | To help confirm ownership, please include any identifying details you may have (e.g., serial number, stickers, scratches, or unique features). 47 | 48 | 🙏 We appreciate your cooperation and hope you’re reunited with your lost item soon. 49 | 50 | Warm regards, 51 | Lost & Found Support Team 52 | 🔁 ReturnIt 53 | `, 54 | }; 55 | 56 | // Send the email 57 | await transporter.sendMail(mailOptions); 58 | res.json({ success: true, message: "Email sent successfully!" }); 59 | } catch (error) { 60 | console.error("Error sending email:", error); 61 | res.status(500).json({ error: "Failed to send email" }); 62 | } 63 | }); 64 | 65 | export default MailRouter; -------------------------------------------------------------------------------- /server/controllers/nearby.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import admin from "firebase-admin"; 3 | 4 | const NearbyRoute = express.Router(); 5 | 6 | 7 | function haversineDistance(lat1, lon1, lat2, lon2) { 8 | const toRadians = (degrees) => degrees * (Math.PI / 180); 9 | const R = 6371; 10 | const dLat = toRadians(lat2 - lat1); 11 | const dLon = toRadians(lon2 - lon1); 12 | const a = 13 | Math.sin(dLat / 2) * Math.sin(dLat / 2) + 14 | Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); 15 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 16 | return R * c; 17 | } 18 | 19 | 20 | async function getAllItems() { 21 | const reportedItemsRef = admin.firestore().collection("reportedItems"); 22 | const foundItemsRef = admin.firestore().collection("foundItems"); 23 | 24 | const reportedItemsSnapshot = await reportedItemsRef.get(); 25 | const foundItemsSnapshot = await foundItemsRef.get(); 26 | 27 | const reportedItems = reportedItemsSnapshot.docs.map((doc) => ({ 28 | id: doc.id, 29 | ...doc.data(), 30 | type: "reported", 31 | })); 32 | 33 | const foundItems = foundItemsSnapshot.docs.map((doc) => ({ 34 | id: doc.id, 35 | ...doc.data(), 36 | type: "found", 37 | })); 38 | 39 | return [...reportedItems, ...foundItems]; // Combine both arrays 40 | } 41 | 42 | // Filter items based on selected location 43 | NearbyRoute.get("/items", async (req, res) => { 44 | const { latitude, longitude, radius = 20 } = req.query; 45 | if (!latitude || !longitude) { 46 | return res.status(400).json({ error: "Latitude and longitude are required" }); 47 | } 48 | 49 | try { 50 | const allItems = await getAllItems(); 51 | const center = [parseFloat(latitude), parseFloat(longitude)]; 52 | const radiusInKm = parseFloat(radius); 53 | 54 | // Filter items within the specified radius 55 | const filteredItems = allItems.filter((item) => { 56 | if (!item.location || !item.location.lat || !item.location.lng) return false; 57 | 58 | const distance = haversineDistance( 59 | center[0], 60 | center[1], 61 | item.location.lat, 62 | item.location.lng 63 | ); 64 | 65 | return distance <= radiusInKm; 66 | }); 67 | 68 | 69 | res.json({ items: filteredItems }); 70 | } catch (error) { 71 | res.status(500).json({ error: "Failed to fetch items" }); 72 | } 73 | }); 74 | 75 | export default NearbyRoute; -------------------------------------------------------------------------------- /server/controllers/UserControl.js: -------------------------------------------------------------------------------- 1 | import admin from "firebase-admin"; 2 | 3 | const db = admin.firestore(); 4 | const auth = admin.auth(); 5 | 6 | export const getUserProfile = async (req, res) => { 7 | const { userId } = req.params; 8 | 9 | try { 10 | const userDoc = await db.collection("users").doc(userId).get(); 11 | const userData = userDoc.exists ? userDoc.data() : {}; 12 | 13 | const authUser = await auth.getUser(userId); 14 | const reportedItemsSnap = await db.collection("reportedItems").where("userId", "==", userId).get(); 15 | const foundItemsSnap = await db.collection("foundItems").where("userId", "==", userId).get(); 16 | 17 | const userProfile = { 18 | name: userData.name || "", 19 | email: authUser.email || "", 20 | phone: userData.phone || "", 21 | profileImage: userData.profileImage || "", 22 | reportsCount: reportedItemsSnap.size, 23 | foundCount: foundItemsSnap.size, 24 | }; 25 | 26 | res.json(userProfile); 27 | } catch (error) { 28 | console.error("Error fetching user profile:", error); 29 | res.status(500).json({ error: "Error fetching user profile" }); 30 | } 31 | }; 32 | 33 | export const updateUserProfile = async (req, res) => { 34 | const { userId } = req.params; 35 | const { name, phone, profileImage, email } = req.body; 36 | 37 | try { 38 | 39 | const userRef = db.collection("users").doc(userId); 40 | await userRef.set({ 41 | name, 42 | phone, 43 | profileImage, 44 | createdAt: admin.firestore.FieldValue.serverTimestamp(), 45 | updatedAt: admin.firestore.FieldValue.serverTimestamp() 46 | }, { merge: true }); 47 | 48 | if (email) { 49 | const authUser = await auth.getUser(userId); 50 | if (authUser.email !== email) { 51 | await auth.updateUser(userId, { email }); 52 | } 53 | } 54 | 55 | res.json({ 56 | success: true, 57 | message: "Profile updated successfully", 58 | data: { 59 | name, 60 | phone, 61 | profileImage, 62 | email: email || undefined 63 | } 64 | }); 65 | } catch (error) { 66 | console.error("Error updating profile:", error); 67 | 68 | let errorMessage = "Error updating profile"; 69 | if (error.code === 'auth/email-already-in-use') { 70 | errorMessage = "Email is already in use by another account"; 71 | } else if (error.code === 'auth/invalid-email') { 72 | errorMessage = "Invalid email address"; 73 | } 74 | 75 | res.status(500).json({ 76 | error: errorMessage, 77 | details: error.message 78 | }); 79 | } 80 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔍 Lost & Found Platform 2 | 3 | A **smart AI-powered lost and found platform** that helps users report, search, and match lost and found items based on **location and description**. 4 | **Built as part of the IPR Project for Semester 2** – This project showcases the integration of AI and modern web technologies to solve real-world problems. 5 | ## 🚀 Features 6 | 7 | ✅ **Report Lost & Found Items** – Users can report lost or found items with details like category, description, location, and optional images. 8 | ✅ **AI-Powered Matchmaking** – Uses a Hugging Face model to intelligently match lost and found items based on descriptions and locations. 9 | ✅ **Location-Based Search** – Users can search for lost or found items within their area. 10 | ✅ **Email Contact System** – Users can click on a lost or found report and directly send an email to the reporter for inquiries. 11 | ✅ **User Authentication** – Firebase-based authentication with **email and Google sign-up options**. 12 | ✅ **Modern & Responsive UI** – Built with **Tailwind CSS** for a seamless mobile-friendly experience. 13 | 14 | ## 🛠️ Tech Stack 15 | 16 | - **Frontend:** React.js, Tailwind CSS 17 | - **Backend:** Express.js, Node.js 18 | - **AI Model:** Hugging Face 19 | - **Database & Authentication:** Firebase (Firestore & Auth) 20 | 21 | ## 🏗️ Setup Instructions 22 | 23 | ### 1️⃣ Clone the Repository 24 | ```bash 25 | git clone https://github.com/yourusername/lost-and-found.git 26 | cd lost-and-found 27 | ``` 28 | 29 | ### 2️⃣ Install Dependencies 30 | ```bash 31 | npm install 32 | ``` 33 | 34 | ### 3️⃣ Set Up Firebase 35 | - Create a Firebase project at [Firebase Console](https://console.firebase.google.com/) 36 | - Enable **Firestore Database** & **Authentication** 37 | - Get Firebase config keys and update `firebase.js` 38 | 39 | ### 4️⃣ Start the Backend Server 40 | ```bash 41 | cd server 42 | node server.js 43 | ``` 44 | 45 | ### 5️⃣ Run the Frontend 46 | ```bash 47 | npm run dev 48 | ``` 49 | 50 | ## 🎯 Usage 51 | 52 | ### 1️⃣ Report Lost or Found Items 53 | - Navigate to the **"Report Item"** or **"Report a Found Item"** page. 54 | - Enter details like **category, description, and location**. 55 | - Optionally upload an **image**. 56 | 57 | ### 2️⃣ Search & AI-Powered Matchmaking 58 | - Use the **location-based search** to find items within your area. 59 | - AI matches lost and found items based on **description & location**. 60 | 61 | ### 3️⃣ Contact Users 62 | - Click on an item report to view details. 63 | - Use the **email button** to contact the owner directly. 64 | 65 | --- 66 | 67 | ## 📌 Future Enhancements 68 | 69 | - **Push notifications** for matching items 70 | - **Image-based AI matching** for better accuracy 71 | - **Multi-language support** for wider accessibility 72 | 73 | --- 74 | 75 | Contributions & feedback are welcome! 🚀 76 | -------------------------------------------------------------------------------- /server/controllers/matchControl.js: -------------------------------------------------------------------------------- 1 | import admin from "firebase-admin"; 2 | import axios from "axios"; 3 | import dotenv from "dotenv"; 4 | 5 | dotenv.config(); 6 | const db = admin.firestore(); 7 | 8 | // Hugging Face API 9 | const HF_API_URL = "https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2"; 10 | const HF_API_KEY = process.env.HF_API_KEY; 11 | 12 | // Fetch lost items by logged-in user 13 | async function getUserLostItems(userId) { 14 | const lostRef = db.collection("reportedItems").where("userId", "==", userId); 15 | const lostDocs = await lostRef.get(); 16 | return lostDocs.docs.map(doc => ({ id: doc.id, ...doc.data() })); 17 | } 18 | 19 | // Fetch all found items 20 | async function getAllFoundItems() { 21 | const foundRef = db.collection("foundItems"); 22 | const foundDocs = await foundRef.get(); 23 | return foundDocs.docs.map(doc => ({ id: doc.id, ...doc.data() })); 24 | } 25 | 26 | // Get similarity score using Hugging Face API 27 | async function getSimilarity(sentence1, sentence2) { 28 | try { 29 | const response = await axios.post( 30 | HF_API_URL, 31 | { inputs: { source_sentence: sentence1, sentences: [sentence2] } }, 32 | { headers: { Authorization: `Bearer ${HF_API_KEY}`, "Content-Type": "application/json" } } 33 | ); 34 | 35 | return Array.isArray(response.data) ? response.data[0] : null; 36 | } catch (error) { 37 | return null; 38 | } 39 | } 40 | 41 | // Haversine formula to calculate distance between two coordinates 42 | function haversineDistance(lat1, lon1, lat2, lon2) { 43 | const toRadians = (degrees) => degrees * (Math.PI / 180); 44 | const R = 6371; // Earth's radius in km 45 | const dLat = toRadians(lat2 - lat1); 46 | const dLon = toRadians(lon2 - lon1); 47 | const a = 48 | Math.sin(dLat / 2) * Math.sin(dLat / 2) + 49 | Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); 50 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 51 | return R * c; // Distance in km 52 | } 53 | 54 | // Match user's lost items with all found items 55 | async function matchUserItems(userId) { 56 | const lostItems = await getUserLostItems(userId); 57 | const foundItems = await getAllFoundItems(); 58 | 59 | let matchedResults = []; 60 | 61 | for (const lost of lostItems) { 62 | let matches = []; 63 | 64 | for (const found of foundItems) { 65 | if (!lost.description || !found.description || !lost.location || !found.location) continue; 66 | 67 | // Calculate description similarity 68 | const similarity = await getSimilarity(lost.description, found.description); 69 | if (similarity === null || similarity < 0.5) continue; 70 | // Calculate distance between lost and found items 71 | const distance = haversineDistance( 72 | lost.location.lat, 73 | lost.location.lng, 74 | found.location.lat, 75 | found.location.lng 76 | ); 77 | 78 | // Skip if distance is greater than 1km 79 | if (distance > 1) continue; 80 | 81 | // Normalize distance to a score between 0 and 1 82 | const normalizedDistance = distance / 1; // Since max distance is 1km 83 | 84 | // Calculate final score (weighted average of similarity and proximity) 85 | const finalScore = (0.7 * similarity) + (0.3 * (1 - normalizedDistance)); 86 | 87 | matches.push({ foundItem: found, similarity: finalScore }); 88 | } 89 | 90 | // Sort matches by final score and take top 3 91 | matches.sort((a, b) => b.similarity - a.similarity); 92 | matchedResults.push({ lostItem: lost, topMatches: matches.slice(0, 3) }); 93 | } 94 | 95 | return matchedResults; 96 | } 97 | 98 | export { matchUserItems }; -------------------------------------------------------------------------------- /client/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { FaGoogle } from "react-icons/fa"; 4 | import { auth, googleProvider } from "../firebaseConfig"; 5 | import { signInWithEmailAndPassword, signInWithPopup, onAuthStateChanged } from "firebase/auth"; 6 | import Visibility from '@mui/icons-material/Visibility'; 7 | import VisibilityOff from '@mui/icons-material/VisibilityOff'; 8 | import { toast, Toaster } from 'react-hot-toast'; 9 | 10 | export default function LoginPage() { 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [passwordVisible, setPasswordVisible] = useState(false); 14 | const [isDarkMode, setIsDarkMode] = useState(false); 15 | const navigate = useNavigate(); 16 | 17 | useEffect(() => { 18 | const storedTheme = localStorage.getItem("darkMode"); 19 | setIsDarkMode(storedTheme === "true"); 20 | 21 | const unsubscribe = onAuthStateChanged(auth, (user) => { 22 | if (user) { 23 | 24 | navigate(user.email.toLowerCase() === "admin@returnit.com" ? "/admin" : "/Home"); 25 | } 26 | }); 27 | 28 | return () => unsubscribe(); 29 | }, [navigate]); 30 | 31 | const handleLogin = async (e) => { 32 | e.preventDefault(); 33 | 34 | try { 35 | const userCredential = await signInWithEmailAndPassword(auth, email, password); 36 | const token = await userCredential.user.getIdToken(); 37 | localStorage.setItem("token", token); 38 | toast.success('Login successful!'); 39 | navigate(userCredential.user.email.toLowerCase() === "admin@returnit.com" ? "/admin" : "/Home"); 40 | } catch (error) { 41 | toast.error("Invalid email or password"); 42 | } 43 | }; 44 | 45 | const handleGoogleLogin = async () => { 46 | try { 47 | const userCredential = await signInWithPopup(auth, googleProvider); 48 | const token = await userCredential.user.getIdToken(); 49 | localStorage.setItem("token", token); 50 | toast.success('Logged in with Google!'); 51 | navigate(userCredential.user.email.toLowerCase() === "admin@returnit.com" ? "/admin" : "/Home"); 52 | } catch (error) { 53 | toast.error("Failed to login with Google"); 54 | } 55 | }; 56 | return ( 57 |
58 | 59 |
62 |
65 |

"Lost things have a way of finding their way back to those who cherish them." ✨🔍

66 |

- Anonymous

67 |
68 |
69 |

Welcome Back

72 | 73 |
74 |
75 | 78 | setEmail(e.target.value)} 82 | className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ 83 | isDarkMode ? "bg-gray-700 border-gray-600 text-white" : "border-gray-300" 84 | }`} 85 | placeholder="Enter your email" 86 | required 87 | /> 88 |
89 |
90 | 93 | setPassword(e.target.value)} 97 | className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ 98 | isDarkMode ? "bg-gray-700 border-gray-600 text-white" : "border-gray-300" 99 | }`} 100 | placeholder="Enter your password" 101 | required 102 | /> 103 | 112 |
113 | 114 | 122 |
123 | 124 |
125 |
128 | or 131 |
134 |
135 | 136 | 146 | 147 |

150 | Don't have an account?{' '} 151 | 154 | Sign up 155 | 156 |

157 |
158 |
159 |
160 | ); 161 | } -------------------------------------------------------------------------------- /client/src/assets/badges.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { HelpCircle } from "lucide-react"; 3 | 4 | const badgeUrls = { 5 | contributor: "https://img.icons8.com/color/48/000000/starburst-shape.png", 6 | helpfulFinder: "https://img.icons8.com/fluency/48/000000/medal.png", 7 | helpfulFinderNextLevel: "https://img.icons8.com/plasticine/48/000000/trophy.png", 8 | activeUser: "https://img.icons8.com/plasticine/48/000000/fire-badge.png", 9 | }; 10 | 11 | const badgeCriteria = { 12 | contributor: { description: "Report at least one lost or found item." }, 13 | helpfulFinder: { description: "Successfully help reunite more than 5 found items.", threshold: 5 }, 14 | helpfulFinderNextLevel: { description: "Successfully help reunite more than 15 found items.", threshold: 15 }, 15 | activeUser: { description: "Maintain consistent activity on the platform." }, 16 | }; 17 | 18 | const fakeBadgeRules = [ 19 | badgeCriteria.contributor.description + " Earn the 'Contributor' badge upon your first report.", 20 | badgeCriteria.helpfulFinder.description + " You'll get the 'Helpful Finder' reward after your 6th successful reunion.", 21 | badgeCriteria.helpfulFinderNextLevel.description + " Reach 16 successful reunions to unlock the 'Dedicated Helper' badge.", 22 | badgeCriteria.activeUser.description + " Log in regularly to achieve this badge.", 23 | ]; 24 | 25 | const ProgressBar = ({ progress }) => ( 26 |
27 |
31 |
32 | ); 33 | 34 | export default function YourBadgesSection({ isDarkMode, reportedItems, foundItems }) { 35 | const [showRules, setShowRules] = useState(false); 36 | const [earnedBadges, setEarnedBadges] = useState([]); 37 | const [progressToNext, setProgressToNext] = useState({}); 38 | 39 | useEffect(() => { 40 | const badges = []; 41 | const progress = {}; 42 | 43 | if (reportedItems && reportedItems.length > 0) { 44 | badges.push("contributor"); 45 | } 46 | 47 | if (foundItems) { 48 | if (foundItems.length > badgeCriteria.helpfulFinder.threshold) { 49 | badges.push("helpfulFinder"); 50 | if (foundItems.length < badgeCriteria.helpfulFinderNextLevel.threshold) { 51 | const remaining = badgeCriteria.helpfulFinderNextLevel.threshold - foundItems.length; 52 | progress["helpfulFinderNextLevel"] = { 53 | needed: remaining, 54 | current: foundItems.length, 55 | threshold: badgeCriteria.helpfulFinderNextLevel.threshold, 56 | description: ` ${remaining} more found result${remaining > 1 ? 's' : ''} till the next level!`, 57 | }; 58 | } 59 | } else { 60 | const remaining = badgeCriteria.helpfulFinder.threshold - foundItems.length; 61 | if (remaining > 0) { 62 | progress["helpfulFinder"] = { 63 | needed: remaining, 64 | current: foundItems.length, 65 | threshold: badgeCriteria.helpfulFinder.threshold, 66 | description: ` ${remaining} more found result${remaining > 1 ? 's' : ''} till the 'Helpful Finder' badge!`, 67 | }; 68 | } 69 | } 70 | } 71 | 72 | setEarnedBadges(badges); 73 | setProgressToNext(progress); 74 | }, [reportedItems, foundItems]); 75 | 76 | const handleRulesClick = () => { 77 | setShowRules(!showRules); 78 | }; 79 | 80 | return ( 81 |
82 |
83 | 90 |
91 | 92 | {showRules && ( 93 |
94 |

How to Earn Rewards

95 | 100 |
101 | )} 102 | 103 |
104 | {earnedBadges.length > 0 ? ( 105 |
106 |
107 | {earnedBadges.map((badge) => ( 108 |
109 | {badgeUrls[badge] && {badge}} 110 | {badge.charAt(0).toUpperCase() + badge.slice(1)} 111 |
112 | ))} 113 |
114 | {Object.keys(progressToNext).length > 0 && ( 115 |
116 |

Progress to Next Reward:

117 |
118 | {Object.entries(progressToNext).map(([badgeKey, progressInfo]) => ( 119 |
120 |
121 | {badgeCriteria[badgeKey]?.description} 122 | {progressInfo.current}/{progressInfo.threshold} 123 |
124 | 0 127 | ? (progressInfo.current / progressInfo.threshold) * 100 128 | : 0 129 | } 130 | /> 131 |

{progressInfo.description}

132 |
133 | ))} 134 |
135 |
136 | )} 137 |
138 | ) : ( 139 |
140 |

No badges earned yet. Start participating to earn rewards!

141 | {progressToNext["helpfulFinder"] && ( 142 |
143 |

Your Progress:

144 |
145 | {badgeCriteria["helpfulFinder"]?.description} 146 | {progressToNext["helpfulFinder"].current}/{badgeCriteria["helpfulFinder"].threshold} 147 |
148 | 0 151 | ? (progressToNext["helpfulFinder"].current / badgeCriteria["helpfulFinder"].threshold) * 100 152 | : 0 153 | } 154 | /> 155 |

{progressToNext["helpfulFinder"].description}

156 |
157 | )} 158 |
159 | )} 160 |
161 |
162 | ); 163 | } -------------------------------------------------------------------------------- /client/src/pages/welcome.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import EmailIcon from "@mui/icons-material/Email"; 3 | import SupportAgentIcon from "@mui/icons-material/SupportAgent"; 4 | import GavelIcon from "@mui/icons-material/Gavel"; 5 | import PrivacyTipIcon from "@mui/icons-material/PrivacyTip"; 6 | import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; 7 | import { useState, useEffect } from "react"; 8 | import LoadingScreen from "../components/loading"; 9 | 10 | export default function LandingPage() { 11 | const navigate = useNavigate(); 12 | const [showLoading, setShowLoading] = useState(false); 13 | const [redirectPath, setRedirectPath] = useState(null); 14 | const [isDarkMode, setIsDarkMode] = useState(() => { 15 | const storedTheme = localStorage.getItem("darkMode"); 16 | return storedTheme ? storedTheme === "true" : false; 17 | }); 18 | const handleRedirect = (path) => { 19 | setRedirectPath(path); 20 | setShowLoading(true); 21 | 22 | setTimeout(() => { 23 | navigate(path); 24 | }, 2000); 25 | }; 26 | 27 | if (showLoading) return ; 28 | 29 | 30 | const darkClass = isDarkMode ? "dark" : ""; 31 | const textColorClass = isDarkMode ? "text-gray-300" : "text-gray-800"; 32 | const bgColorClass = isDarkMode ? "bg-gray-900" : "bg-gradient-to-b from-blue-50 to-white"; 33 | const navBgClass = isDarkMode ? "bg-gray-800 shadow-md" : "bg-white shadow-sm"; 34 | const headerBgClass = isDarkMode ? "bg-gradient-to-r from-blue-700 to-blue-800 text-white" : "bg-gradient-to-r from-blue-500 to-blue-600 text-white"; 35 | const buttonClass = `px-4 py-2 rounded-md transition font-medium ${isDarkMode ? "text-blue-400 hover:bg-blue-800 border border-blue-400" : "text-blue-600 hover:bg-blue-50 border border-blue-600"}`; 36 | const primaryButtonClass = `px-4 py-2 rounded-md transition font-medium ${isDarkMode ? "bg-blue-700 text-white hover:bg-blue-600" : "bg-blue-600 text-white hover:bg-blue-700"}`; 37 | const sectionBgClass = isDarkMode ? "bg-gray-800 shadow-md text-white" : "bg-white shadow-md text-gray-600"; 38 | const sectionTitleClass = `text-2xl font-semibold mb-4 ${isDarkMode ? "text-blue-400" : "text-blue-600"}`; 39 | const footerBgClass = isDarkMode ? "bg-gray-800 border-t border-gray-700" : "bg-white border-t"; 40 | const footerLinkClass = `hover:underline ${isDarkMode ? "text-blue-400" : "text-blue-600"}`; 41 | const footerTextColorClass = isDarkMode ? "text-gray-400" : "text-gray-600"; 42 | 43 | return ( 44 |
45 | 62 | 63 |
64 |

Find Lost Items, Report Found Items

65 |

66 | Welcome to ReturnIt – Your Campus Lost & Found Solution! 67 | Losing or finding an item on campus can be stressful, but ReturnIt is here to make the process simple and secure! 68 |

69 |
70 | 71 |
72 |
73 |

Recent Lost & Found Cases

74 |

75 | Lost something? Found something? Our platform helps reunite items with their owners. 76 | The more you use ReturnIt, the more effective our community becomes! 77 |

78 |
79 | 80 |
81 |

How It Works

82 |
83 |
84 | 1 85 |

Sign Up & Secure Your Account

86 |
87 |
88 | 2 89 |

Post About Lost or Found Items

90 |
91 |
92 | 3 93 |

Smart Validation System for Security

94 |
95 |
96 | 4 97 |

Easy Communication to Return Items

98 |
99 |
100 |
101 |
102 | 103 |
104 |
105 |
106 |

107 | © {new Date().getFullYear()} ReturnIt. All rights reserved. 108 |

109 |

110 | We’re committed to helping users find and return lost items safely and securely using modern technology and community collaboration. 111 |

112 |
113 |
114 |
115 | 116 | Contact Us 117 |
118 | 124 |

125 | Support available: 24/7 126 |

127 |
128 |
129 |
Our Policies
130 |
131 | 132 | Terms of Service 133 |
134 |
135 | 136 | Privacy Policy 137 |
138 |
139 | 140 | FAQ 141 |
142 |
143 |
144 |
145 |
146 | ); 147 | } -------------------------------------------------------------------------------- /client/src/pages/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { FaGoogle } from "react-icons/fa"; 4 | import { auth, googleProvider } from "../firebaseConfig"; 5 | import { CircularProgress } from "@mui/material"; 6 | import { createUserWithEmailAndPassword, signInWithPopup, updateProfile } from "firebase/auth"; 7 | import { toast } from "react-hot-toast"; 8 | 9 | const SignUp = () => { 10 | const [username, setUsername] = useState(""); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [confirmPassword, setConfirmPassword] = useState(""); 14 | const [loading, setLoading] = useState(false); 15 | const navigate = useNavigate(); 16 | const [isDarkMode, setIsDarkMode] = useState(() => { 17 | const storedTheme = localStorage.getItem("darkMode"); 18 | return storedTheme ? storedTheme === "true" : false; 19 | }); 20 | 21 | const handleSignUp = async (e) => { 22 | e.preventDefault(); 23 | 24 | if (!username.trim()) return toast.error("Username is required"); 25 | if (!email.trim()) return toast.error("Email is required"); 26 | if (!password) return toast.error("Password is required"); 27 | if (password.length < 6) return toast.error("Password must be at least 6 characters"); 28 | if (password !== confirmPassword) return toast.error("Passwords do not match"); 29 | 30 | setLoading(true); 31 | 32 | try { 33 | const userCredential = await createUserWithEmailAndPassword(auth, email, password); 34 | const user = userCredential.user; 35 | await updateProfile(user, { displayName: username }); 36 | 37 | const idToken = await user.getIdToken(); 38 | 39 | const res = await fetch("https://los-n-found.onrender.com/api/auth/signup", { 40 | method: "POST", 41 | headers: { "Content-Type": "application/json", "Authorization": `Bearer ${idToken}`, }, 42 | body: JSON.stringify({ uid: user.uid, username, email }), 43 | }); 44 | 45 | if (!res.ok) throw new Error("Failed to save user to database"); 46 | 47 | toast.success("Account created successfully!"); 48 | navigate("/Home"); 49 | } catch (err) { 50 | console.error("Signup Error:", err); 51 | switch (err.code) { 52 | case "auth/email-already-in-use": 53 | toast.error("Email already in use."); 54 | break; 55 | case "auth/invalid-email": 56 | toast.error("Invalid email address."); 57 | break; 58 | case "auth/weak-password": 59 | toast.error("Password too weak (min 6 characters)."); 60 | break; 61 | default: 62 | toast.error(err.message || "Something went wrong."); 63 | } 64 | } finally { 65 | setLoading(false); 66 | } 67 | }; 68 | 69 | const handleGoogleSignIn = async () => { 70 | try { 71 | const userCredential = await signInWithPopup(auth, googleProvider); 72 | const user = userCredential.user; 73 | 74 | await fetch("https://los-n-found.onrender.com/api/auth/signup", { 75 | method: "POST", 76 | headers: { "Content-Type": "application/json" }, 77 | body: JSON.stringify({ uid: user.uid, username: user.displayName, email: user.email }), 78 | }); 79 | 80 | toast.success("Signed in with Google!"); 81 | navigate("/Home"); 82 | } catch (err) { 83 | toast.error(err.message || "Google Sign-In failed."); 84 | } 85 | }; 86 | 87 | const darkClass = isDarkMode ? "dark" : ""; 88 | 89 | return ( 90 |
91 |
92 |
93 |

"Join our community of helpful students"

94 |

ReturnIt makes campus life easier for everyone

95 |
96 | 97 |
98 |

Create Your Account

99 | 100 |
101 |
102 | 103 | setUsername(e.target.value)} 107 | className={`w-full px-4 py-2 border ${isDarkMode ? "border-gray-600 bg-gray-700 text-white" : "border-gray-300"} rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`} 108 | placeholder="Choose a username" 109 | required 110 | /> 111 |
112 | 113 |
114 | 115 | setEmail(e.target.value)} 119 | className={`w-full px-4 py-2 border ${isDarkMode ? "border-gray-600 bg-gray-700 text-white" : "border-gray-300"} rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`} 120 | placeholder="Enter your email" 121 | required 122 | /> 123 |
124 | 125 |
126 | 127 | setPassword(e.target.value)} 131 | className={`w-full px-4 py-2 border ${isDarkMode ? "border-gray-600 bg-gray-700 text-white" : "border-gray-300"} rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`} 132 | placeholder="Create a password" 133 | required 134 | /> 135 |
136 | 137 |
138 | 139 | setConfirmPassword(e.target.value)} 143 | className={`w-full px-4 py-2 border ${isDarkMode ? "border-gray-600 bg-gray-700 text-white" : "border-gray-300"} rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`} 144 | placeholder="Confirm your password" 145 | required 146 | /> 147 |
148 | 149 | 156 |
157 | 158 |
159 |
160 | or 161 |
162 |
163 | 164 | 170 | 171 |

172 | Already have an account?{' '} 173 | 174 | Log in 175 | 176 |

177 |
178 |
179 |
180 | ); 181 | }; 182 | 183 | export default SignUp; -------------------------------------------------------------------------------- /client/src/pages/Report.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 ReportPage() { 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(() => { 32 | const storedTheme = localStorage.getItem("darkMode"); 33 | return storedTheme ? storedTheme === "true" : false; 34 | }); 35 | 36 | const navigate = useNavigate(); 37 | const auth = getAuth(app); 38 | const categories = ["Electronics", "Clothing", "Home Appliances", "Books", "Automotive", "Animals/Pets"]; 39 | 40 | useEffect(() => { 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 | 67 | ) : null; 68 | } 69 | 70 | const handleSubmit = async (e) => { 71 | e.preventDefault(); 72 | setError(null); 73 | setSuccess(false); 74 | setLoading(true); 75 | 76 | const user = auth.currentUser; 77 | if (!user) { 78 | setError("You must be logged in to report a lost item."); 79 | setLoading(false); 80 | return; 81 | } 82 | 83 | if (!location.lat || !location.lng) { 84 | setError("Please select a location on the map."); 85 | setLoading(false); 86 | return; 87 | } 88 | 89 | let imageUrl = ""; 90 | if (imageFile) { 91 | try { 92 | const formData = new FormData(); 93 | formData.append("image", imageFile); 94 | 95 | const uploadResponse = await fetch("https://los-n-found.onrender.com/api/cloudinary/upload", { 96 | method: "POST", 97 | body: formData, 98 | }); 99 | 100 | const uploadData = await uploadResponse.json(); 101 | if (uploadResponse.ok) { 102 | imageUrl = uploadData.imageUrl; 103 | } else { 104 | throw new Error("Image upload failed."); 105 | } 106 | } catch (error) { 107 | setError("Image upload failed. Please try again."); 108 | setLoading(false); 109 | return; 110 | } 111 | } 112 | 113 | const reportData = { 114 | category, 115 | description, 116 | location, 117 | imageUrl, 118 | userId: user.uid, 119 | }; 120 | 121 | try { 122 | const response = await fetch("https://los-n-found.onrender.com/api/report", { 123 | method: "POST", 124 | headers: { 125 | "Content-Type": "application/json", 126 | Authorization: `Bearer ${await user.getIdToken()}`, 127 | }, 128 | body: JSON.stringify(reportData), 129 | }); 130 | 131 | const data = await response.json(); 132 | if (response.ok) { 133 | setSuccess(true); 134 | setCategory(""); 135 | setDescription(""); 136 | setLocation({ lat: null, lng: null }); 137 | setImageFile(null); 138 | } else { 139 | setError(data.error || "Something went wrong."); 140 | } 141 | } catch (err) { 142 | setError("Server error, please try again."); 143 | } 144 | 145 | setLoading(false); 146 | }; 147 | 148 | const darkClass = isDarkMode ? "dark" : ""; 149 | 150 | return ( 151 |
152 |
153 |
154 |
155 |
156 |

Report a Lost Item

157 | 163 |
164 | 165 | {loading && } 166 | {error &&

{error}

} 167 | {success && ( 168 |
169 | Item reported successfully! 170 |
171 | )} 172 | 173 |
174 |
175 | 176 | 187 |
188 | 189 |
190 | 191 |