Repository: safak/full-stack-estate Branch: main Commit: f4d40157ad81 Files: 75 Total size: 93.7 KB Directory structure: gitextract_quk43qar/ ├── api/ │ ├── .gitignore │ ├── app.js │ ├── controllers/ │ │ ├── auth.controller.js │ │ ├── chat.controller.js │ │ ├── message.controller.js │ │ ├── post.controller.js │ │ ├── test.controller.js │ │ └── user.controller.js │ ├── lib/ │ │ └── prisma.js │ ├── middleware/ │ │ └── verifyToken.js │ ├── package.json │ ├── prisma/ │ │ └── schema.prisma │ └── routes/ │ ├── auth.route.js │ ├── chat.route.js │ ├── message.route.js │ ├── post.route.js │ ├── test.route.js │ └── user.route.js ├── client/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.jsx │ │ ├── components/ │ │ │ ├── card/ │ │ │ │ ├── Card.jsx │ │ │ │ └── card.scss │ │ │ ├── chat/ │ │ │ │ ├── Chat.jsx │ │ │ │ └── chat.scss │ │ │ ├── filter/ │ │ │ │ ├── Filter.jsx │ │ │ │ └── filter.scss │ │ │ ├── list/ │ │ │ │ ├── List.jsx │ │ │ │ └── list.scss │ │ │ ├── map/ │ │ │ │ ├── Map.jsx │ │ │ │ └── map.scss │ │ │ ├── navbar/ │ │ │ │ ├── Navbar.jsx │ │ │ │ └── navbar.scss │ │ │ ├── pin/ │ │ │ │ ├── Pin.jsx │ │ │ │ └── pin.scss │ │ │ ├── searchBar/ │ │ │ │ ├── SearchBar.jsx │ │ │ │ └── searchBar.scss │ │ │ ├── slider/ │ │ │ │ ├── Slider.jsx │ │ │ │ └── slider.scss │ │ │ └── uploadWidget/ │ │ │ └── UploadWidget.jsx │ │ ├── context/ │ │ │ ├── AuthContext.jsx │ │ │ └── SocketContext.jsx │ │ ├── index.css │ │ ├── index.scss │ │ ├── lib/ │ │ │ ├── apiRequest.js │ │ │ ├── dummydata.js │ │ │ ├── loaders.js │ │ │ └── notificationStore.js │ │ ├── main.jsx │ │ ├── responsive.scss │ │ └── routes/ │ │ ├── homePage/ │ │ │ ├── homePage.jsx │ │ │ └── homePage.scss │ │ ├── layout/ │ │ │ ├── layout.jsx │ │ │ └── layout.scss │ │ ├── listPage/ │ │ │ ├── listPage.jsx │ │ │ └── listPage.scss │ │ ├── login/ │ │ │ ├── login.jsx │ │ │ └── login.scss │ │ ├── newPostPage/ │ │ │ ├── newPostPage.jsx │ │ │ └── newPostPage.scss │ │ ├── profilePage/ │ │ │ ├── profilePage.jsx │ │ │ └── profilePage.scss │ │ ├── profileUpdatePage/ │ │ │ ├── profileUpdatePage.jsx │ │ │ └── profileUpdatePage.scss │ │ ├── register/ │ │ │ ├── register.jsx │ │ │ └── register.scss │ │ └── singlePage/ │ │ ├── singlePage.jsx │ │ └── singlePage.scss │ └── vite.config.js └── socket/ ├── .gitignore ├── app.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: api/.gitignore ================================================ node_modules .env ================================================ FILE: api/app.js ================================================ import express from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; import authRoute from "./routes/auth.route.js"; import postRoute from "./routes/post.route.js"; import testRoute from "./routes/test.route.js"; import userRoute from "./routes/user.route.js"; import chatRoute from "./routes/chat.route.js"; import messageRoute from "./routes/message.route.js"; const app = express(); app.use(cors({ origin: process.env.CLIENT_URL, credentials: true })); app.use(express.json()); app.use(cookieParser()); app.use("/api/auth", authRoute); app.use("/api/users", userRoute); app.use("/api/posts", postRoute); app.use("/api/test", testRoute); app.use("/api/chats", chatRoute); app.use("/api/messages", messageRoute); app.listen(8800, () => { console.log("Server is running!"); }); ================================================ FILE: api/controllers/auth.controller.js ================================================ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import prisma from "../lib/prisma.js"; export const register = async (req, res) => { const { username, email, password } = req.body; try { // HASH THE PASSWORD const hashedPassword = await bcrypt.hash(password, 10); console.log(hashedPassword); // CREATE A NEW USER AND SAVE TO DB const newUser = await prisma.user.create({ data: { username, email, password: hashedPassword, }, }); console.log(newUser); res.status(201).json({ message: "User created successfully" }); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to create user!" }); } }; export const login = async (req, res) => { const { username, password } = req.body; try { // CHECK IF THE USER EXISTS const user = await prisma.user.findUnique({ where: { username }, }); if (!user) return res.status(400).json({ message: "Invalid Credentials!" }); // CHECK IF THE PASSWORD IS CORRECT const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) return res.status(400).json({ message: "Invalid Credentials!" }); // GENERATE COOKIE TOKEN AND SEND TO THE USER // res.setHeader("Set-Cookie", "test=" + "myValue").json("success") const age = 1000 * 60 * 60 * 24 * 7; const token = jwt.sign( { id: user.id, isAdmin: false, }, process.env.JWT_SECRET_KEY, { expiresIn: age } ); const { password: userPassword, ...userInfo } = user; res .cookie("token", token, { httpOnly: true, // secure:true, maxAge: age, }) .status(200) .json(userInfo); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to login!" }); } }; export const logout = (req, res) => { res.clearCookie("token").status(200).json({ message: "Logout Successful" }); }; ================================================ FILE: api/controllers/chat.controller.js ================================================ import prisma from "../lib/prisma.js"; export const getChats = async (req, res) => { const tokenUserId = req.userId; try { const chats = await prisma.chat.findMany({ where: { userIDs: { hasSome: [tokenUserId], }, }, }); for (const chat of chats) { const receiverId = chat.userIDs.find((id) => id !== tokenUserId); const receiver = await prisma.user.findUnique({ where: { id: receiverId, }, select: { id: true, username: true, avatar: true, }, }); chat.receiver = receiver; } res.status(200).json(chats); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get chats!" }); } }; export const getChat = async (req, res) => { const tokenUserId = req.userId; try { const chat = await prisma.chat.findUnique({ where: { id: req.params.id, userIDs: { hasSome: [tokenUserId], }, }, include: { messages: { orderBy: { createdAt: "asc", }, }, }, }); await prisma.chat.update({ where: { id: req.params.id, }, data: { seenBy: { push: [tokenUserId], }, }, }); res.status(200).json(chat); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get chat!" }); } }; export const addChat = async (req, res) => { const tokenUserId = req.userId; try { const newChat = await prisma.chat.create({ data: { userIDs: [tokenUserId, req.body.receiverId], }, }); res.status(200).json(newChat); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to add chat!" }); } }; export const readChat = async (req, res) => { const tokenUserId = req.userId; try { const chat = await prisma.chat.update({ where: { id: req.params.id, userIDs: { hasSome: [tokenUserId], }, }, data: { seenBy: { set: [tokenUserId], }, }, }); res.status(200).json(chat); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to read chat!" }); } }; ================================================ FILE: api/controllers/message.controller.js ================================================ import prisma from "../lib/prisma.js"; export const addMessage = async (req, res) => { const tokenUserId = req.userId; const chatId = req.params.chatId; const text = req.body.text; try { const chat = await prisma.chat.findUnique({ where: { id: chatId, userIDs: { hasSome: [tokenUserId], }, }, }); if (!chat) return res.status(404).json({ message: "Chat not found!" }); const message = await prisma.message.create({ data: { text, chatId, userId: tokenUserId, }, }); await prisma.chat.update({ where: { id: chatId, }, data: { seenBy: [tokenUserId], lastMessage: text, }, }); res.status(200).json(message); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to add message!" }); } }; ================================================ FILE: api/controllers/post.controller.js ================================================ import prisma from "../lib/prisma.js"; import jwt from "jsonwebtoken"; export const getPosts = async (req, res) => { const query = req.query; try { const posts = await prisma.post.findMany({ where: { city: query.city || undefined, type: query.type || undefined, property: query.property || undefined, bedroom: parseInt(query.bedroom) || undefined, price: { gte: parseInt(query.minPrice) || undefined, lte: parseInt(query.maxPrice) || undefined, }, }, }); // setTimeout(() => { res.status(200).json(posts); // }, 3000); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get posts" }); } }; export const getPost = async (req, res) => { const id = req.params.id; try { const post = await prisma.post.findUnique({ where: { id }, include: { postDetail: true, user: { select: { username: true, avatar: true, }, }, }, }); const token = req.cookies?.token; if (token) { jwt.verify(token, process.env.JWT_SECRET_KEY, async (err, payload) => { if (!err) { const saved = await prisma.savedPost.findUnique({ where: { userId_postId: { postId: id, userId: payload.id, }, }, }); res.status(200).json({ ...post, isSaved: saved ? true : false }); } }); } res.status(200).json({ ...post, isSaved: false }); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get post" }); } }; export const addPost = async (req, res) => { const body = req.body; const tokenUserId = req.userId; try { const newPost = await prisma.post.create({ data: { ...body.postData, userId: tokenUserId, postDetail: { create: body.postDetail, }, }, }); res.status(200).json(newPost); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to create post" }); } }; export const updatePost = async (req, res) => { try { res.status(200).json(); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to update posts" }); } }; export const deletePost = async (req, res) => { const id = req.params.id; const tokenUserId = req.userId; try { const post = await prisma.post.findUnique({ where: { id }, }); if (post.userId !== tokenUserId) { return res.status(403).json({ message: "Not Authorized!" }); } await prisma.post.delete({ where: { id }, }); res.status(200).json({ message: "Post deleted" }); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to delete post" }); } }; ================================================ FILE: api/controllers/test.controller.js ================================================ import jwt from "jsonwebtoken"; export const shouldBeLoggedIn = async (req, res) => { console.log(req.userId) res.status(200).json({ message: "You are Authenticated" }); }; export const shouldBeAdmin = async (req, res) => { const token = req.cookies.token; if (!token) return res.status(401).json({ message: "Not Authenticated!" }); jwt.verify(token, process.env.JWT_SECRET_KEY, async (err, payload) => { if (err) return res.status(403).json({ message: "Token is not Valid!" }); if (!payload.isAdmin) { return res.status(403).json({ message: "Not authorized!" }); } }); res.status(200).json({ message: "You are Authenticated" }); }; ================================================ FILE: api/controllers/user.controller.js ================================================ import prisma from "../lib/prisma.js"; import bcrypt from "bcrypt"; export const getUsers = async (req, res) => { try { const users = await prisma.user.findMany(); res.status(200).json(users); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get users!" }); } }; export const getUser = async (req, res) => { const id = req.params.id; try { const user = await prisma.user.findUnique({ where: { id }, }); res.status(200).json(user); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get user!" }); } }; export const updateUser = async (req, res) => { const id = req.params.id; const tokenUserId = req.userId; const { password, avatar, ...inputs } = req.body; if (id !== tokenUserId) { return res.status(403).json({ message: "Not Authorized!" }); } let updatedPassword = null; try { if (password) { updatedPassword = await bcrypt.hash(password, 10); } const updatedUser = await prisma.user.update({ where: { id }, data: { ...inputs, ...(updatedPassword && { password: updatedPassword }), ...(avatar && { avatar }), }, }); const { password: userPassword, ...rest } = updatedUser; res.status(200).json(rest); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to update users!" }); } }; export const deleteUser = async (req, res) => { const id = req.params.id; const tokenUserId = req.userId; if (id !== tokenUserId) { return res.status(403).json({ message: "Not Authorized!" }); } try { await prisma.user.delete({ where: { id }, }); res.status(200).json({ message: "User deleted" }); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to delete users!" }); } }; export const savePost = async (req, res) => { const postId = req.body.postId; const tokenUserId = req.userId; try { const savedPost = await prisma.savedPost.findUnique({ where: { userId_postId: { userId: tokenUserId, postId, }, }, }); if (savedPost) { await prisma.savedPost.delete({ where: { id: savedPost.id, }, }); res.status(200).json({ message: "Post removed from saved list" }); } else { await prisma.savedPost.create({ data: { userId: tokenUserId, postId, }, }); res.status(200).json({ message: "Post saved" }); } } catch (err) { console.log(err); res.status(500).json({ message: "Failed to delete users!" }); } }; export const profilePosts = async (req, res) => { const tokenUserId = req.userId; try { const userPosts = await prisma.post.findMany({ where: { userId: tokenUserId }, }); const saved = await prisma.savedPost.findMany({ where: { userId: tokenUserId }, include: { post: true, }, }); const savedPosts = saved.map((item) => item.post); res.status(200).json({ userPosts, savedPosts }); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get profile posts!" }); } }; export const getNotificationNumber = async (req, res) => { const tokenUserId = req.userId; try { const number = await prisma.chat.count({ where: { userIDs: { hasSome: [tokenUserId], }, NOT: { seenBy: { hasSome: [tokenUserId], }, }, }, }); res.status(200).json(number); } catch (err) { console.log(err); res.status(500).json({ message: "Failed to get profile posts!" }); } }; ================================================ FILE: api/lib/prisma.js ================================================ import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export default prisma; ================================================ FILE: api/middleware/verifyToken.js ================================================ import jwt from "jsonwebtoken"; export const verifyToken = (req, res, next) => { const token = req.cookies.token; if (!token) return res.status(401).json({ message: "Not Authenticated!" }); jwt.verify(token, process.env.JWT_SECRET_KEY, async (err, payload) => { if (err) return res.status(403).json({ message: "Token is not Valid!" }); req.userId = payload.id; next(); }); }; ================================================ FILE: api/package.json ================================================ { "name": "api", "version": "1.0.0", "description": "", "main": "app.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@prisma/client": "^5.11.0", "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.18.3", "jsonwebtoken": "^9.0.2", "prisma": "^5.11.0" } } ================================================ FILE: api/prisma/schema.prisma ================================================ // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client-js" } datasource db { provider = "mongodb" url = env("DATABASE_URL") } model Post { id String @id @default(auto()) @map("_id") @db.ObjectId title String price Int images String[] address String city String bedroom Int bathroom Int latitude String longitude String type Type property Property createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) userId String @db.ObjectId postDetail PostDetail? savedPosts SavedPost[] } enum Type { buy rent } enum Property { apartment house condo land } model PostDetail { id String @id @default(auto()) @map("_id") @db.ObjectId desc String utilities String? pet String? income String? size Int? school Int? bus Int? restaurant Int? post Post @relation(fields: [postId], references: [id]) postId String @unique @db.ObjectId } model SavedPost { id String @id @default(auto()) @map("_id") @db.ObjectId user User @relation(fields: [userId], references: [id]) post Post @relation(fields: [postId], references: [id]) userId String @unique @db.ObjectId postId String @unique @db.ObjectId createdAt DateTime @default(now()) @@unique([userId, postId]) } model User { id String @id @default(auto()) @map("_id") @db.ObjectId email String @unique username String @unique password String avatar String? createdAt DateTime @default(now()) posts Post[] savedPosts SavedPost[] chats Chat[] @relation(fields: [chatIDs], references: [id]) chatIDs String[] @db.ObjectId } model Chat { id String @id @default(auto()) @map("_id") @db.ObjectId users User[] @relation(fields: [userIDs], references: [id]) userIDs String[] @db.ObjectId createdAt DateTime @default(now()) seenBy String[] @db.ObjectId messages Message[] lastMessage String? } model Message { id String @id @default(auto()) @map("_id") @db.ObjectId text String userId String chat Chat @relation(fields: [chatId], references: [id]) chatId String @db.ObjectId createdAt DateTime @default(now()) } ================================================ FILE: api/routes/auth.route.js ================================================ import express from "express"; import { login, logout, register } from "../controllers/auth.controller.js"; const router = express.Router(); router.post("/register", register); router.post("/login", login); router.post("/logout", logout); export default router; ================================================ FILE: api/routes/chat.route.js ================================================ import express from "express"; import { getChats, getChat, addChat, readChat, } from "../controllers/chat.controller.js"; import { verifyToken } from "../middleware/verifyToken.js"; const router = express.Router(); router.get("/", verifyToken, getChats); router.get("/:id", verifyToken, getChat); router.post("/", verifyToken, addChat); router.put("/read/:id", verifyToken, readChat); export default router; ================================================ FILE: api/routes/message.route.js ================================================ import express from "express"; import { addMessage } from "../controllers/message.controller.js"; import {verifyToken} from "../middleware/verifyToken.js"; const router = express.Router(); router.post("/:chatId", verifyToken, addMessage); export default router; ================================================ FILE: api/routes/post.route.js ================================================ import express from "express"; import {verifyToken} from "../middleware/verifyToken.js"; import { addPost, deletePost, getPost, getPosts, updatePost } from "../controllers/post.controller.js"; const router = express.Router(); router.get("/", getPosts); router.get("/:id", getPost); router.post("/", verifyToken, addPost); router.put("/:id", verifyToken, updatePost); router.delete("/:id", verifyToken, deletePost); export default router; ================================================ FILE: api/routes/test.route.js ================================================ import express from "express"; import { shouldBeAdmin, shouldBeLoggedIn } from "../controllers/test.controller.js"; import { verifyToken } from "../middleware/verifyToken.js"; const router = express.Router(); router.get("/should-be-logged-in", verifyToken, shouldBeLoggedIn); router.get("/should-be-admin", shouldBeAdmin); export default router; ================================================ FILE: api/routes/user.route.js ================================================ import express from "express"; import { deleteUser, getUser, getUsers, updateUser, savePost, profilePosts, getNotificationNumber } from "../controllers/user.controller.js"; import {verifyToken} from "../middleware/verifyToken.js"; const router = express.Router(); router.get("/", getUsers); // router.get("/search/:id", verifyToken, getUser); router.put("/:id", verifyToken, updateUser); router.delete("/:id", verifyToken, deleteUser); router.post("/save", verifyToken, savePost); router.get("/profilePosts", verifyToken, profilePosts); router.get("/notification", verifyToken, getNotificationNumber); export default router; ================================================ FILE: client/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, settings: { react: { version: '18.2' } }, plugins: ['react-refresh'], rules: { "react/prop-types": "off", "no-unused-vars": "warn", 'react/jsx-no-target-blank': 'off', 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, } ================================================ FILE: client/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: client/README.md ================================================ # React Real Estate UI Design ================================================ FILE: client/index.html ================================================ Lama Real Estate App
================================================ FILE: client/package.json ================================================ { "name": "estateui", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "axios": "^1.6.7", "dompurify": "^3.0.9", "leaflet": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-leaflet": "^4.2.1", "react-quill": "^2.0.0", "react-router-dom": "^6.22.2", "sass": "^1.71.1", "socket.io-client": "^4.7.5", "timeago.js": "^4.0.2", "zustand": "^4.5.2" }, "devDependencies": { "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "vite": "^5.1.4" } } ================================================ FILE: client/src/App.jsx ================================================ import HomePage from "./routes/homePage/homePage"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import ListPage from "./routes/listPage/listPage"; import { Layout, RequireAuth } from "./routes/layout/layout"; import SinglePage from "./routes/singlePage/singlePage"; import ProfilePage from "./routes/profilePage/profilePage"; import Login from "./routes/login/login"; import Register from "./routes/register/register"; import ProfileUpdatePage from "./routes/profileUpdatePage/profileUpdatePage"; import NewPostPage from "./routes/newPostPage/newPostPage"; import { listPageLoader, profilePageLoader, singlePageLoader } from "./lib/loaders"; function App() { const router = createBrowserRouter([ { path: "/", element: , children: [ { path: "/", element: , }, { path: "/list", element: , loader: listPageLoader, }, { path: "/:id", element: , loader: singlePageLoader, }, { path: "/login", element: , }, { path: "/register", element: , }, ], }, { path: "/", element: , children: [ { path: "/profile", element: , loader: profilePageLoader }, { path: "/profile/update", element: , }, { path: "/add", element: , }, ], }, ]); return ; } export default App; ================================================ FILE: client/src/components/card/Card.jsx ================================================ import { Link } from "react-router-dom"; import "./card.scss"; function Card({ item }) { return (

{item.title}

{item.address}

$ {item.price}

{item.bedroom} bedroom
{item.bathroom} bathroom
); } export default Card; ================================================ FILE: client/src/components/card/card.scss ================================================ @import "../../responsive.scss"; .card { display: flex; gap: 20px; .imageContainer { flex: 2; height: 200px; @include md { display: none; } img { width: 100%; height: 100%; object-fit: cover; border-radius: 10px; } } .textContainer { flex: 3; display: flex; flex-direction: column; justify-content: space-between; gap: 10px; img { width: 16px; height: 16px; } .title { font-size: 20px; font-weight: 600; color: #444; transition: all 0.4s ease; &:hover { color: #000; scale: 1.01; } } .address { font-size: 14px; display: flex; align-items: center; gap: 5px; color: #888; } .price { font-size: 20px; font-weight: 300; padding: 5px; border-radius: 5px; background-color: rgba(254, 205, 81, 0.438); width: max-content; } .bottom { display: flex; justify-content: space-between; gap: 10px; .features { display: flex; gap: 20px; font-size: 14px; .feature { display: flex; align-items: center; gap: 5px; background-color: whitesmoke; padding: 5px; border-radius: 5px; } } .icons { display: flex; gap: 20px; .icon { border: 1px solid #999; padding: 2px 5px; border-radius: 5px; cursor: pointer; display: flex; align-items: center; justify-content: center; &:hover { background-color: lightgray; } } } } } } ================================================ FILE: client/src/components/chat/Chat.jsx ================================================ import { useContext, useEffect, useRef, useState } from "react"; import "./chat.scss"; import { AuthContext } from "../../context/AuthContext"; import apiRequest from "../../lib/apiRequest"; import { format } from "timeago.js"; import { SocketContext } from "../../context/SocketContext"; import { useNotificationStore } from "../../lib/notificationStore"; function Chat({ chats }) { const [chat, setChat] = useState(null); const { currentUser } = useContext(AuthContext); const { socket } = useContext(SocketContext); const messageEndRef = useRef(); const decrease = useNotificationStore((state) => state.decrease); useEffect(() => { messageEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [chat]); const handleOpenChat = async (id, receiver) => { try { const res = await apiRequest("/chats/" + id); if (!res.data.seenBy.includes(currentUser.id)) { decrease(); } setChat({ ...res.data, receiver }); } catch (err) { console.log(err); } }; const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const text = formData.get("text"); if (!text) return; try { const res = await apiRequest.post("/messages/" + chat.id, { text }); setChat((prev) => ({ ...prev, messages: [...prev.messages, res.data] })); e.target.reset(); socket.emit("sendMessage", { receiverId: chat.receiver.id, data: res.data, }); } catch (err) { console.log(err); } }; useEffect(() => { const read = async () => { try { await apiRequest.put("/chats/read/" + chat.id); } catch (err) { console.log(err); } }; if (chat && socket) { socket.on("getMessage", (data) => { if (chat.id === data.chatId) { setChat((prev) => ({ ...prev, messages: [...prev.messages, data] })); read(); } }); } return () => { socket.off("getMessage"); }; }, [socket, chat]); return (

Messages

{chats?.map((c) => (
handleOpenChat(c.id, c.receiver)} > {c.receiver.username}

{c.lastMessage}

))}
{chat && (
{chat.receiver.username}
setChat(null)}> X
{chat.messages.map((message) => (

{message.text}

{format(message.createdAt)}
))}
)}
); } export default Chat; ================================================ FILE: client/src/components/chat/chat.scss ================================================ .chat { height: 100%; display: flex; flex-direction: column; .messages { flex: 1; display: flex; flex-direction: column; gap: 20px; overflow-y: scroll; h1 { font-weight: 300; } .message { background-color: white; padding: 20px; border-radius: 10px; display: flex; align-items: center; gap: 20px; cursor: pointer; img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } span { font-weight: bold; } } } .chatBox { flex: 1; background-color: white; display: flex; flex-direction: column; justify-content: space-between; .top { background-color: #f7c14b85; padding: 20px; font-weight: bold; display: flex; align-items: center; justify-content: space-between; .user { display: flex; align-items: center; gap: 20px; img { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; } } .close { cursor: pointer; } } .center { height: 350px; overflow: scroll; padding: 20px; display: flex; flex-direction: column; gap: 20px; .chatMessage{ width: 50%; &.own{ align-self: flex-end; text-align: right; } span{ font-size: 12px; background-color: #f7c14b39; padding: 2px; border-radius: 5px; } } } .bottom { border-top: 2px solid #f7c14b85; height: 60px; display: flex; align-items: center; justify-content: space-between; textarea{ flex:3; height: 100%; border: none; padding: 20px; } button{ flex:1; background-color: #f7c14b85; height: 100%; border: none; cursor: pointer; } } } } ================================================ FILE: client/src/components/filter/Filter.jsx ================================================ import { useState } from "react"; import "./filter.scss"; import { useSearchParams } from "react-router-dom"; function Filter() { const [searchParams, setSearchParams] = useSearchParams(); const [query, setQuery] = useState({ type: searchParams.get("type") || "", city: searchParams.get("city") || "", property: searchParams.get("property") || "", minPrice: searchParams.get("minPrice") || "", maxPrice: searchParams.get("maxPrice") || "", bedroom: searchParams.get("bedroom") || "", }); const handleChange = (e) => { setQuery({ ...query, [e.target.name]: e.target.value, }); }; const handleFilter = () => { setSearchParams(query); }; return (

Search results for {searchParams.get("city")}

); } export default Filter; ================================================ FILE: client/src/components/filter/filter.scss ================================================ .filter { display: flex; flex-direction: column; gap: 10px; h1 { font-weight: 300; font-size: 24px; } .item { display: flex; flex-direction: column; gap: 2px; label { font-size: 10px; } input, select { width: 100px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 5px; font-size: 14px; } } .top { input { width: 100%; } } .bottom { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 20px; button { width: 100px; padding: 10px; border: none; cursor: pointer; background-color: #fece51; img { width: 24px; height: 24px; } } } } ================================================ FILE: client/src/components/list/List.jsx ================================================ import './list.scss' import Card from"../card/Card" function List({posts}){ return (
{posts.map(item=>( ))}
) } export default List ================================================ FILE: client/src/components/list/list.scss ================================================ .list{ display: flex; flex-direction: column; gap: 50px; } ================================================ FILE: client/src/components/map/Map.jsx ================================================ import { MapContainer, TileLayer } from "react-leaflet"; import "./map.scss"; import "leaflet/dist/leaflet.css"; import Pin from "../pin/Pin"; function Map({ items }) { return ( {items.map((item) => ( ))} ); } export default Map; ================================================ FILE: client/src/components/map/map.scss ================================================ .map{ width: 100%; height: 100%; border-radius: 20px; } ================================================ FILE: client/src/components/navbar/Navbar.jsx ================================================ import { useContext, useState } from "react"; import "./navbar.scss"; import { Link } from "react-router-dom"; import { AuthContext } from "../../context/AuthContext"; import { useNotificationStore } from "../../lib/notificationStore"; function Navbar() { const [open, setOpen] = useState(false); const { currentUser } = useContext(AuthContext); const fetch = useNotificationStore((state) => state.fetch); const number = useNotificationStore((state) => state.number); if(currentUser) fetch(); return ( ); } export default Navbar; ================================================ FILE: client/src/components/navbar/navbar.scss ================================================ @import "../../responsive.scss"; nav { height: 100px; display: flex; justify-content: space-between; align-items: center; a { transition: all 0.4s ease; @include sm { display: none; } &:hover { scale: 1.05; } } .left { flex: 3; display: flex; align-items: center; gap: 50px; .logo { font-weight: bold; font-size: 20px; display: flex; align-items: center; gap: 10px; img { width: 28px; } span { @include md { display: none; } @include sm { display: initial; } } } } .right { flex: 2; display: flex; align-items: center; justify-content: flex-end; background-color: #fcf5f3; height: 100%; @include md { background-color: transparent; } a { padding: 12px 24px; margin: 20px; } .user{ display: flex; align-items: center; font-weight: bold; img{ width: 40px; height: 40px; border-radius: 50%; object-fit: cover; margin-right: 20px; } span{ @include sm{ display: none; } } .profile{ padding: 12px 24px; background-color: #fece51; cursor: pointer; border: none; position: relative; .notification{ position: absolute; top: -8px; right: -8px; background-color: red; color: white; border-radius: 50%; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; } } } .register { background-color: #fece51; } .menuIcon { display: none; z-index: 999; img { width: 36px; height: 36px; cursor: pointer; } @include sm { display: inline; } } .menu { position: absolute; top: 0; right: -50%; background-color: black; color: white; height: 100vh; width: 50%; transition: all 1s ease; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 24px; &.active { right: 0; } @include sm { a { display: initial; } } } } } ================================================ FILE: client/src/components/pin/Pin.jsx ================================================ import { Marker, Popup } from "react-leaflet"; import "./pin.scss"; import { Link } from "react-router-dom"; function Pin({ item }) { return (
{item.title} {item.bedroom} bedroom $ {item.price}
); } export default Pin; ================================================ FILE: client/src/components/pin/pin.scss ================================================ .popupContainer{ display: flex; gap: 20px; img{ width: 64px; height: 48px; object-fit: cover; border-radius: 5px; } .textContainer{ display: flex; flex-direction: column; justify-content: space-between; } } ================================================ FILE: client/src/components/searchBar/SearchBar.jsx ================================================ import { useState } from "react"; import "./searchBar.scss"; import { Link } from "react-router-dom"; const types = ["buy", "rent"]; function SearchBar() { const [query, setQuery] = useState({ type: "buy", city: "", minPrice: 0, maxPrice: 0, }); const switchType = (val) => { setQuery((prev) => ({ ...prev, type: val })); }; const handleChange = (e) => { setQuery((prev) => ({ ...prev, [e.target.name]: e.target.value })); }; return (
{types.map((type) => ( ))}
); } export default SearchBar; ================================================ FILE: client/src/components/searchBar/searchBar.scss ================================================ @import "../../responsive.scss"; .searchBar { .type { button { padding: 16px 36px; border: 1px solid #999; border-bottom: none; cursor: pointer; background-color: white; text-transform: capitalize; &.active { background-color: black; color: white; } &:first-child { border-top-left-radius: 5px; border-right: none; } &:last-child { border-top-right-radius: 5px; border-left: none; } } } form { border: 1px solid #999; display: flex; justify-content: space-between; height: 64px; gap: 5px; @include sm { flex-direction: column; border: none; } input { border: none; padding: 0px 10px; width: 200px; @include lg { padding: 0px 5px; &:nth-child(2), &:nth-child(3) { width: 140px; } } @include md { width: 200px; &:nth-child(2), &:nth-child(3) { width: 200px; } } @include sm{ width: auto; &:nth-child(2), &:nth-child(3) { width: auto; } padding: 20px; border: 1px solid #999; } } a{ flex: 1; background-color: #fece51; display: flex; align-items: center; justify-content: center; button { border: none; cursor: pointer; background-color: #fece51; @include sm{ padding: 10px; } img { width: 24px; height: 24px; } } } } } ================================================ FILE: client/src/components/slider/Slider.jsx ================================================ import { useState } from "react"; import "./slider.scss"; function Slider({ images }) { const [imageIndex, setImageIndex] = useState(null); const changeSlide = (direction) => { if (direction === "left") { if (imageIndex === 0) { setImageIndex(images.length - 1); } else { setImageIndex(imageIndex - 1); } } else { if (imageIndex === images.length - 1) { setImageIndex(0); } else { setImageIndex(imageIndex + 1); } } }; return (
{imageIndex !== null && (
changeSlide("left")}>
changeSlide("right")}>
setImageIndex(null)}> X
)}
setImageIndex(0)} />
{images.slice(1).map((image, index) => ( setImageIndex(index + 1)} /> ))}
); } export default Slider; ================================================ FILE: client/src/components/slider/slider.scss ================================================ @import "../../responsive.scss"; .slider { width: 100%; height: 350px; display: flex; gap: 20px; @include sm { height: 280px; } .fullSlider { position: absolute; width: 100vw; height: 100vh; top: 0; left: 0; background-color: black; display: flex; justify-content: space-between; align-items: center; z-index: 9999; .arrow { flex: 1; display: flex; align-items: center; justify-content: center; img { width: 50px; @include md { width: 30px; } @include sm { width: 20px; } &.right { transform: rotate(180deg); } } } .imgContainer { flex: 10; img { width: 100%; height: 100%; object-fit: cover; } } .close { position: absolute; top: 0; right: 0; color: white; font-size: 36px; font-weight: bold; padding: 50px; cursor: pointer; } } img { width: 100%; height: 100%; object-fit: cover; border-radius: 10px; cursor: pointer; } .bigImage { flex: 3; @include sm { flex: 2; } } .smallImages { flex: 1; display: flex; flex-direction: column; justify-content: space-between; gap: 20px; @include sm { flex: 1; } img { height: 100px; @include sm { height: 80px; } } } } ================================================ FILE: client/src/components/uploadWidget/UploadWidget.jsx ================================================ import { createContext, useEffect, useState } from "react"; // Create a context to manage the script loading state const CloudinaryScriptContext = createContext(); function UploadWidget({ uwConfig, setPublicId, setState }) { const [loaded, setLoaded] = useState(false); useEffect(() => { // Check if the script is already loaded if (!loaded) { const uwScript = document.getElementById("uw"); if (!uwScript) { // If not loaded, create and load the script const script = document.createElement("script"); script.setAttribute("async", ""); script.setAttribute("id", "uw"); script.src = "https://upload-widget.cloudinary.com/global/all.js"; script.addEventListener("load", () => setLoaded(true)); document.body.appendChild(script); } else { // If already loaded, update the state setLoaded(true); } } }, [loaded]); const initializeCloudinaryWidget = () => { if (loaded) { var myWidget = window.cloudinary.createUploadWidget( uwConfig, (error, result) => { if (!error && result && result.event === "success") { console.log("Done! Here is the image info: ", result.info); setState((prev) => [...prev, result.info.secure_url]); } } ); document.getElementById("upload_widget").addEventListener( "click", function () { myWidget.open(); }, false ); } }; return ( ); } export default UploadWidget; export { CloudinaryScriptContext }; ================================================ FILE: client/src/context/AuthContext.jsx ================================================ import { createContext, useEffect, useState } from "react"; export const AuthContext = createContext(); export const AuthContextProvider = ({ children }) => { const [currentUser, setCurrentUser] = useState( JSON.parse(localStorage.getItem("user")) || null ); const updateUser = (data) => { setCurrentUser(data); }; useEffect(() => { localStorage.setItem("user", JSON.stringify(currentUser)); }, [currentUser]); return ( {children} ); }; ================================================ FILE: client/src/context/SocketContext.jsx ================================================ import { createContext, useContext, useEffect, useState } from "react"; import { io } from "socket.io-client"; import { AuthContext } from "./AuthContext"; export const SocketContext = createContext(); export const SocketContextProvider = ({ children }) => { const { currentUser } = useContext(AuthContext); const [socket, setSocket] = useState(null); useEffect(() => { setSocket(io("http://localhost:4000")); }, []); useEffect(() => { currentUser && socket?.emit("newUser", currentUser.id); }, [currentUser, socket]); return ( {children} ); }; ================================================ FILE: client/src/index.css ================================================ ================================================ FILE: client/src/index.scss ================================================ *{ padding: 0; margin: 0; box-sizing: border-box; } a { text-decoration: none; color: inherit; } body{ font-family: "Lato", sans-serif; overflow: hidden; } ================================================ FILE: client/src/lib/apiRequest.js ================================================ import axios from "axios"; const apiRequest = axios.create({ baseURL: "http://localhost:8800/api", withCredentials: true, }); export default apiRequest; ================================================ FILE: client/src/lib/dummydata.js ================================================ export const listData = [ { id: 1, title: "A Great Apartment Next to the Beach!", images: ["https://images.pexels.com/photos/1918291/pexels-photo-1918291.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 2, bathroom: 1, price: 1000, address: "456 Park Avenue, London", latitude: 51.5074, longitude: -0.1278, }, { id: 2, title: "An Awesome Apartment Near the Park! Almost too good to be true!", images: ["https://images.pexels.com/photos/1428348/pexels-photo-1428348.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 3, bathroom: 2, price: 1500, address: "789 Oxford Street, London", latitude: 52.4862, longitude: -1.8904, }, { id: 3, title: "A New Apartment in the City!", images: ["https://images.pexels.com/photos/2062426/pexels-photo-2062426.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 1, bathroom: 1, price: 800, address: "101 Baker Street, London", latitude: 53.4808, longitude: -2.2426, }, { id: 4, title: "Great Location! Great Price! Great Apartment!", images: ["https://images.pexels.com/photos/2467285/pexels-photo-2467285.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 2, bathroom: 1, price: 1000, address: "234 Kingsway, London,", latitude: 53.8008, longitude: -1.5491, }, { id: 5, title: "Apartment 5", images: ["https://images.pexels.com/photos/276625/pexels-photo-276625.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 3, bathroom: 2, price: 1500, address: "567 Victoria Road, London", latitude: 53.4084, longitude: -2.9916, }, { id: 6, title: "Apartment 6", images: ["https://images.pexels.com/photos/271816/pexels-photo-271816.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 1, bathroom: 1, price: 800, address: "890 Regent Street, London", latitude: 54.9783, longitude: -1.6174, }, { id: 7, title: "Apartment 7", images: ["https://images.pexels.com/photos/2029667/pexels-photo-2029667.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 2, bathroom: 1, price: 1000, address: "112 Piccadilly, London", latitude: 53.3811, longitude: -1.4701, }, { id: 8, title: "Apartment 8", images: ["https://images.pexels.com/photos/276724/pexels-photo-276724.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"], bedroom: 3, bathroom: 2, price: 1500, address: "8765 Main High Street, London", latitude: 51.4545, longitude: -2.5879, }, ]; export const singlePostData = { id: 1, title: "Beautiful Apartment", price: 1200, images: [ "https://images.pexels.com/photos/1918291/pexels-photo-1918291.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", "https://images.pexels.com/photos/1428348/pexels-photo-1428348.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", "https://images.pexels.com/photos/2062426/pexels-photo-2062426.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", "https://images.pexels.com/photos/2467285/pexels-photo-2467285.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", ], bedRooms: 2, bathroom: 1, size: 861, latitude: 51.5074, longitude: -0.1278, city: "London", address: "1234 Broadway St", school: "250m away", bus: "100m away", restaurant: "50m away", description: "Future alike hill pull picture swim magic chain seed engineer nest outer raise bound easy poetry gain loud weigh me recognize farmer bare danger. actually put square leg vessels earth engine matter key cup indeed body film century shut place environment were stage vertical roof bottom lady function breeze darkness beside tin view local breathe carbon swam declared magnet escape has from pile apart route coffee storm someone hold space use ahead sheep jungle closely natural attached part top grain your grade trade corn salmon trouble new bend most teacher range anybody every seat fifteen eventually", }; export const userData = { id: 1, name: "John Doe", img: "https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", }; ================================================ FILE: client/src/lib/loaders.js ================================================ import { defer } from "react-router-dom"; import apiRequest from "./apiRequest"; export const singlePageLoader = async ({ request, params }) => { const res = await apiRequest("/posts/" + params.id); return res.data; }; export const listPageLoader = async ({ request, params }) => { const query = request.url.split("?")[1]; const postPromise = apiRequest("/posts?" + query); return defer({ postResponse: postPromise, }); }; export const profilePageLoader = async () => { const postPromise = apiRequest("/users/profilePosts"); const chatPromise = apiRequest("/chats"); return defer({ postResponse: postPromise, chatResponse: chatPromise, }); }; ================================================ FILE: client/src/lib/notificationStore.js ================================================ import { create } from "zustand"; import apiRequest from "./apiRequest"; export const useNotificationStore = create((set) => ({ number: 0, fetch: async () => { const res = await apiRequest("/users/notification"); set({ number: res.data }); }, decrease: () => { set((prev) => ({ number: prev.number - 1 })); }, reset: () => { set({ number: 0 }); }, })); ================================================ FILE: client/src/main.jsx ================================================ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.scss"; import { AuthContextProvider } from "./context/AuthContext.jsx"; import { SocketContextProvider } from "./context/SocketContext.jsx"; ReactDOM.createRoot(document.getElementById("root")).render( ); ================================================ FILE: client/src/responsive.scss ================================================ @mixin sm { @media (max-width: 738px) { @content; } } @mixin md { @media (max-width: 1024px) { @content; } } @mixin lg { @media (max-width: 1366px) { @content; } } ================================================ FILE: client/src/routes/homePage/homePage.jsx ================================================ import { useContext } from "react"; import SearchBar from "../../components/searchBar/SearchBar"; import "./homePage.scss"; import { AuthContext } from "../../context/AuthContext"; function HomePage() { const {currentUser} = useContext(AuthContext) return (

