real estate client and server
This commit is contained in:
parent
9a726805fc
commit
dd186a4ae5
33 changed files with 7927 additions and 0 deletions
25
real_estate/.gitignore
vendored
Normal file
25
real_estate/.gitignore
vendored
Normal 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
|
21
real_estate/client/.eslintrc.cjs
Normal file
21
real_estate/client/.eslintrc.cjs
Normal 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
24
real_estate/client/.gitignore
vendored
Normal 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?
|
8
real_estate/client/README.md
Normal file
8
real_estate/client/README.md
Normal 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
|
13
real_estate/client/index.html
Normal file
13
real_estate/client/index.html
Normal 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
5836
real_estate/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
real_estate/client/package.json
Normal file
35
real_estate/client/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
6
real_estate/client/postcss.config.js
Normal file
6
real_estate/client/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
25
real_estate/client/src/App.jsx
Normal file
25
real_estate/client/src/App.jsx
Normal 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
|
BIN
real_estate/client/src/assets/hero.jpg
Normal file
BIN
real_estate/client/src/assets/hero.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 MiB |
34
real_estate/client/src/components/Navbar.jsx
Normal file
34
real_estate/client/src/components/Navbar.jsx
Normal 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
|
45
real_estate/client/src/components/OAuth.jsx
Normal file
45
real_estate/client/src/components/OAuth.jsx
Normal 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
|
17
real_estate/client/src/firebase.js
Normal file
17
real_estate/client/src/firebase.js
Normal 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);
|
5
real_estate/client/src/index.css
Normal file
5
real_estate/client/src/index.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
14
real_estate/client/src/main.jsx
Normal file
14
real_estate/client/src/main.jsx
Normal 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>,
|
||||||
|
)
|
9
real_estate/client/src/pages/Contact.jsx
Normal file
9
real_estate/client/src/pages/Contact.jsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
|
||||||
|
const Contact = () => {
|
||||||
|
return (
|
||||||
|
<div>Contact</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Contact
|
22
real_estate/client/src/pages/Home.jsx
Normal file
22
real_estate/client/src/pages/Home.jsx
Normal 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
|
61
real_estate/client/src/pages/Login.jsx
Normal file
61
real_estate/client/src/pages/Login.jsx
Normal 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
|
7
real_estate/client/src/pages/Profile.jsx
Normal file
7
real_estate/client/src/pages/Profile.jsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
export const Profile = () => {
|
||||||
|
return (
|
||||||
|
<div>Profile</div>
|
||||||
|
)
|
||||||
|
}
|
64
real_estate/client/src/pages/SignUp.jsx
Normal file
64
real_estate/client/src/pages/SignUp.jsx
Normal 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
|
23
real_estate/client/src/redux/store.js
Normal file
23
real_estate/client/src/redux/store.js
Normal 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);
|
29
real_estate/client/src/redux/user/userSlice.js
Normal file
29
real_estate/client/src/redux/user/userSlice.js
Normal 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;
|
36
real_estate/client/tailwind.config.js
Normal file
36
real_estate/client/tailwind.config.js
Normal 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') }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
16
real_estate/client/vite.config.js
Normal file
16
real_estate/client/vite.config.js
Normal 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
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
22
real_estate/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
63
real_estate/server/controllers/auth.controller.js
Normal file
63
real_estate/server/controllers/auth.controller.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
5
real_estate/server/controllers/user.controller.js
Normal file
5
real_estate/server/controllers/user.controller.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const test = (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: 'API route is working',
|
||||||
|
})
|
||||||
|
}
|
39
real_estate/server/index.js
Normal file
39
real_estate/server/index.js
Normal 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,
|
||||||
|
})
|
||||||
|
})
|
26
real_estate/server/models/user.model.js
Normal file
26
real_estate/server/models/user.model.js
Normal 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;
|
10
real_estate/server/routes/auth.route.js
Normal file
10
real_estate/server/routes/auth.route.js
Normal 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
|
8
real_estate/server/routes/user.route.js
Normal file
8
real_estate/server/routes/user.route.js
Normal 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
|
6
real_estate/server/utils/error.js
Normal file
6
real_estate/server/utils/error.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export const errorHandler = (statusCode, message) =>{
|
||||||
|
const error = new Error();
|
||||||
|
error.statusCode = statusCode;
|
||||||
|
error.message = message;
|
||||||
|
return error
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue