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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lama Real Estate App</title>
<meta name="description" content="Buy, Sell and Rent Properties" />
<link
href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
================================================
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: <Layout />,
children: [
{
path: "/",
element: <HomePage />,
},
{
path: "/list",
element: <ListPage />,
loader: listPageLoader,
},
{
path: "/:id",
element: <SinglePage />,
loader: singlePageLoader,
},
{
path: "/login",
element: <Login />,
},
{
path: "/register",
element: <Register />,
},
],
},
{
path: "/",
element: <RequireAuth />,
children: [
{
path: "/profile",
element: <ProfilePage />,
loader: profilePageLoader
},
{
path: "/profile/update",
element: <ProfileUpdatePage />,
},
{
path: "/add",
element: <NewPostPage />,
},
],
},
]);
return <RouterProvider router={router} />;
}
export default App;
================================================
FILE: client/src/components/card/Card.jsx
================================================
import { Link } from "react-router-dom";
import "./card.scss";
function Card({ item }) {
return (
<div className="card">
<Link to={`/${item.id}`} className="imageContainer">
<img src={item.images[0]} alt="" />
</Link>
<div className="textContainer">
<h2 className="title">
<Link to={`/${item.id}`}>{item.title}</Link>
</h2>
<p className="address">
<img src="/pin.png" alt="" />
<span>{item.address}</span>
</p>
<p className="price">$ {item.price}</p>
<div className="bottom">
<div className="features">
<div className="feature">
<img src="/bed.png" alt="" />
<span>{item.bedroom} bedroom</span>
</div>
<div className="feature">
<img src="/bath.png" alt="" />
<span>{item.bathroom} bathroom</span>
</div>
</div>
<div className="icons">
<div className="icon">
<img src="/save.png" alt="" />
</div>
<div className="icon">
<img src="/chat.png" alt="" />
</div>
</div>
</div>
</div>
</div>
);
}
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 (
<div className="chat">
<div className="messages">
<h1>Messages</h1>
{chats?.map((c) => (
<div
className="message"
key={c.id}
style={{
backgroundColor:
c.seenBy.includes(currentUser.id) || chat?.id === c.id
? "white"
: "#fecd514e",
}}
onClick={() => handleOpenChat(c.id, c.receiver)}
>
<img src={c.receiver.avatar || "/noavatar.jpg"} alt="" />
<span>{c.receiver.username}</span>
<p>{c.lastMessage}</p>
</div>
))}
</div>
{chat && (
<div className="chatBox">
<div className="top">
<div className="user">
<img src={chat.receiver.avatar || "noavatar.jpg"} alt="" />
{chat.receiver.username}
</div>
<span className="close" onClick={() => setChat(null)}>
X
</span>
</div>
<div className="center">
{chat.messages.map((message) => (
<div
className="chatMessage"
style={{
alignSelf:
message.userId === currentUser.id
? "flex-end"
: "flex-start",
textAlign:
message.userId === currentUser.id ? "right" : "left",
}}
key={message.id}
>
<p>{message.text}</p>
<span>{format(message.createdAt)}</span>
</div>
))}
<div ref={messageEndRef}></div>
</div>
<form onSubmit={handleSubmit} className="bottom">
<textarea name="text"></textarea>
<button>Send</button>
</form>
</div>
)}
</div>
);
}
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 (
<div className="filter">
<h1>
Search results for <b>{searchParams.get("city")}</b>
</h1>
<div className="top">
<div className="item">
<label htmlFor="city">Location</label>
<input
type="text"
id="city"
name="city"
placeholder="City Location"
onChange={handleChange}
defaultValue={query.city}
/>
</div>
</div>
<div className="bottom">
<div className="item">
<label htmlFor="type">Type</label>
<select
name="type"
id="type"
onChange={handleChange}
defaultValue={query.type}
>
<option value="">any</option>
<option value="buy">Buy</option>
<option value="rent">Rent</option>
</select>
</div>
<div className="item">
<label htmlFor="property">Property</label>
<select
name="property"
id="property"
onChange={handleChange}
defaultValue={query.property}
>
<option value="">any</option>
<option value="apartment">Apartment</option>
<option value="house">House</option>
<option value="condo">Condo</option>
<option value="land">Land</option>
</select>
</div>
<div className="item">
<label htmlFor="minPrice">Min Price</label>
<input
type="number"
id="minPrice"
name="minPrice"
placeholder="any"
onChange={handleChange}
defaultValue={query.minPrice}
/>
</div>
<div className="item">
<label htmlFor="maxPrice">Max Price</label>
<input
type="text"
id="maxPrice"
name="maxPrice"
placeholder="any"
onChange={handleChange}
defaultValue={query.maxPrice}
/>
</div>
<div className="item">
<label htmlFor="bedroom">Bedroom</label>
<input
type="text"
id="bedroom"
name="bedroom"
placeholder="any"
onChange={handleChange}
defaultValue={query.bedroom}
/>
</div>
<button onClick={handleFilter}>
<img src="/search.png" alt="" />
</button>
</div>
</div>
);
}
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 (
<div className='list'>
{posts.map(item=>(
<Card key={item.id} item={item}/>
))}
</div>
)
}
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 (
<MapContainer
center={
items.length === 1
? [items[0].latitude, items[0].longitude]
: [52.4797, -1.90269]
}
zoom={7}
scrollWheelZoom={false}
className="map"
>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{items.map((item) => (
<Pin item={item} key={item.id} />
))}
</MapContainer>
);
}
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 (
<nav>
<div className="left">
<a href="/" className="logo">
<img src="/logo.png" alt="" />
<span>LamaEstate</span>
</a>
<a href="/">Home</a>
<a href="/">About</a>
<a href="/">Contact</a>
<a href="/">Agents</a>
</div>
<div className="right">
{currentUser ? (
<div className="user">
<img src={currentUser.avatar || "/noavatar.jpg"} alt="" />
<span>{currentUser.username}</span>
<Link to="/profile" className="profile">
{number > 0 && <div className="notification">{number}</div>}
<span>Profile</span>
</Link>
</div>
) : (
<>
<a href="/login">Sign in</a>
<a href="/register" className="register">
Sign up
</a>
</>
)}
<div className="menuIcon">
<img
src="/menu.png"
alt=""
onClick={() => setOpen((prev) => !prev)}
/>
</div>
<div className={open ? "menu active" : "menu"}>
<a href="/">Home</a>
<a href="/">About</a>
<a href="/">Contact</a>
<a href="/">Agents</a>
<a href="/">Sign in</a>
<a href="/">Sign up</a>
</div>
</div>
</nav>
);
}
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 (
<Marker position={[item.latitude, item.longitude]}>
<Popup>
<div className="popupContainer">
<img src={item.images[0]} alt="" />
<div className="textContainer">
<Link to={`/${item.id}`}>{item.title}</Link>
<span>{item.bedroom} bedroom</span>
<b>$ {item.price}</b>
</div>
</div>
</Popup>
</Marker>
);
}
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 (
<div className="searchBar">
<div className="type">
{types.map((type) => (
<button
key={type}
onClick={() => switchType(type)}
className={query.type === type ? "active" : ""}
>
{type}
</button>
))}
</div>
<form>
<input
type="text"
name="city"
placeholder="City"
onChange={handleChange}
/>
<input
type="number"
name="minPrice"
min={0}
max={10000000}
placeholder="Min Price"
onChange={handleChange}
/>
<input
type="number"
name="maxPrice"
min={0}
max={10000000}
placeholder="Max Price"
onChange={handleChange}
/>
<Link
to={`/list?type=${query.type}&city=${query.city}&minPrice=${query.minPrice}&maxPrice=${query.maxPrice}`}
>
<button>
<img src="/search.png" alt="" />
</button>
</Link>
</form>
</div>
);
}
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 (
<div className="slider">
{imageIndex !== null && (
<div className="fullSlider">
<div className="arrow" onClick={() => changeSlide("left")}>
<img src="/arrow.png" alt="" />
</div>
<div className="imgContainer">
<img src={images[imageIndex]} alt="" />
</div>
<div className="arrow" onClick={() => changeSlide("right")}>
<img src="/arrow.png" className="right" alt="" />
</div>
<div className="close" onClick={() => setImageIndex(null)}>
X
</div>
</div>
)}
<div className="bigImage">
<img src={images[0]} alt="" onClick={() => setImageIndex(0)} />
</div>
<div className="smallImages">
{images.slice(1).map((image, index) => (
<img
src={image}
alt=""
key={index}
onClick={() => setImageIndex(index + 1)}
/>
))}
</div>
</div>
);
}
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 (
<CloudinaryScriptContext.Provider value={{ loaded }}>
<button
id="upload_widget"
className="cloudinary-button"
onClick={initializeCloudinaryWidget}
>
Upload
</button>
</CloudinaryScriptContext.Provider>
);
}
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 (
<AuthContext.Provider value={{ currentUser,updateUser }}>
{children}
</AuthContext.Provider>
);
};
================================================
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 (
<SocketContext.Provider value={{ socket }}>
{children}
</SocketContext.Provider>
);
};
================================================
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(
<React.StrictMode>
<AuthContextProvider>
<SocketContextProvider>
<App />
</SocketContextProvider>
</AuthContextProvider>
</React.StrictMode>
);
================================================
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 (
<div className="homePage">
<div className="textContainer">
<div className="wrapper">
<h1 className="title">Find Real Estate & Get Your Dream Place</h1>
<p>
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!
</p>
<SearchBar />
<div className="boxes">
<div className="box">
<h1>16+</h1>
<h2>Years of Experience</h2>
</div>
<div className="box">
<h1>200</h1>
<h2>Award Gained</h2>
</div>
<div className="box">
<h1>2000+</h1>
<h2>Property Ready</h2>
</div>
</div>
</div>
</div>
<div className="imgContainer">
<img src="/bg.png" alt="" />
</div>
</div>
);
}
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 (
<div className="layout">
<div className="navbar">
<Navbar />
</div>
<div className="content">
<Outlet />
</div>
</div>
);
}
function RequireAuth() {
const { currentUser } = useContext(AuthContext);
if (!currentUser) return <Navigate to="/login" />;
else {
return (
<div className="layout">
<div className="navbar">
<Navbar />
</div>
<div className="content">
<Outlet />
</div>
</div>
);
}
}
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 (
<div className="listPage">
<div className="listContainer">
<div className="wrapper">
<Filter />
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={data.postResponse}
errorElement={<p>Error loading posts!</p>}
>
{(postResponse) =>
postResponse.data.map((post) => (
<Card key={post.id} item={post} />
))
}
</Await>
</Suspense>
</div>
</div>
<div className="mapContainer">
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={data.postResponse}
errorElement={<p>Error loading posts!</p>}
>
{(postResponse) => <Map items={postResponse.data} />}
</Await>
</Suspense>
</div>
</div>
);
}
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 (
<div className="login">
<div className="formContainer">
<form onSubmit={handleSubmit}>
<h1>Welcome back</h1>
<input
name="username"
required
minLength={3}
maxLength={20}
type="text"
placeholder="Username"
/>
<input
name="password"
type="password"
required
placeholder="Password"
/>
<button disabled={isLoading}>Login</button>
{error && <span>{error}</span>}
<Link to="/register">{"Don't"} you have an account?</Link>
</form>
</div>
<div className="imgContainer">
<img src="/bg.png" alt="" />
</div>
</div>
);
}
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 (
<div className="newPostPage">
<div className="formContainer">
<h1>Add New Post</h1>
<div className="wrapper">
<form onSubmit={handleSubmit}>
<div className="item">
<label htmlFor="title">Title</label>
<input id="title" name="title" type="text" />
</div>
<div className="item">
<label htmlFor="price">Price</label>
<input id="price" name="price" type="number" />
</div>
<div className="item">
<label htmlFor="address">Address</label>
<input id="address" name="address" type="text" />
</div>
<div className="item description">
<label htmlFor="desc">Description</label>
<ReactQuill theme="snow" onChange={setValue} value={value} />
</div>
<div className="item">
<label htmlFor="city">City</label>
<input id="city" name="city" type="text" />
</div>
<div className="item">
<label htmlFor="bedroom">Bedroom Number</label>
<input min={1} id="bedroom" name="bedroom" type="number" />
</div>
<div className="item">
<label htmlFor="bathroom">Bathroom Number</label>
<input min={1} id="bathroom" name="bathroom" type="number" />
</div>
<div className="item">
<label htmlFor="latitude">Latitude</label>
<input id="latitude" name="latitude" type="text" />
</div>
<div className="item">
<label htmlFor="longitude">Longitude</label>
<input id="longitude" name="longitude" type="text" />
</div>
<div className="item">
<label htmlFor="type">Type</label>
<select name="type">
<option value="rent" defaultChecked>
Rent
</option>
<option value="buy">Buy</option>
</select>
</div>
<div className="item">
<label htmlFor="type">Property</label>
<select name="property">
<option value="apartment">Apartment</option>
<option value="house">House</option>
<option value="condo">Condo</option>
<option value="land">Land</option>
</select>
</div>
<div className="item">
<label htmlFor="utilities">Utilities Policy</label>
<select name="utilities">
<option value="owner">Owner is responsible</option>
<option value="tenant">Tenant is responsible</option>
<option value="shared">Shared</option>
</select>
</div>
<div className="item">
<label htmlFor="pet">Pet Policy</label>
<select name="pet">
<option value="allowed">Allowed</option>
<option value="not-allowed">Not Allowed</option>
</select>
</div>
<div className="item">
<label htmlFor="income">Income Policy</label>
<input
id="income"
name="income"
type="text"
placeholder="Income Policy"
/>
</div>
<div className="item">
<label htmlFor="size">Total Size (sqft)</label>
<input min={0} id="size" name="size" type="number" />
</div>
<div className="item">
<label htmlFor="school">School</label>
<input min={0} id="school" name="school" type="number" />
</div>
<div className="item">
<label htmlFor="bus">bus</label>
<input min={0} id="bus" name="bus" type="number" />
</div>
<div className="item">
<label htmlFor="restaurant">Restaurant</label>
<input min={0} id="restaurant" name="restaurant" type="number" />
</div>
<button className="sendButton">Add</button>
{error && <span>error</span>}
</form>
</div>
</div>
<div className="sideContainer">
{images.map((image, index) => (
<img src={image} key={index} alt="" />
))}
<UploadWidget
uwConfig={{
multiple: true,
cloudName: "lamadev",
uploadPreset: "estate",
folder: "posts",
}}
setState={setImages}
/>
</div>
</div>
);
}
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 (
<div className="profilePage">
<div className="details">
<div className="wrapper">
<div className="title">
<h1>User Information</h1>
<Link to="/profile/update">
<button>Update Profile</button>
</Link>
</div>
<div className="info">
<span>
Avatar:
<img src={currentUser.avatar || "noavatar.jpg"} alt="" />
</span>
<span>
Username: <b>{currentUser.username}</b>
</span>
<span>
E-mail: <b>{currentUser.email}</b>
</span>
<button onClick={handleLogout}>Logout</button>
</div>
<div className="title">
<h1>My List</h1>
<Link to="/add">
<button>Create New Post</button>
</Link>
</div>
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={data.postResponse}
errorElement={<p>Error loading posts!</p>}
>
{(postResponse) => <List posts={postResponse.data.userPosts} />}
</Await>
</Suspense>
<div className="title">
<h1>Saved List</h1>
</div>
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={data.postResponse}
errorElement={<p>Error loading posts!</p>}
>
{(postResponse) => <List posts={postResponse.data.savedPosts} />}
</Await>
</Suspense>
</div>
</div>
<div className="chatContainer">
<div className="wrapper">
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={data.chatResponse}
errorElement={<p>Error loading chats!</p>}
>
{(chatResponse) => <Chat chats={chatResponse.data}/>}
</Await>
</Suspense>
</div>
</div>
</div>
);
}
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 (
<div className="profileUpdatePage">
<div className="formContainer">
<form onSubmit={handleSubmit}>
<h1>Update Profile</h1>
<div className="item">
<label htmlFor="username">Username</label>
<input
id="username"
name="username"
type="text"
defaultValue={currentUser.username}
/>
</div>
<div className="item">
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
defaultValue={currentUser.email}
/>
</div>
<div className="item">
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button>Update</button>
{error && <span>error</span>}
</form>
</div>
<div className="sideContainer">
<img src={avatar[0] || currentUser.avatar || "/noavatar.jpg"} alt="" className="avatar" />
<UploadWidget
uwConfig={{
cloudName: "lamadev",
uploadPreset: "estate",
multiple: false,
maxImageFileSize: 2000000,
folder: "avatars",
}}
setState={setAvatar}
/>
</div>
</div>
);
}
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 (
<div className="registerPage">
<div className="formContainer">
<form onSubmit={handleSubmit}>
<h1>Create an Account</h1>
<input name="username" type="text" placeholder="Username" />
<input name="email" type="text" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button disabled={isLoading}>Register</button>
{error && <span>{error}</span>}
<Link to="/login">Do you have an account?</Link>
</form>
</div>
<div className="imgContainer">
<img src="/bg.png" alt="" />
</div>
</div>
);
}
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 (
<div className="singlePage">
<div className="details">
<div className="wrapper">
<Slider images={post.images} />
<div className="info">
<div className="top">
<div className="post">
<h1>{post.title}</h1>
<div className="address">
<img src="/pin.png" alt="" />
<span>{post.address}</span>
</div>
<div className="price">$ {post.price}</div>
</div>
<div className="user">
<img src={post.user.avatar} alt="" />
<span>{post.user.username}</span>
</div>
</div>
<div
className="bottom"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(post.postDetail.desc),
}}
></div>
</div>
</div>
</div>
<div className="features">
<div className="wrapper">
<p className="title">General</p>
<div className="listVertical">
<div className="feature">
<img src="/utility.png" alt="" />
<div className="featureText">
<span>Utilities</span>
{post.postDetail.utilities === "owner" ? (
<p>Owner is responsible</p>
) : (
<p>Tenant is responsible</p>
)}
</div>
</div>
<div className="feature">
<img src="/pet.png" alt="" />
<div className="featureText">
<span>Pet Policy</span>
{post.postDetail.pet === "allowed" ? (
<p>Pets Allowed</p>
) : (
<p>Pets not Allowed</p>
)}
</div>
</div>
<div className="feature">
<img src="/fee.png" alt="" />
<div className="featureText">
<span>Income Policy</span>
<p>{post.postDetail.income}</p>
</div>
</div>
</div>
<p className="title">Sizes</p>
<div className="sizes">
<div className="size">
<img src="/size.png" alt="" />
<span>{post.postDetail.size} sqft</span>
</div>
<div className="size">
<img src="/bed.png" alt="" />
<span>{post.bedroom} beds</span>
</div>
<div className="size">
<img src="/bath.png" alt="" />
<span>{post.bathroom} bathroom</span>
</div>
</div>
<p className="title">Nearby Places</p>
<div className="listHorizontal">
<div className="feature">
<img src="/school.png" alt="" />
<div className="featureText">
<span>School</span>
<p>
{post.postDetail.school > 999
? post.postDetail.school / 1000 + "km"
: post.postDetail.school + "m"}{" "}
away
</p>
</div>
</div>
<div className="feature">
<img src="/pet.png" alt="" />
<div className="featureText">
<span>Bus Stop</span>
<p>{post.postDetail.bus}m away</p>
</div>
</div>
<div className="feature">
<img src="/fee.png" alt="" />
<div className="featureText">
<span>Restaurant</span>
<p>{post.postDetail.restaurant}m away</p>
</div>
</div>
</div>
<p className="title">Location</p>
<div className="mapContainer">
<Map items={[post]} />
</div>
<div className="buttons">
<button>
<img src="/chat.png" alt="" />
Send a Message
</button>
<button
onClick={handleSave}
style={{
backgroundColor: saved ? "#fece51" : "white",
}}
>
<img src="/save.png" alt="" />
{saved ? "Place Saved" : "Save the Place"}
</button>
</div>
</div>
</div>
</div>
);
}
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"
}
}
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
SYMBOL INDEX (21 symbols across 20 files)
FILE: client/src/App.jsx
function App (line 13) | function App() {
FILE: client/src/components/card/Card.jsx
function Card (line 4) | function Card({ item }) {
FILE: client/src/components/chat/Chat.jsx
function Chat (line 9) | function Chat({ chats }) {
FILE: client/src/components/filter/Filter.jsx
function Filter (line 5) | function Filter() {
FILE: client/src/components/list/List.jsx
function List (line 4) | function List({posts}){
FILE: client/src/components/map/Map.jsx
function Map (line 6) | function Map({ items }) {
FILE: client/src/components/navbar/Navbar.jsx
function Navbar (line 7) | function Navbar() {
FILE: client/src/components/pin/Pin.jsx
function Pin (line 5) | function Pin({ item }) {
FILE: client/src/components/searchBar/SearchBar.jsx
function SearchBar (line 7) | function SearchBar() {
FILE: client/src/components/slider/Slider.jsx
function Slider (line 4) | function Slider({ images }) {
FILE: client/src/components/uploadWidget/UploadWidget.jsx
function UploadWidget (line 6) | function UploadWidget({ uwConfig, setPublicId, setState }) {
FILE: client/src/routes/homePage/homePage.jsx
function HomePage (line 6) | function HomePage() {
FILE: client/src/routes/layout/layout.jsx
function Layout (line 7) | function Layout() {
function RequireAuth (line 20) | function RequireAuth() {
FILE: client/src/routes/listPage/listPage.jsx
function ListPage (line 8) | function ListPage() {
FILE: client/src/routes/login/login.jsx
function Login (line 7) | function Login() {
FILE: client/src/routes/newPostPage/newPostPage.jsx
function NewPostPage (line 9) | function NewPostPage() {
FILE: client/src/routes/profilePage/profilePage.jsx
function ProfilePage (line 9) | function ProfilePage() {
FILE: client/src/routes/profileUpdatePage/profileUpdatePage.jsx
function ProfileUpdatePage (line 8) | function ProfileUpdatePage() {
FILE: client/src/routes/register/register.jsx
function Register (line 7) | function Register() {
FILE: client/src/routes/singlePage/singlePage.jsx
function SinglePage (line 10) | function SinglePage() {
Condensed preview — 75 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (106K chars).
[
{
"path": "api/.gitignore",
"chars": 17,
"preview": "node_modules\n.env"
},
{
"path": "api/app.js",
"chars": 813,
"preview": "import express from \"express\";\nimport cors from \"cors\";\nimport cookieParser from \"cookie-parser\";\nimport authRoute from "
},
{
"path": "api/controllers/auth.controller.js",
"chars": 1999,
"preview": "import bcrypt from \"bcrypt\";\nimport jwt from \"jsonwebtoken\";\nimport prisma from \"../lib/prisma.js\";\n\nexport const regist"
},
{
"path": "api/controllers/chat.controller.js",
"chars": 2320,
"preview": "import prisma from \"../lib/prisma.js\";\n\nexport const getChats = async (req, res) => {\n const tokenUserId = req.userId;\n"
},
{
"path": "api/controllers/message.controller.js",
"chars": 892,
"preview": "import prisma from \"../lib/prisma.js\";\n\nexport const addMessage = async (req, res) => {\n const tokenUserId = req.userId"
},
{
"path": "api/controllers/post.controller.js",
"chars": 2898,
"preview": "import prisma from \"../lib/prisma.js\";\nimport jwt from \"jsonwebtoken\";\n\nexport const getPosts = async (req, res) => {\n "
},
{
"path": "api/controllers/test.controller.js",
"chars": 670,
"preview": "import jwt from \"jsonwebtoken\";\n\nexport const shouldBeLoggedIn = async (req, res) => {\n console.log(req.userId)\n res.s"
},
{
"path": "api/controllers/user.controller.js",
"chars": 3732,
"preview": "import prisma from \"../lib/prisma.js\";\nimport bcrypt from \"bcrypt\";\n\nexport const getUsers = async (req, res) => {\n try"
},
{
"path": "api/lib/prisma.js",
"chars": 107,
"preview": "import { PrismaClient } from \"@prisma/client\";\n\nconst prisma = new PrismaClient();\n\nexport default prisma;\n"
},
{
"path": "api/middleware/verifyToken.js",
"chars": 400,
"preview": "import jwt from \"jsonwebtoken\";\n\nexport const verifyToken = (req, res, next) => {\n const token = req.cookies.token;\n\n "
},
{
"path": "api/package.json",
"chars": 450,
"preview": "{\n \"name\": \"api\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"app.js\",\n \"type\": \"module\",\n \"scripts\": {\n "
},
{
"path": "api/prisma/schema.prisma",
"chars": 2663,
"preview": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for "
},
{
"path": "api/routes/auth.route.js",
"chars": 265,
"preview": "import express from \"express\";\nimport { login, logout, register } from \"../controllers/auth.controller.js\";\n\nconst route"
},
{
"path": "api/routes/chat.route.js",
"chars": 419,
"preview": "import express from \"express\";\nimport {\n getChats,\n getChat,\n addChat,\n readChat,\n} from \"../controllers/chat.contro"
},
{
"path": "api/routes/message.route.js",
"chars": 268,
"preview": "import express from \"express\";\nimport {\n addMessage\n} from \"../controllers/message.controller.js\";\nimport {verifyToken}"
},
{
"path": "api/routes/post.route.js",
"chars": 441,
"preview": "import express from \"express\";\nimport {verifyToken} from \"../middleware/verifyToken.js\";\nimport { addPost, deletePost, g"
},
{
"path": "api/routes/test.route.js",
"chars": 350,
"preview": "import express from \"express\";\nimport { shouldBeAdmin, shouldBeLoggedIn } from \"../controllers/test.controller.js\";\nimpo"
},
{
"path": "api/routes/user.route.js",
"chars": 642,
"preview": "import express from \"express\";\nimport {\n deleteUser,\n getUser,\n getUsers,\n updateUser,\n savePost,\n profilePosts,\n "
},
{
"path": "client/.eslintrc.cjs",
"chars": 627,
"preview": "module.exports = {\n root: true,\n env: { browser: true, es2020: true },\n extends: [\n 'eslint:recommended',\n 'plu"
},
{
"path": "client/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "client/README.md",
"chars": 29,
"preview": "# React Real Estate UI Design"
},
{
"path": "client/index.html",
"chars": 626,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "client/package.json",
"chars": 933,
"preview": "{\n \"name\": \"estateui\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n "
},
{
"path": "client/src/App.jsx",
"chars": 1739,
"preview": "import HomePage from \"./routes/homePage/homePage\";\nimport { createBrowserRouter, RouterProvider } from \"react-router-dom"
},
{
"path": "client/src/components/card/Card.jsx",
"chars": 1263,
"preview": "import { Link } from \"react-router-dom\";\nimport \"./card.scss\";\n\nfunction Card({ item }) {\n return (\n <div className="
},
{
"path": "client/src/components/card/card.scss",
"chars": 1747,
"preview": "@import \"../../responsive.scss\";\n\n.card {\n display: flex;\n gap: 20px;\n\n .imageContainer {\n flex: 2;\n height: 20"
},
{
"path": "client/src/components/chat/Chat.jsx",
"chars": 3984,
"preview": "import { useContext, useEffect, useRef, useState } from \"react\";\nimport \"./chat.scss\";\nimport { AuthContext } from \"../."
},
{
"path": "client/src/components/chat/chat.scss",
"chars": 2038,
"preview": ".chat {\n height: 100%;\n display: flex;\n flex-direction: column;\n\n .messages {\n flex: 1;\n display: flex;\n fl"
},
{
"path": "client/src/components/filter/Filter.jsx",
"chars": 3226,
"preview": "import { useState } from \"react\";\nimport \"./filter.scss\";\nimport { useSearchParams } from \"react-router-dom\";\n\nfunction "
},
{
"path": "client/src/components/filter/filter.scss",
"chars": 755,
"preview": ".filter {\n display: flex;\n flex-direction: column;\n gap: 10px;\n\n h1 {\n font-weight: 300;\n font-size: 24px;\n }"
},
{
"path": "client/src/components/list/List.jsx",
"chars": 229,
"preview": "import './list.scss'\nimport Card from\"../card/Card\"\n\nfunction List({posts}){\n return (\n <div className='list'>\n "
},
{
"path": "client/src/components/list/list.scss",
"chars": 64,
"preview": ".list{\n display: flex;\n flex-direction: column;\n gap: 50px;\n}"
},
{
"path": "client/src/components/map/Map.jsx",
"chars": 736,
"preview": "import { MapContainer, TileLayer } from \"react-leaflet\";\nimport \"./map.scss\";\nimport \"leaflet/dist/leaflet.css\";\nimport "
},
{
"path": "client/src/components/map/map.scss",
"chars": 61,
"preview": ".map{\n width: 100%;\n height: 100%;\n border-radius: 20px;\n}"
},
{
"path": "client/src/components/navbar/Navbar.jsx",
"chars": 1914,
"preview": "import { useContext, useState } from \"react\";\nimport \"./navbar.scss\";\nimport { Link } from \"react-router-dom\";\nimport { "
},
{
"path": "client/src/components/navbar/navbar.scss",
"chars": 2473,
"preview": "@import \"../../responsive.scss\";\n\nnav {\n height: 100px;\n display: flex;\n justify-content: space-between;\n align-item"
},
{
"path": "client/src/components/pin/Pin.jsx",
"chars": 573,
"preview": "import { Marker, Popup } from \"react-leaflet\";\nimport \"./pin.scss\";\nimport { Link } from \"react-router-dom\";\n\nfunction P"
},
{
"path": "client/src/components/pin/pin.scss",
"chars": 248,
"preview": ".popupContainer{\n display: flex;\n gap: 20px;\n\n img{\n width: 64px;\n height: 48px;\n object-fit: cover;\n bor"
},
{
"path": "client/src/components/searchBar/SearchBar.jsx",
"chars": 1610,
"preview": "import { useState } from \"react\";\nimport \"./searchBar.scss\";\nimport { Link } from \"react-router-dom\";\n\nconst types = [\"b"
},
{
"path": "client/src/components/searchBar/searchBar.scss",
"chars": 1664,
"preview": "@import \"../../responsive.scss\";\n\n.searchBar {\n .type {\n\n\n button {\n padding: 16px 36px;\n border: 1px soli"
},
{
"path": "client/src/components/slider/Slider.jsx",
"chars": 1551,
"preview": "import { useState } from \"react\";\nimport \"./slider.scss\";\n\nfunction Slider({ images }) {\n const [imageIndex, setImageIn"
},
{
"path": "client/src/components/slider/slider.scss",
"chars": 1488,
"preview": "@import \"../../responsive.scss\";\n\n.slider {\n width: 100%;\n height: 350px;\n display: flex;\n gap: 20px;\n\n @include sm"
},
{
"path": "client/src/components/uploadWidget/UploadWidget.jsx",
"chars": 1855,
"preview": "import { createContext, useEffect, useState } from \"react\";\n\n// Create a context to manage the script loading state\ncons"
},
{
"path": "client/src/context/AuthContext.jsx",
"chars": 566,
"preview": "import { createContext, useEffect, useState } from \"react\";\n\nexport const AuthContext = createContext();\n\nexport const A"
},
{
"path": "client/src/context/SocketContext.jsx",
"chars": 656,
"preview": "import { createContext, useContext, useEffect, useState } from \"react\";\nimport { io } from \"socket.io-client\";\nimport { "
},
{
"path": "client/src/index.css",
"chars": 0,
"preview": ""
},
{
"path": "client/src/index.scss",
"chars": 171,
"preview": "*{\n padding: 0;\n margin: 0;\n box-sizing: border-box;\n}\n\na {\n text-decoration: none;\n color: inherit;\n}\n\nbody{\n fon"
},
{
"path": "client/src/lib/apiRequest.js",
"chars": 158,
"preview": "import axios from \"axios\";\n\nconst apiRequest = axios.create({\n baseURL: \"http://localhost:8800/api\",\n withCredentials:"
},
{
"path": "client/src/lib/dummydata.js",
"chars": 4247,
"preview": "export const listData = [\n {\n id: 1,\n title: \"A Great Apartment Next to the Beach!\",\n images: [\"https://images"
},
{
"path": "client/src/lib/loaders.js",
"chars": 677,
"preview": "import { defer } from \"react-router-dom\";\nimport apiRequest from \"./apiRequest\";\n\nexport const singlePageLoader = async "
},
{
"path": "client/src/lib/notificationStore.js",
"chars": 384,
"preview": "import { create } from \"zustand\";\nimport apiRequest from \"./apiRequest\";\n\nexport const useNotificationStore = create((se"
},
{
"path": "client/src/main.jsx",
"chars": 492,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"./App.jsx\";\nimport \"./index.scss\";\n"
},
{
"path": "client/src/responsive.scss",
"chars": 190,
"preview": "@mixin sm {\n @media (max-width: 738px) {\n @content;\n }\n}\n\n@mixin md {\n @media (max-width: 1024px) {\n @content;\n"
},
{
"path": "client/src/routes/homePage/homePage.jsx",
"chars": 1328,
"preview": "import { useContext } from \"react\";\nimport SearchBar from \"../../components/searchBar/SearchBar\";\nimport \"./homePage.scs"
},
{
"path": "client/src/routes/homePage/homePage.scss",
"chars": 1216,
"preview": "@import \"../../responsive.scss\";\n\n.homePage {\n display: flex;\n height: 100%;\n\n .textContainer {\n flex: 3;\n\n .wr"
},
{
"path": "client/src/routes/layout/layout.jsx",
"chars": 811,
"preview": "import \"./layout.scss\";\nimport Navbar from \"../../components/navbar/Navbar\";\nimport { Navigate, Outlet } from \"react-rou"
},
{
"path": "client/src/routes/layout/layout.scss",
"chars": 547,
"preview": "@import \"../../responsive.scss\";\n\n.layout {\n height: 100vh;\n max-width: 1366px;\n margin-left: auto;\n margin-right: a"
},
{
"path": "client/src/routes/listPage/listPage.jsx",
"chars": 1247,
"preview": "import \"./listPage.scss\";\nimport Filter from \"../../components/filter/Filter\";\nimport Card from \"../../components/card/C"
},
{
"path": "client/src/routes/listPage/listPage.scss",
"chars": 455,
"preview": "@import \"../../responsive.scss\";\n\n.listPage {\n display: flex;\n height: 100%;\n\n .listContainer {\n flex: 3;\n heig"
},
{
"path": "client/src/routes/login/login.jsx",
"chars": 1754,
"preview": "import { useContext, useState } from \"react\";\nimport \"./login.scss\";\nimport { Link, useNavigate } from \"react-router-dom"
},
{
"path": "client/src/routes/login/login.scss",
"chars": 1091,
"preview": ".login {\r\n height: 100%;\r\n display: flex;\r\n\r\n .formContainer {\r\n flex: 3;\r\n height: 100%;\r\n display: flex;\r\n"
},
{
"path": "client/src/routes/newPostPage/newPostPage.jsx",
"chars": 6327,
"preview": "import { useState } from \"react\";\nimport \"./newPostPage.scss\";\nimport ReactQuill from \"react-quill\";\nimport \"react-quill"
},
{
"path": "client/src/routes/newPostPage/newPostPage.scss",
"chars": 1367,
"preview": ".newPostPage {\n height: 100%;\n display: flex;\n\n .formContainer {\n flex: 3;\n overflow: scroll;\n\n .wrapper {\n "
},
{
"path": "client/src/routes/profilePage/profilePage.jsx",
"chars": 2764,
"preview": "import Chat from \"../../components/chat/Chat\";\nimport List from \"../../components/list/List\";\nimport \"./profilePage.scss"
},
{
"path": "client/src/routes/profilePage/profilePage.scss",
"chars": 1497,
"preview": "@import \"../../responsive.scss\";\n\n.profilePage {\n display: flex;\n height: 100%;\n\n @include md{\n flex-direction: co"
},
{
"path": "client/src/routes/profileUpdatePage/profileUpdatePage.jsx",
"chars": 2463,
"preview": "import { useContext, useState } from \"react\";\nimport \"./profileUpdatePage.scss\";\nimport { AuthContext } from \"../../cont"
},
{
"path": "client/src/routes/profileUpdatePage/profileUpdatePage.scss",
"chars": 938,
"preview": ".profileUpdatePage {\n height: 100%;\n display: flex;\n\n .formContainer {\n flex:3;\n display: flex;\n align-items"
},
{
"path": "client/src/routes/register/register.jsx",
"chars": 1591,
"preview": "import \"./register.scss\";\nimport { Link, useNavigate } from \"react-router-dom\";\nimport axios from \"axios\";\nimport { useS"
},
{
"path": "client/src/routes/register/register.scss",
"chars": 1042,
"preview": ".registerPage {\n height: 100%;\n display: flex;\n\n .formContainer {\n flex: 3;\n height: 100%;\n display: flex;\n "
},
{
"path": "client/src/routes/singlePage/singlePage.jsx",
"chars": 5347,
"preview": "import \"./singlePage.scss\";\nimport Slider from \"../../components/slider/Slider\";\nimport Map from \"../../components/map/M"
},
{
"path": "client/src/routes/singlePage/singlePage.scss",
"chars": 4197,
"preview": "@import \"../../responsive.scss\";\n\n.singlePage {\n display: flex;\n height: 100%;\n\n @include md {\n flex-direction: co"
},
{
"path": "client/vite.config.js",
"chars": 163,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
},
{
"path": "socket/.gitignore",
"chars": 12,
"preview": "node_modules"
},
{
"path": "socket/app.js",
"chars": 887,
"preview": "import { Server } from \"socket.io\";\n\nconst io = new Server({\n cors: {\n origin: \"http://localhost:5173\",\n },\n});\n\nle"
},
{
"path": "socket/package.json",
"chars": 289,
"preview": "{\n \"name\": \"socket\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"app.js\",\n \"type\": \"module\",\n \"scripts\": {\n"
}
]
About this extraction
This page contains the full source code of the safak/full-stack-estate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 75 files (93.7 KB), approximately 26.5k tokens, and a symbol index with 21 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.