Find Real Estate & Get Your Dream Place

Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos explicabo suscipit cum eius, iure est nulla animi consequatur facilis id pariatur fugit quos laudantium temporibus dolor ea repellat provident impedit!

16+

Years of Experience

200

Award Gained

2000+

Property Ready

); } export default HomePage; ================================================ FILE: client/src/routes/homePage/homePage.scss ================================================ @import "../../responsive.scss"; .homePage { display: flex; height: 100%; .textContainer { flex: 3; .wrapper { padding-right: 100px; display: flex; flex-direction: column; justify-content: center; gap: 50px; height: 100%; @include lg{ padding-right: 50px; } @include md{ padding: 0; } @include sm{ justify-content: flex-start; } .title { font-size: 64px; @include lg { font-size: 48px; } } .boxes { display: flex; justify-content: space-between; @include sm { display: none; } h1 { font-size: 36px; @include lg { font-size: 32px; } } h2 { font-size: 20px; font-weight: 300; } } } } .imgContainer { flex: 2; background-color: #fcf5f3; position: relative; display: flex; align-items: center; @include md { display: none; } img { width: 115%; position: absolute; right: 0; @include lg { width: 105%; } } } } ================================================ FILE: client/src/routes/layout/layout.jsx ================================================ import "./layout.scss"; import Navbar from "../../components/navbar/Navbar"; import { Navigate, Outlet } from "react-router-dom"; import { useContext } from "react"; import { AuthContext } from "../../context/AuthContext"; function Layout() { return (
); } function RequireAuth() { const { currentUser } = useContext(AuthContext); if (!currentUser) return ; else { return (
); } } export { Layout, RequireAuth }; ================================================ FILE: client/src/routes/layout/layout.scss ================================================ @import "../../responsive.scss"; .layout { height: 100vh; max-width: 1366px; margin-left: auto; margin-right: auto; padding-left: 20px; padding-right: 20px; display: flex; flex-direction: column; @include lg { // background-color: rgb(247, 210, 196); max-width: 1280px; } @include md { // background-color: rgb(186, 203, 234); max-width: 768px; } @include sm { // background-color: rgb(239, 200, 200); max-width: 640px; } .content { // flex: 1; height: calc(100vh - 100px); } } ================================================ FILE: client/src/routes/listPage/listPage.jsx ================================================ import "./listPage.scss"; import Filter from "../../components/filter/Filter"; import Card from "../../components/card/Card"; import Map from "../../components/map/Map"; import { Await, useLoaderData } from "react-router-dom"; import { Suspense } from "react"; function ListPage() { const data = useLoaderData(); return (
Loading...

}> Error loading posts!

} > {(postResponse) => postResponse.data.map((post) => ( )) }
Loading...

}> Error loading posts!

} > {(postResponse) => }
); } export default ListPage; ================================================ FILE: client/src/routes/listPage/listPage.scss ================================================ @import "../../responsive.scss"; .listPage { display: flex; height: 100%; .listContainer { flex: 3; height: 100%; .wrapper{ height: 100%; padding-right: 50px; display: flex; flex-direction: column; gap: 50px; overflow-y: scroll; padding-bottom: 50px; } } .mapContainer { flex: 2; height: 100%; background-color: #fcf5f3; @include md{ display: none; } } } ================================================ FILE: client/src/routes/login/login.jsx ================================================ import { useContext, useState } from "react"; import "./login.scss"; import { Link, useNavigate } from "react-router-dom"; import apiRequest from "../../lib/apiRequest"; import { AuthContext } from "../../context/AuthContext"; function Login() { const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); const {updateUser} = useContext(AuthContext) const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(""); const formData = new FormData(e.target); const username = formData.get("username"); const password = formData.get("password"); try { const res = await apiRequest.post("/auth/login", { username, password, }); updateUser(res.data) navigate("/"); } catch (err) { setError(err.response.data.message); } finally { setIsLoading(false); } }; return (

Welcome back

{error && {error}} {"Don't"} you have an account?
); } export default Login; ================================================ FILE: client/src/routes/login/login.scss ================================================ .login { height: 100%; display: flex; .formContainer { flex: 3; height: 100%; display: flex; align-items: center; justify-content: center; form { display: flex; flex-direction: column; gap: 20px; input{ padding: 20px; border: 1px solid gray; border-radius: 5px; } button{ padding: 20px; border-radius: 5px; border: none; background-color: teal; color: white; font-weight: bold; cursor: pointer; &:disabled{ background-color: #BED9D8; cursor: not-allowed; } } span{ color: rgba(255, 0, 0, 0.591); } a{ font-size: 14px; color: gray; border-bottom: 1px solid gray; width: max-content; } } } .imgContainer { flex: 2; background-color: #fcf5f3; display: flex; align-items: center; justify-content: center; img { width: 100%; } } } ================================================ FILE: client/src/routes/newPostPage/newPostPage.jsx ================================================ import { useState } from "react"; import "./newPostPage.scss"; import ReactQuill from "react-quill"; import "react-quill/dist/quill.snow.css"; import apiRequest from "../../lib/apiRequest"; import UploadWidget from "../../components/uploadWidget/UploadWidget"; import { useNavigate } from "react-router-dom"; function NewPostPage() { const [value, setValue] = useState(""); const [images, setImages] = useState([]); const [error, setError] = useState(""); const navigate = useNavigate() const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const inputs = Object.fromEntries(formData); try { const res = await apiRequest.post("/posts", { postData: { title: inputs.title, price: parseInt(inputs.price), address: inputs.address, city: inputs.city, bedroom: parseInt(inputs.bedroom), bathroom: parseInt(inputs.bathroom), type: inputs.type, property: inputs.property, latitude: inputs.latitude, longitude: inputs.longitude, images: images, }, postDetail: { desc: value, utilities: inputs.utilities, pet: inputs.pet, income: inputs.income, size: parseInt(inputs.size), school: parseInt(inputs.school), bus: parseInt(inputs.bus), restaurant: parseInt(inputs.restaurant), }, }); navigate("/"+res.data.id) } catch (err) { console.log(err); setError(error); } }; return (

Add New Post

{error && error}
{images.map((image, index) => ( ))}
); } export default NewPostPage; ================================================ FILE: client/src/routes/newPostPage/newPostPage.scss ================================================ .newPostPage { height: 100%; display: flex; .formContainer { flex: 3; overflow: scroll; .wrapper { margin: 30px 50px 100px 0px; form { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 20px; .item { width: 30%; display: flex; flex-direction: column; gap: 5px; label { } input { padding: 20px; border-radius: 5px; border: 1px solid gray; } select { padding: 19px; } &.description { width: 100%; height: 320px; .quill > .ql-container > .ql-editor{ height: 200px; font-size: 16px; } } } .sendButton { width: 30%; border-radius: 5px; border: none; background-color: teal; color: white; font-weight: bold; cursor: pointer; } } } } .sideContainer { flex: 2; background-color: #fcf5f3; display: flex; flex-direction: column; gap: 20px; align-items: center; justify-content: center; img { width: 50%; height: 180px; object-fit: cover; border-radius: 5px; } } } ================================================ FILE: client/src/routes/profilePage/profilePage.jsx ================================================ import Chat from "../../components/chat/Chat"; import List from "../../components/list/List"; import "./profilePage.scss"; import apiRequest from "../../lib/apiRequest"; import { Await, Link, useLoaderData, useNavigate } from "react-router-dom"; import { Suspense, useContext } from "react"; import { AuthContext } from "../../context/AuthContext"; function ProfilePage() { const data = useLoaderData(); const { updateUser, currentUser } = useContext(AuthContext); const navigate = useNavigate(); const handleLogout = async () => { try { await apiRequest.post("/auth/logout"); updateUser(null); navigate("/"); } catch (err) { console.log(err); } }; return (

User Information

Avatar: Username: {currentUser.username} E-mail: {currentUser.email}

My List

Loading...

}> Error loading posts!

} > {(postResponse) => }

Saved List

Loading...

}> Error loading posts!

} > {(postResponse) => }
Loading...

}> Error loading chats!

} > {(chatResponse) => }
); } export default ProfilePage; ================================================ FILE: client/src/routes/profilePage/profilePage.scss ================================================ @import "../../responsive.scss"; .profilePage { display: flex; height: 100%; @include md{ flex-direction: column; overflow: scroll; } .details { flex: 3; overflow-y: scroll; padding-bottom: 50px; @include md{ flex:none; height: max-content; } .wrapper{ padding-right: 50px; display: flex; flex-direction: column; gap: 50px; .title{ display: flex; align-items: center; justify-content: space-between; h1{ font-weight: 300; } button{ padding: 12px 24px; background-color: #fece51; cursor: pointer; border: none; } } .info{ display: flex; flex-direction: column; gap: 20px; span{ display: flex; align-items: center; gap: 20px; } button{ width: 100px; background-color: teal; border: none; color: white; padding: 10px 20px; cursor: pointer; border-radius: 5px; } img{ width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } } } } .chatContainer { flex: 2; background-color: #fcf5f3; height: 100%; @include md{ flex:none; height: max-content; } .wrapper{ padding: 0px 20px; height: 100%; } } } ================================================ FILE: client/src/routes/profileUpdatePage/profileUpdatePage.jsx ================================================ import { useContext, useState } from "react"; import "./profileUpdatePage.scss"; import { AuthContext } from "../../context/AuthContext"; import apiRequest from "../../lib/apiRequest"; import { useNavigate } from "react-router-dom"; import UploadWidget from "../../components/uploadWidget/UploadWidget"; function ProfileUpdatePage() { const { currentUser, updateUser } = useContext(AuthContext); const [error, setError] = useState(""); const [avatar, setAvatar] = useState([]); const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const { username, email, password } = Object.fromEntries(formData); try { const res = await apiRequest.put(`/users/${currentUser.id}`, { username, email, password, avatar:avatar[0] }); updateUser(res.data); navigate("/profile"); } catch (err) { console.log(err); setError(err.response.data.message); } }; return (

Update Profile

{error && error}
); } export default ProfileUpdatePage; ================================================ FILE: client/src/routes/profileUpdatePage/profileUpdatePage.scss ================================================ .profileUpdatePage { height: 100%; display: flex; .formContainer { flex:3; display: flex; align-items: center; justify-content: center; form { display: flex; flex-direction: column; gap: 20px; .item { display: flex; flex-direction: column; gap: 5px; label { } input { padding: 20px; border-radius: 5px; border: 1px solid gray; } } button { padding: 20px; border-radius: 5px; border: none; background-color: teal; color: white; font-weight: bold; cursor: pointer; } } } .sideContainer { flex: 2; background-color: #fcf5f3; display: flex; flex-direction: column; gap: 20px; align-items: center; justify-content: center; .avatar{ width: 50%; object-fit: cover; } } } ================================================ FILE: client/src/routes/register/register.jsx ================================================ import "./register.scss"; import { Link, useNavigate } from "react-router-dom"; import axios from "axios"; import { useState } from "react"; import apiRequest from "../../lib/apiRequest"; function Register() { const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); setError("") setIsLoading(true); const formData = new FormData(e.target); const username = formData.get("username"); const email = formData.get("email"); const password = formData.get("password"); try { const res = await apiRequest.post("/auth/register", { username, email, password, }); navigate("/login"); } catch (err) { setError(err.response.data.message); } finally { setIsLoading(false); } }; return (

Create an Account

{error && {error}} Do you have an account?
); } export default Register; ================================================ FILE: client/src/routes/register/register.scss ================================================ .registerPage { height: 100%; display: flex; .formContainer { flex: 3; height: 100%; display: flex; align-items: center; justify-content: center; form { display: flex; flex-direction: column; gap: 20px; input { padding: 20px; border: 1px solid gray; border-radius: 5px; } button { padding: 20px; border-radius: 5px; border: none; background-color: teal; color: white; font-weight: bold; cursor: pointer; &:disabled { background-color: #bed9d8; cursor: not-allowed; } } span { color: rgba(255, 0, 0, 0.591); } a { font-size: 14px; color: gray; border-bottom: 1px solid gray; width: max-content; } } } .imgContainer { flex: 2; background-color: #fcf5f3; display: flex; align-items: center; justify-content: center; img { width: 100%; } } } ================================================ FILE: client/src/routes/singlePage/singlePage.jsx ================================================ import "./singlePage.scss"; import Slider from "../../components/slider/Slider"; import Map from "../../components/map/Map"; import { useNavigate, useLoaderData } from "react-router-dom"; import DOMPurify from "dompurify"; import { useContext, useState } from "react"; import { AuthContext } from "../../context/AuthContext"; import apiRequest from "../../lib/apiRequest"; function SinglePage() { const post = useLoaderData(); const [saved, setSaved] = useState(post.isSaved); const { currentUser } = useContext(AuthContext); const navigate = useNavigate(); const handleSave = async () => { if (!currentUser) { navigate("/login"); } // AFTER REACT 19 UPDATE TO USEOPTIMISTIK HOOK setSaved((prev) => !prev); try { await apiRequest.post("/users/save", { postId: post.id }); } catch (err) { console.log(err); setSaved((prev) => !prev); } }; return (

{post.title}

{post.address}
$ {post.price}
{post.user.username}

General

Utilities {post.postDetail.utilities === "owner" ? (

Owner is responsible

) : (

Tenant is responsible

)}
Pet Policy {post.postDetail.pet === "allowed" ? (

Pets Allowed

) : (

Pets not Allowed

)}
Income Policy

{post.postDetail.income}

Sizes

{post.postDetail.size} sqft
{post.bedroom} beds
{post.bathroom} bathroom

Nearby Places

School

{post.postDetail.school > 999 ? post.postDetail.school / 1000 + "km" : post.postDetail.school + "m"}{" "} away

Bus Stop

{post.postDetail.bus}m away

Restaurant

{post.postDetail.restaurant}m away

Location

); } export default SinglePage; ================================================ FILE: client/src/routes/singlePage/singlePage.scss ================================================ @import "../../responsive.scss"; .singlePage { display: flex; height: 100%; @include md { flex-direction: column; overflow: scroll; } .details { flex: 3; height: 100%; overflow-y: scroll; @include md { flex: none; height: max-content; margin-bottom: 50px; } .wrapper { padding-right: 50px; @include lg { padding-right: 20px; } @include md { padding-right: 0px; } .info { margin-top: 50px; .top { display: flex; justify-content: space-between; @include sm { flex-direction: column; gap: 20px; } .post { display: flex; flex-direction: column; gap: 20px; h1 { font-weight: 400; } .address { display: flex; gap: 5px; align-items: center; color: #888; font-size: 14px; img { width: 16px; height: 16px; } } .price { padding: 5px; background-color: rgba(254, 205, 81, 0.438); border-radius: 5px; width: max-content; font-size: 20px; font-weight: 300; } } .user { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; padding: 0px 50px; border-radius: 10px; background-color: rgba(254, 205, 81, 0.209); font-weight: 600; @include sm { padding: 20px 50px; } img { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; } } } .bottom { margin-top: 50px; color: #555; line-height: 20px; } } } } .features { flex: 2; background-color: #fcf5f3; height: 100%; overflow-y: scroll; @include md { flex: none; height: max-content; margin-bottom: 50px; } .wrapper { padding: 0px 20px; display: flex; flex-direction: column; gap: 20px; @include md { padding: 20px; } img { width: 24px; height: 24px; } .title { font-weight: bold; font-size: 18px; margin-bottom: 10px; } .feature { display: flex; align-items: center; gap: 10px; img { background-color: rgba(254, 205, 81, 0.209); } .featureText { span { font-weight: bold; } p { font-size: 14px; } } } .listVertical { display: flex; flex-direction: column; gap: 20px; padding: 20px 10px; background-color: white; border-radius: 10px; } .listHorizontal { display: flex; justify-content: space-between; padding: 20px 10px; background-color: white; border-radius: 10px; } .sizes { display: flex; justify-content: space-between; @include lg { font-size: 12px; } .size { display: flex; align-items: center; gap: 10px; background-color: white; padding: 10px; border-radius: 5px; } } .mapContainer { width: 100%; height: 200px; } .buttons { display: flex; justify-content: space-between; button { padding: 20px; display: flex; align-items: center; gap: 5px; background-color: white; border: 1px solid #fece51; border-radius: 5px; cursor: pointer; img { width: 16px; height: 16px; } } } } } } ================================================ FILE: client/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: socket/.gitignore ================================================ node_modules ================================================ FILE: socket/app.js ================================================ import { Server } from "socket.io"; const io = new Server({ cors: { origin: "http://localhost:5173", }, }); let onlineUser = []; const addUser = (userId, socketId) => { const userExits = onlineUser.find((user) => user.userId === userId); if (!userExits) { onlineUser.push({ userId, socketId }); } }; const removeUser = (socketId) => { onlineUser = onlineUser.filter((user) => user.socketId !== socketId); }; const getUser = (userId) => { return onlineUser.find((user) => user.userId === userId); }; io.on("connection", (socket) => { socket.on("newUser", (userId) => { addUser(userId, socket.id); }); socket.on("sendMessage", ({ receiverId, data }) => { const receiver = getUser(receiverId); io.to(receiver.socketId).emit("getMessage", data); }); socket.on("disconnect", () => { removeUser(socket.id); }); }); io.listen("4000"); ================================================ FILE: socket/package.json ================================================ { "name": "socket", "version": "1.0.0", "description": "", "main": "app.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "socket.io": "^4.7.5" } }