real estate client and server

This commit is contained in:
Juthatip McDevitt 2024-03-10 20:57:20 -05:00
parent 9a726805fc
commit dd186a4ae5
33 changed files with 7927 additions and 0 deletions

25
real_estate/.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# 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?
.env

View file

@ -0,0 +1,21 @@
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/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
real_estate/client/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# 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?

View file

@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chicagoland | Lifestyle properties</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5836
real_estate/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
{
"name": "client",
"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": {
"@reduxjs/toolkit": "^2.2.1",
"firebase": "^10.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.3",
"redux-persist": "^6.0.0"
},
"devDependencies": {
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.18",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^5.1.4"
}
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -0,0 +1,25 @@
import {BrowserRouter, Routes, Route} from 'react-router-dom'
import Home from './pages/Home'
import Login from './pages/Login'
import SignUp from './pages/SignUp'
import Contact from './pages/Contact'
import { Profile } from './pages/Profile'
import Navbar from './components/Navbar'
const App = () => {
return (
<BrowserRouter>
<Navbar/>
<Routes>
<Route path='/' element={<Home/>}/>
<Route path='/profile' element={<Profile/>}/>
<Route path='/login' element={<Login/>}/>
<Route path='/signup' element={<SignUp/>}/>
<Route path='/contact' element={<Contact/>}/>
</Routes>
</BrowserRouter>
)
}
export default App

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

View file

@ -0,0 +1,34 @@
//import { IoIosSearch } from "react-icons/io";
import { IoIosLock } from "react-icons/io";
import {Link} from 'react-router-dom'
//import {useSelector} from 'react-redux'
const Navbar = () => {
return (
<header className="bg-blue-950">
<div className="flex justify-between items-center mx-auto">
<Link to="/">
<div className="logo flex flex-col w-45 p-4 text-center bg-white sm:p-5 sm:w-50 ">
<h1 className=" text-blue-900 text-xl border-b-2 uppercase tracking-wide font-serif sm:text-2xl">Chicagoland</h1>
<h3 className=" text-gray-600 text-xs mt-1 uppercase tracking-widest">Lifestyle properties</h3>
</div>
</Link>
{/*<form className="bg-white rounded-lg flex items-center">
<input type="text" placeholder="Search your dream property" className="bg-transparent px-4 py-2 focus:outline-none text-sm w-60 sm:w-64"/>
<IoIosSearch className="text-slate-600 text-lg"/>
</form>*/}
<ul className="flex gap-4 text-white tracking-wide text-center px-6 uppercase text-sm sm:px-10">
<Link to="/"><li className="hidden md:inline">Home</li></Link>
<Link to="/"><li className="hidden md:inline">Buying</li></Link>
<Link to="/"><li className="hidden md:inline">Selling</li></Link>
<Link to="/contact"><li className="hidden md:inline">Contact</li></Link>
<Link to="/login" className="flex items-center gap-1"><li>Login</li><IoIosLock className="hidden md:inline"/></Link>
</ul>
</div>
</header>
)
}
export default Navbar

View file

@ -0,0 +1,45 @@
import { IoLogoGoogle } from "react-icons/io";
import {GoogleAuthProvider, getAuth, signInWithPopup} from 'firebase/auth'
import { app } from "../firebase";
import { useDispatch } from "react-redux";
import { loginSuccess } from "../redux/user/userSlice";
import { useNavigate } from "react-router-dom";
const OAuth = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const handleGoogleClick = async() => {
try {
const provider = new GoogleAuthProvider()
const auth = getAuth(app)
const result = await signInWithPopup(auth, provider)
const res = await fetch('/server/auth/google', {
method: 'POST',
headers:{
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: result.user.displayName,
email: result.user.email,
photo: result.user.photoURL
}),
})
const data = await res.json();
dispatch(loginSuccess(data));
navigate('/')
} catch (error) {
console.log('Cannot login with Google', error)
}
}
return (
<button onClick={handleGoogleClick} type="button" className="flex items-center justify-center text-center bg-red-700 p-2 rounded-md hover:opacity-80 text-white gap-2">
<IoLogoGoogle className="text-lg"/>
<div className="uppercase text-xs tracking-wide">Continue with google</div>
</button>
)
}
export default OAuth

View file

@ -0,0 +1,17 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: "real-estate-03.firebaseapp.com",
projectId: "real-estate-03",
storageBucket: "real-estate-03.appspot.com",
messagingSenderId: "253758457254",
appId: "1:253758457254:web:19acf955e2fc60d5297b67"
};
// Initialize Firebase
export const app = initializeApp(firebaseConfig);

View file

@ -0,0 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,14 @@
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import {persistor, store} from './redux/store.js'
import {Provider} from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
)

View file

@ -0,0 +1,9 @@
const Contact = () => {
return (
<div>Contact</div>
)
}
export default Contact

View file

@ -0,0 +1,22 @@
import { IoIosSearch } from "react-icons/io";
const Home = () => {
return (
<>
<div className="relative flex items-center justify-center h-[80vh]">
<div className="bg-hero-pattern absolute z-10 w-full h-[80vh] bg-no-repeat bg-cover bg-center"></div>
<div className="relative z-30 text-white text-center flex flex-col justify-center items-center">
<h1 className="uppercase tracking-tight text-5xl font-serif mb-4 text-shadow-lg shadow-black">Chicago properties for sale</h1>
<p className="text-shadow-DEFAULT shadow-black">Lorem ipsum dolor sit, amet consectetur adipisicing elit. <br />Alias dolorem error atque aut, asperiores et tempore iste quaerat pariatur maxime?</p>
<form className="text-black mt-5 bg-white rounded-lg flex items-center">
<input type="text" placeholder="Search your dream property" className="px-4 py-3 focus:outline-none bg-transparent text-sm w-60 sm:w-80"/>
<IoIosSearch className="text-slate-600 text-lg m-2"/>
</form>
</div>
</div>
</>
)
}
export default Home

View file

@ -0,0 +1,61 @@
import { useState } from 'react'
import {Link, useNavigate} from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import {loginStart, loginSuccess, loginFailure} from '../redux/user/userSlice'
import OAuth from '../components/OAuth'
const Login = () => {
const [formData, setFromData] = useState({})
const {loading, error} = useSelector((state) => state.user)
const navigate = useNavigate();
const dispatch = useDispatch();
const handleChange = (e) => {
setFromData({
...formData,
[e.target.id]: e.target.value,
});
};
const handleSubmit = async(e) =>{
e.preventDefault();
try {
dispatch(loginStart());
const res = await fetch('/server/auth/login',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await res.json();
if(data.success === false){
dispatch(loginFailure(data.message));
return;
}
dispatch(loginSuccess(data))
navigate('/')
} catch (error) {
dispatch(loginFailure(error.message))
}
}
return (
<div className="p-2 max-w-xs mx-auto">
<h1 className="text-2xl uppercase text-center text-blue-900 font-serif my-10 tracking-wide">Login</h1>
<form className="flex flex-col gap-5 text-sm" onSubmit={handleSubmit}>
<input type="text" placeholder="Email" id="email" className="border p-2 rounded-md" onChange={handleChange}/>
<input type="password" placeholder="Password" id="password" className="border p-2 rounded-md" onChange={handleChange}/>
<button disabled={loading} className="bg-blue-950 text-white p-2 rounded-md uppercase hover:opacity-90 disabled:opacity-80">{loading ? 'Loading...' : 'Login'}</button>
<OAuth/>
</form>
<div className="flex gap-2 mt-2">
<p>Do not have an account?</p>
<Link to='/signup'><span className='text-blue-900 hover:underline'>Sign Up</span></Link>
</div>
{error && <p className='text-red-500 mt-5'>{error}</p>}
</div>
)
}
export default Login

View file

@ -0,0 +1,7 @@
export const Profile = () => {
return (
<div>Profile</div>
)
}

View file

@ -0,0 +1,64 @@
import { useState } from 'react'
import {Link, useNavigate} from 'react-router-dom'
import OAuth from '../components/OAuth';
const SignUp = () => {
const [formData, setFromData] = useState({})
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleChange = (e) => {
setFromData({
...formData,
[e.target.id]: e.target.value,
});
};
const handleSubmit = async(e) =>{
e.preventDefault();
try {
setLoading(true);
const res = await fetch('/server/auth/signup',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await res.json();
if(data.success === false){
setLoading(false)
setError(data.message);
return;
}
setLoading(false);
setError(null);
navigate('/login')
} catch (error) {
setLoading(false);
setError(error.message)
}
}
return (
<div className="p-2 max-w-xs mx-auto">
<h1 className="text-2xl uppercase text-center text-blue-900 font-serif my-10 tracking-wide">Sign Up</h1>
<form className="flex flex-col gap-5 text-sm" onSubmit={handleSubmit}>
<input type="text" placeholder="Username" id="username" className="border p-2 rounded-md" onChange={handleChange}/>
<input type="text" placeholder="Email" id="email" className="border p-2 rounded-md" onChange={handleChange}/>
<input type="password" placeholder="Password" id="password" className="border p-2 rounded-md" onChange={handleChange}/>
<button disabled={loading} className="bg-blue-950 text-white p-2 rounded-md uppercase hover:opacity-90 disabled:opacity-80">{loading ? 'Loading...' : 'Sign Up'}</button>
<OAuth/>
</form>
<div className="flex gap-2 mt-2">
<p>Already have an account?</p>
<Link to='/login'><span className='text-blue-900 hover:underline'>Login</span></Link>
</div>
{error && <p className='text-red-500 mt-5'>{error}</p>}
</div>
)
}
export default SignUp

View file

@ -0,0 +1,23 @@
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import userReducer from './user/userSlice'
import {persistReducer, persistStore} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const rootReducer = combineReducers({user: userReducer})
const persistConfig = {
key: 'root',
storage,
version: 1,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export const persistor = persistStore(store);

View file

@ -0,0 +1,29 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
currentUser: null,
error: null,
loading: false,
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers:{
loginStart: (state) => {
state.loading = true;
},
loginSuccess: (state, action) =>{
state.currentUser = action.payload;
state.loading = false;
state.error = null;
},
loginFailure: (state, action) =>{
state.error = action.payload;
state.loading = false;
}
}
});
export const {loginStart, loginSuccess, loginFailure} = userSlice.actions;
export default userSlice.reducer;

View file

@ -0,0 +1,36 @@
/** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin')
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
backgroundImage: {
'hero-pattern': "url('./src/assets/hero.jpg')",
},
textShadow: {
sm: '1px 1px 2px var(--tw-shadow-color)',
DEFAULT: '2px 2px 4px var(--tw-shadow-color)',
lg: '4px 4px 8px var(--tw-shadow-color)',
xl: '4px 4px 16px var(--tw-shadow-color)',
},
},
},
plugins: [
plugin(function ({ matchUtilities, theme }) {
matchUtilities(
{
'text-shadow': (value) => ({
textShadow: value,
}),
},
{ values: theme('textShadow') }
)
}
)
],
}

View file

@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
server:{
proxy:{
'/server': {
target: 'http://localhost:3000',
secure: false,
},
},
},
plugins: [react()],
})

1373
real_estate/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
real_estate/package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "real_estate",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "nodemon server/index.js",
"start": "mode server/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.1",
"nodemon": "^3.1.0"
}
}

View file

@ -0,0 +1,63 @@
import User from "../models/user.model.js";
import bcryptjs from 'bcryptjs'
import { errorHandler } from "../utils/error.js";
import jwt from 'jsonwebtoken'
import { userInfo } from "os";
export const signup = async(req, res, next) => {
const {username, email, password} = req.body
const hashedPassword = bcryptjs.hashSync(password, 10)
const newUser = new User({username, email, password: hashedPassword});
try {
await newUser.save()
res.status(201).json("User created succesfully")
} catch (error) {
next(error)
}
};
export const login = async(req, res, next) => {
const {email, password} = req.body
try {
const validUser = await User.findOne({email});
if(!validUser) return next(errorHandler(404, 'User not found'));
const validPassword = bcryptjs.compareSync(password, validUser.password);
if(!validPassword) return next(errorHandler(401, 'Wrong credentials'));
const token = jwt.sign({id: validUser._id}, process.env.JWT_SECRET,)
const {password: pass, ...rest} = validUser._doc;
res.cookie('access_token', token, {httpOnly: true}).status(200).json(rest);
} catch (error) {
next(error)
}
};
export const google = async(req, res, next) => {
try {
const user = await User.findOne({email: req.body.email})
if(user){
const token = jwt.sign({id: user._id}, process.env.JWT_SECRET);
const {password: pass, ...rest} = user._doc;
res.cookie('access_token', token, {httpOnly: true}).status(200).json(rest);
}else{
const generatedPassword = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-8);
const hashedPassword = bcryptjs.hashSync(generatedPassword, 10);
const newUser = new User({
username: req.body.name.split(" ").join("").toLowerCase()+Math.random().toString(36).slice(-4),
email: req.body.email,
password: hashedPassword,
avatar: req.body.photo,
});
await newUser.save();
const token = jwt.sign({id: newUser._id}, process.env.JWT_SECRET);
const {password: pass, ...rest} = newUser._doc;
res.cookie('access_token', token, {httpOnly: true}).status(200).json(rest);
}
} catch (error) {
next(error)
}
}

View file

@ -0,0 +1,5 @@
export const test = (req, res) => {
res.json({
message: 'API route is working',
})
}

View file

@ -0,0 +1,39 @@
import express from 'express';
import mongoose from 'mongoose'
import dotenv from 'dotenv'
import userRouter from './routes/user.route.js'
import authRouter from './routes/auth.route.js'
dotenv.config();
//connect with MongoDB
mongoose.connect(process.env.MONGODB).then(() => {
console.log('Connected to MongoDB')
}).catch((error) => {
console.log(error)
})
//connect with Express
const app = express();
app.use(express.json());
app.listen(3000, () =>{
console.log('Server is running on port 3000')
})
//create api route
app.use('/server/user', userRouter)
app.use('/server/auth', authRouter)
//middleware to catch an error
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500
const message = err.message || "Internal server error"
return res.status(statusCode),json({
success: false,
statusCode,
message,
})
})

View file

@ -0,0 +1,26 @@
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
avatar: {
type: String,
default: "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png",
},
}, {timestamps:true});
const User = mongoose.model('User', userSchema)
export default User;

View file

@ -0,0 +1,10 @@
import express from 'express'
import { google, login, signup } from '../controllers/auth.controller.js';
const router = express.Router();
router.post('/signup', signup);
router.post('/login', login);
router.post('/google', google)
export default router

View file

@ -0,0 +1,8 @@
import express from 'express'
import { test } from '../controllers/user.controller.js';
const router = express.Router();
router.get('/test', test)
export default router

View file

@ -0,0 +1,6 @@
export const errorHandler = (statusCode, message) =>{
const error = new Error();
error.statusCode = statusCode;
error.message = message;
return error
}