From 3cab9f9fcd22c21c20385a488f04e5e0039c8071 Mon Sep 17 00:00:00 2001 From: Adib Sadman Date: Mon, 25 Nov 2024 17:14:52 +0600 Subject: [PATCH] fix: navbar positioning and remove gradient backgrounds Changes made: - Removed gradient background animations from globals.css - Fixed navbar positioning to be properly fixed at top - Removed unnecessary padding and margins from layout - Simplified glass-morphism styling with minimal transparency - Updated border styling for better visual consistency - Cleaned up container and grid padding - Optimized main content layout structure --- README.md | 62 ++ backend/middleware/authMiddleware.js | 91 +++ backend/middleware/errorHandler.js | 46 ++ backend/models/User.js | 51 +- backend/package-lock.json | 10 + backend/package.json | 1 + backend/utils/errorResponse.js | 12 + frontend/.eslintrc.js | 63 ++ frontend/.husky/pre-commit | 1 + frontend/components/GlassButton.js | 98 +++ frontend/components/GlassCard.js | 56 ++ frontend/components/GlassInput.js | 123 ++++ frontend/components/GlassMenu.js | 94 +++ frontend/components/GlassPropertyDetails.js | 283 ++++++++ frontend/components/GlassSearch.js | 299 ++++++++ frontend/components/Layout.js | 330 ++++----- frontend/components/Navbar.js | 466 +++++++++---- frontend/components/Navbar.module.css | 79 +++ frontend/components/PageContainer.js | 51 ++ frontend/components/PropertyCard.js | 135 +++- frontend/components/auth/withAuth.js | 41 ++ frontend/components/common/OptimizedImage.js | 36 + frontend/components/layout/index.js | 17 + frontend/components/properties/BookingForm.js | 90 +++ .../components/properties/PropertyDetails.js | 74 ++ frontend/components/withAuth.js | 12 +- frontend/contexts/AuthContext.js | 217 ++---- frontend/hooks/useAuth.js | 100 +++ frontend/lib/axios.js | 36 + frontend/lib/websocket.js | 139 ++-- frontend/package-lock.json | 235 ++++--- frontend/package.json | 20 +- frontend/pages/_app.js | 10 +- frontend/pages/admin/dashboard.js | 336 ++++----- frontend/pages/dashboard.js | 5 +- frontend/pages/index.js | 636 +++++++++++------- frontend/pages/login.js | 296 ++++---- frontend/pages/profile.js | 3 +- frontend/pages/properties/[id].js | 368 +++------- frontend/pages/register.js | 3 +- frontend/styles/globals.css | 217 +++++- frontend/styles/theme.js | 174 +++-- frontend/theme/index.js | 78 +++ 43 files changed, 3872 insertions(+), 1622 deletions(-) create mode 100644 backend/middleware/authMiddleware.js create mode 100644 backend/middleware/errorHandler.js create mode 100644 backend/utils/errorResponse.js create mode 100644 frontend/.eslintrc.js create mode 100644 frontend/components/GlassButton.js create mode 100644 frontend/components/GlassCard.js create mode 100644 frontend/components/GlassInput.js create mode 100644 frontend/components/GlassMenu.js create mode 100644 frontend/components/GlassPropertyDetails.js create mode 100644 frontend/components/GlassSearch.js create mode 100644 frontend/components/Navbar.module.css create mode 100644 frontend/components/PageContainer.js create mode 100644 frontend/components/auth/withAuth.js create mode 100644 frontend/components/common/OptimizedImage.js create mode 100644 frontend/components/layout/index.js create mode 100644 frontend/components/properties/BookingForm.js create mode 100644 frontend/components/properties/PropertyDetails.js create mode 100644 frontend/hooks/useAuth.js create mode 100644 frontend/lib/axios.js create mode 100644 frontend/theme/index.js diff --git a/README.md b/README.md index 2fc3ead..2459957 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,68 @@ The platform serves as both a social network and rental service, making it easie - πŸ” Input Validation - πŸ“ Activity Logging +## πŸ—ΊοΈ Project Roadmap + +### Phase 1: Foundation (Current) +- [x] Project setup and architecture +- [x] Basic UI components with glass-morphism design +- [x] Authentication system +- [ ] Property listing core features +- [ ] Search and filter functionality + +### Phase 2: Enhanced Features (Q1 2025) +- [ ] Advanced property search with map integration +- [ ] Real-time chat between users and property owners +- [ ] Virtual tour integration +- [ ] Review and rating system +- [ ] Payment integration + +### Phase 3: Advanced Features (Q2 2025) +- [ ] AI-powered property recommendations +- [ ] Automated rental agreement generation +- [ ] Mobile app development +- [ ] Analytics dashboard for property owners +- [ ] Multi-language support + +### Phase 4: Scaling & Optimization (Q3 2025) +- [ ] Performance optimization +- [ ] SEO enhancement +- [ ] Advanced analytics +- [ ] Market analysis tools +- [ ] API marketplace for third-party integrations + +## πŸ“Š Current Project Status + +| Feature | Status | Notes | +|---------|--------|-------| +| Authentication | βœ… Working | Email and social login implemented | +| User Profiles | βœ… Working | Basic profile management available | +| Property Listing | ⚠️ Partial | Basic listing without advanced features | +| Search System | ⚠️ Partial | Basic search implemented, advanced filters pending | +| Glass-morphism UI | βœ… Working | Complete component library with modern design | +| Responsive Design | βœ… Working | Fully responsive across all devices | +| Chat System | ❌ Pending | Planned for Phase 2 | +| Payment System | ❌ Pending | Planned for Phase 2 | +| Admin Dashboard | ⚠️ Partial | Basic management features available | +| Email Notifications | βœ… Working | Transactional emails implemented | +| Map Integration | ❌ Pending | Planned for Phase 2 | +| Image Upload | βœ… Working | With optimization and CDN delivery | +| Property Analytics | ❌ Pending | Planned for Phase 3 | + +## 🎯 Current Sprint Focus +- Enhancing property search functionality +- Implementing advanced filters +- Optimizing image loading and caching +- Improving user experience with smoother transitions +- Adding more interactive elements to property listings + +## πŸ”„ Recent Updates +- Implemented glass-morphism design system +- Added responsive navigation +- Enhanced user authentication flow +- Optimized property listing performance +- Added basic search functionality + ## πŸ› οΈ Development ### Tech Stack diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 0000000..41cbed7 --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,91 @@ +const jwt = require('jsonwebtoken'); +const asyncHandler = require('express-async-handler'); +const User = require('../models/User'); + +// Protect routes +const protect = asyncHandler(async (req, res, next) => { + let token; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith('Bearer') + ) { + try { + // Get token from header + token = req.headers.authorization.split(' ')[1]; + + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // Get user from token + req.user = await User.findById(decoded.id).select('-password'); + + next(); + } catch (error) { + console.error(error); + res.status(401); + throw new Error('Not authorized'); + } + } + + if (!token) { + res.status(401); + throw new Error('Not authorized, no token'); + } +}); + +// Admin middleware +const admin = (req, res, next) => { + if (req.user && req.user.role === 'admin') { + next(); + } else { + res.status(401); + throw new Error('Not authorized as admin'); + } +}; + +// Renter middleware +const renter = (req, res, next) => { + if (req.user && req.user.role === 'renter') { + next(); + } else { + res.status(401); + throw new Error('Not authorized as renter'); + } +}; + +// Super admin middleware +const superAdmin = (req, res, next) => { + if (req.user && req.user.role === 'superadmin') { + next(); + } else { + res.status(401); + throw new Error('Not authorized as super admin'); + } +}; + +// Check if user is authenticated and has required role +const authorize = (roles = []) => { + if (typeof roles === 'string') { + roles = [roles]; + } + + return [ + protect, + (req, res, next) => { + if (roles.length && !roles.includes(req.user.role)) { + res.status(401); + throw new Error(`Not authorized as ${roles.join(', ')}`); + } + next(); + } + ]; +}; + +module.exports = { + protect, + admin, + renter, + superAdmin, + authorize +}; diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js new file mode 100644 index 0000000..361a7b9 --- /dev/null +++ b/backend/middleware/errorHandler.js @@ -0,0 +1,46 @@ +const ErrorResponse = require('../utils/errorResponse'); + +const errorHandler = (err, req, res, next) => { + let error = { ...err }; + error.message = err.message; + + // Log to console for dev + console.error(err.stack.red); + + // Mongoose bad ObjectId + if (err.name === 'CastError') { + const message = `Resource not found with id of ${err.value}`; + error = new ErrorResponse(message, 404); + } + + // Mongoose duplicate key + if (err.code === 11000) { + const message = 'Duplicate field value entered'; + error = new ErrorResponse(message, 400); + } + + // Mongoose validation error + if (err.name === 'ValidationError') { + const message = Object.values(err.errors).map(val => val.message); + error = new ErrorResponse(message, 400); + } + + // JWT errors + if (err.name === 'JsonWebTokenError') { + const message = 'Invalid token. Please log in again!'; + error = new ErrorResponse(message, 401); + } + + if (err.name === 'TokenExpiredError') { + const message = 'Your token has expired! Please log in again.'; + error = new ErrorResponse(message, 401); + } + + res.status(error.statusCode || 500).json({ + success: false, + error: error.message || 'Server Error', + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined + }); +}; + +module.exports = errorHandler; diff --git a/backend/models/User.js b/backend/models/User.js index 0615061..218b30d 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -1,5 +1,6 @@ const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); +const crypto = require('crypto'); const userSchema = new mongoose.Schema({ name: { @@ -26,7 +27,7 @@ const userSchema = new mongoose.Schema({ phone: { type: String, required: [true, 'Phone number is required'], - match: [/^[+]?[\d\s-]{10,}$/, 'Please enter a valid phone number'] + match: [/^(\+88)?01[3-9]\d{8}$/, 'Please enter a valid Bangladeshi phone number'] }, nid: { type: String, @@ -44,10 +45,24 @@ const userSchema = new mongoose.Schema({ enum: ['user', 'renter', 'owner', 'student', 'admin', 'super_admin'], default: 'user' }, + avatar: { + type: String, + default: 'default-avatar.jpg' + }, + verified: { + type: Boolean, + default: false + }, + verificationToken: String, + verificationTokenExpire: Date, + resetPasswordToken: String, + resetPasswordExpire: Date, createdAt: { type: Date, default: Date.now } +}, { + timestamps: true }); // Encrypt password using bcrypt @@ -65,4 +80,38 @@ userSchema.methods.matchPassword = async function(enteredPassword) { return await bcrypt.compare(enteredPassword, this.password); }; +// Generate and hash password token +userSchema.methods.getResetPasswordToken = function() { + // Generate token + const resetToken = crypto.randomBytes(20).toString('hex'); + + // Hash token and set to resetPasswordToken field + this.resetPasswordToken = crypto + .createHash('sha256') + .update(resetToken) + .digest('hex'); + + // Set expire + this.resetPasswordExpire = Date.now() + 10 * 60 * 1000; // 10 minutes + + return resetToken; +}; + +// Generate email verification token +userSchema.methods.getVerificationToken = function() { + // Generate token + const verificationToken = crypto.randomBytes(20).toString('hex'); + + // Hash token and set to verificationToken field + this.verificationToken = crypto + .createHash('sha256') + .update(verificationToken) + .digest('hex'); + + // Set expire + this.verificationTokenExpire = Date.now() + 24 * 60 * 60 * 1000; // 24 hours + + return verificationToken; +}; + module.exports = mongoose.model('User', userSchema); diff --git a/backend/package-lock.json b/backend/package-lock.json index 9ab118e..dce9476 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "bcryptjs": "^2.4.3", + "colors": "^1.4.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.2", @@ -834,6 +835,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", diff --git a/backend/package.json b/backend/package.json index 3927d9d..7f204ce 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "bcryptjs": "^2.4.3", + "colors": "^1.4.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.2", diff --git a/backend/utils/errorResponse.js b/backend/utils/errorResponse.js new file mode 100644 index 0000000..e7a400c --- /dev/null +++ b/backend/utils/errorResponse.js @@ -0,0 +1,12 @@ +class ErrorResponse extends Error { + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = ErrorResponse; diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 0000000..6ee8698 --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,63 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true, + jest: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@next/next/recommended', + 'next/core-web-vitals', + ], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 12, + sourceType: 'module', + }, + plugins: ['react', 'react-hooks', '@next/next'], + settings: { + react: { + version: 'detect', + }, + }, + rules: { + // React specific rules + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'react/display-name': 'off', + 'react/no-unescaped-entities': 'off', + + // Next.js specific rules + '@next/next/no-img-element': 'warn', + '@next/next/no-html-link-for-pages': 'error', + + // React Hooks rules + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + // General JavaScript/ES6 rules + 'no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'prefer-const': 'warn', + 'no-var': 'error', + + // Import rules + 'import/no-anonymous-default-export': 'off', + }, + globals: { + React: 'writable', + JSX: 'writable', + Promise: 'writable', + Set: 'writable', + Map: 'writable', + }, +}; diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit index d24fdfc..eee5159 100644 --- a/frontend/.husky/pre-commit +++ b/frontend/.husky/pre-commit @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" npx lint-staged +npm run lint && npm run test diff --git a/frontend/components/GlassButton.js b/frontend/components/GlassButton.js new file mode 100644 index 0000000..d98776c --- /dev/null +++ b/frontend/components/GlassButton.js @@ -0,0 +1,98 @@ +import { Button } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const GlassButton = styled(Button)(({ theme, variant = 'contained', color = 'primary', size = 'medium' }) => ({ + background: variant === 'contained' + ? `linear-gradient(45deg, ${theme.palette[color].main} 30%, ${theme.palette[color].light} 90%)` + : 'rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(10px)', + WebkitBackdropFilter: 'blur(10px)', + border: variant === 'contained' + ? 'none' + : `1px solid ${theme.palette[color].main}`, + borderRadius: '8px', + boxShadow: variant === 'contained' + ? '0 4px 20px rgba(0, 0, 0, 0.15)' + : 'none', + color: variant === 'contained' + ? '#fff' + : theme.palette[color].main, + padding: size === 'large' + ? '12px 24px' + : size === 'small' + ? '6px 16px' + : '8px 20px', + textTransform: 'none', + fontWeight: 500, + letterSpacing: '0.5px', + transition: 'all 0.3s ease-in-out', + position: 'relative', + overflow: 'hidden', + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(120deg, rgba(255,255,255,0.3), rgba(255,255,255,0.1))', + opacity: 0, + transition: 'opacity 0.3s ease-in-out', + }, + + '&:hover': { + background: variant === 'contained' + ? `linear-gradient(45deg, ${theme.palette[color].dark} 30%, ${theme.palette[color].main} 90%)` + : 'rgba(255, 255, 255, 0.2)', + boxShadow: variant === 'contained' + ? '0 6px 30px rgba(0, 0, 0, 0.2)' + : 'none', + transform: 'translateY(-2px)', + + '&::before': { + opacity: 1, + }, + }, + + '&:active': { + transform: 'translateY(0)', + boxShadow: variant === 'contained' + ? '0 2px 10px rgba(0, 0, 0, 0.1)' + : 'none', + }, + + '&.Mui-disabled': { + background: theme.palette.action.disabledBackground, + color: theme.palette.action.disabled, + boxShadow: 'none', + border: 'none', + backdropFilter: 'none', + WebkitBackdropFilter: 'none', + + '&::before': { + display: 'none', + }, + }, + + // Loading state + '& .MuiCircularProgress-root': { + marginRight: theme.spacing(1), + color: 'inherit', + }, + + // Icon styling + '& .MuiButton-startIcon, & .MuiButton-endIcon': { + transition: 'transform 0.2s ease-in-out', + }, + + '&:hover .MuiButton-startIcon': { + transform: 'translateX(-2px)', + }, + + '&:hover .MuiButton-endIcon': { + transform: 'translateX(2px)', + }, +})); + +export default GlassButton; diff --git a/frontend/components/GlassCard.js b/frontend/components/GlassCard.js new file mode 100644 index 0000000..422d005 --- /dev/null +++ b/frontend/components/GlassCard.js @@ -0,0 +1,56 @@ +import { Paper, Box } from '@mui/material'; + +const GlassCard = ({ children, elevation = 0, hover = true, ...props }) => { + return ( + + + {children} + + + ); +}; + +export default GlassCard; diff --git a/frontend/components/GlassInput.js b/frontend/components/GlassInput.js new file mode 100644 index 0000000..ed7192d --- /dev/null +++ b/frontend/components/GlassInput.js @@ -0,0 +1,123 @@ +import { TextField } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const GlassInput = styled(TextField)(({ theme }) => ({ + '& .MuiInputBase-root': { + background: 'rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(10px)', + WebkitBackdropFilter: 'blur(10px)', + borderRadius: '8px', + border: '1px solid rgba(255, 255, 255, 0.2)', + transition: 'all 0.3s ease-in-out', + overflow: 'hidden', + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(120deg, rgba(255,255,255,0.2), rgba(255,255,255,0.1))', + opacity: 0, + transition: 'opacity 0.3s ease-in-out', + zIndex: 0, + }, + + '& .MuiInputBase-input': { + position: 'relative', + zIndex: 1, + padding: '12px 16px', + color: theme.palette.text.primary, + + '&::placeholder': { + color: theme.palette.text.secondary, + opacity: 0.7, + }, + }, + + '& .MuiInputAdornment-root': { + position: 'relative', + zIndex: 1, + color: theme.palette.text.secondary, + }, + + '&:hover': { + background: 'rgba(255, 255, 255, 0.15)', + borderColor: 'rgba(255, 255, 255, 0.3)', + + '&::before': { + opacity: 1, + }, + + '& .MuiInputAdornment-root': { + color: theme.palette.primary.main, + }, + }, + + '&.Mui-focused': { + background: 'rgba(255, 255, 255, 0.2)', + borderColor: theme.palette.primary.main, + boxShadow: `0 0 0 2px ${theme.palette.primary.main}25`, + + '& .MuiInputAdornment-root': { + color: theme.palette.primary.main, + }, + }, + + '&.Mui-error': { + borderColor: theme.palette.error.main, + + '&.Mui-focused': { + boxShadow: `0 0 0 2px ${theme.palette.error.main}25`, + }, + }, + + '&.Mui-disabled': { + background: theme.palette.action.disabledBackground, + borderColor: 'transparent', + backdropFilter: 'none', + WebkitBackdropFilter: 'none', + + '& .MuiInputBase-input': { + color: theme.palette.text.disabled, + }, + + '& .MuiInputAdornment-root': { + color: theme.palette.text.disabled, + }, + + '&::before': { + display: 'none', + }, + }, + }, + + '& .MuiFormLabel-root': { + color: theme.palette.text.secondary, + transition: 'color 0.3s ease-in-out', + + '&.Mui-focused': { + color: theme.palette.primary.main, + }, + + '&.Mui-error': { + color: theme.palette.error.main, + }, + + '&.Mui-disabled': { + color: theme.palette.text.disabled, + }, + }, + + '& .MuiFormHelperText-root': { + marginLeft: '4px', + marginRight: '4px', + + '&.Mui-error': { + color: theme.palette.error.main, + }, + }, +})); + +export default GlassInput; diff --git a/frontend/components/GlassMenu.js b/frontend/components/GlassMenu.js new file mode 100644 index 0000000..397e33a --- /dev/null +++ b/frontend/components/GlassMenu.js @@ -0,0 +1,94 @@ +import { Menu, MenuItem } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const StyledMenu = styled(Menu)(({ theme }) => ({ + '& .MuiPaper-root': { + background: 'rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(10px)', + WebkitBackdropFilter: 'blur(10px)', + borderRadius: '12px', + border: '1px solid rgba(255, 255, 255, 0.2)', + boxShadow: '0 4px 30px rgba(0, 0, 0, 0.1)', + padding: theme.spacing(1), + minWidth: '200px', + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(120deg, rgba(255,255,255,0.2), rgba(255,255,255,0.1))', + borderRadius: 'inherit', + opacity: 0, + transition: 'opacity 0.3s ease-in-out', + zIndex: 0, + }, + + '&:hover::before': { + opacity: 1, + }, + }, +})); + +const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ + borderRadius: '8px', + padding: theme.spacing(1, 2), + margin: theme.spacing(0.5, 0), + position: 'relative', + zIndex: 1, + transition: 'all 0.3s ease-in-out', + + '& .MuiSvgIcon-root': { + marginRight: theme.spacing(1.5), + fontSize: '1.25rem', + transition: 'transform 0.2s ease-in-out', + }, + + '&:hover': { + background: 'rgba(255, 255, 255, 0.1)', + transform: 'translateX(4px)', + + '& .MuiSvgIcon-root': { + transform: 'scale(1.1)', + }, + }, + + '&.Mui-selected': { + background: 'rgba(33, 150, 243, 0.15)', + + '&:hover': { + background: 'rgba(33, 150, 243, 0.25)', + }, + }, + + '&.danger': { + color: theme.palette.error.main, + + '&:hover': { + background: 'rgba(211, 47, 47, 0.1)', + }, + }, +})); + +const GlassMenu = ({ children, ...props }) => { + return ( + + {children} + + ); +}; + +export { GlassMenu, StyledMenuItem as GlassMenuItem }; diff --git a/frontend/components/GlassPropertyDetails.js b/frontend/components/GlassPropertyDetails.js new file mode 100644 index 0000000..f048f94 --- /dev/null +++ b/frontend/components/GlassPropertyDetails.js @@ -0,0 +1,283 @@ +import { + Box, + Typography, + Grid, + IconButton, + Chip, + ImageList, + ImageListItem, + Divider, +} from '@mui/material'; +import { + Favorite, + FavoriteBorder, + Share, + LocationOn, + Hotel, + AttachMoney, + SquareFoot, + Bathtub, + KingBed, + LocalParking, + Security, + Elevator, + AcUnit, +} from '@mui/icons-material'; +import { styled } from '@mui/material/styles'; +import GlassButton from './GlassButton'; + +const DetailsContainer = styled(Box)(({ theme }) => ({ + background: 'rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(10px)', + WebkitBackdropFilter: 'blur(10px)', + borderRadius: '24px', + border: '1px solid rgba(255, 255, 255, 0.2)', + padding: theme.spacing(3), + position: 'relative', + overflow: 'hidden', + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(120deg, rgba(255,255,255,0.2), rgba(255,255,255,0.1))', + opacity: 0, + transition: 'opacity 0.3s ease-in-out', + zIndex: 0, + }, + + '&:hover::before': { + opacity: 1, + }, +})); + +const ImageContainer = styled(Box)(({ theme }) => ({ + position: 'relative', + borderRadius: '16px', + overflow: 'hidden', + marginBottom: theme.spacing(3), + + '& img': { + width: '100%', + height: '400px', + objectFit: 'cover', + transition: 'transform 0.3s ease-in-out', + }, + + '&:hover img': { + transform: 'scale(1.05)', + }, +})); + +const FeatureChip = styled(Chip)(({ theme }) => ({ + margin: theme.spacing(0.5), + background: 'rgba(255, 255, 255, 0.1)', + border: '1px solid rgba(255, 255, 255, 0.2)', + backdropFilter: 'blur(5px)', + transition: 'all 0.3s ease-in-out', + + '&:hover': { + background: 'rgba(255, 255, 255, 0.2)', + transform: 'translateY(-2px)', + }, + + '& .MuiChip-icon': { + color: 'inherit', + }, +})); + +const GradientText = styled(Typography)(({ theme, color = 'primary' }) => ({ + background: `linear-gradient(45deg, ${theme.palette[color].main} 30%, ${theme.palette[color].light} 90%)`, + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + fontWeight: 700, +})); + +const GlassPropertyDetails = ({ property, onFavorite, onShare, onContact }) => { + const { + title, + description, + price, + location, + images, + features, + amenities, + isFavorite, + specifications, + } = property; + + const amenityIcons = { + 'Air Conditioning': , + 'Parking': , + 'Security': , + 'Elevator': , + }; + + return ( + + + + {images.slice(0, 4).map((image, index) => ( + + {`Property + + ))} + + + + {isFavorite ? : } + + + + + + + + + + + + {title} + + + + + {location} + + + + {description} + + + + + + + + Specifications + + + {specifications.map(({ icon: Icon, label, value }) => ( + + + + + {label} + + + {value} + + + + ))} + + + + + + + + Features & Amenities + + + {amenities.map((amenity) => ( + + ))} + + + + + + + + ৳{price.toLocaleString()} + + /month + + + + + + Contact Owner + + + Share Property + + + + + + + ); +}; + +export default GlassPropertyDetails; diff --git a/frontend/components/GlassSearch.js b/frontend/components/GlassSearch.js new file mode 100644 index 0000000..486bce6 --- /dev/null +++ b/frontend/components/GlassSearch.js @@ -0,0 +1,299 @@ +import { useState } from 'react'; +import { + Box, + InputBase, + IconButton, + Paper, + Collapse, + Chip, + Slider, + FormControl, + FormGroup, + FormControlLabel, + Checkbox, +} from '@mui/material'; +import { + Search as SearchIcon, + FilterList as FilterIcon, + LocationOn, + AttachMoney, + Hotel, + Clear, +} from '@mui/icons-material'; +import { styled } from '@mui/material/styles'; + +const SearchContainer = styled(Paper)(({ theme }) => ({ + background: 'rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(10px)', + WebkitBackdropFilter: 'blur(10px)', + borderRadius: '16px', + border: '1px solid rgba(255, 255, 255, 0.2)', + transition: 'all 0.3s ease-in-out', + overflow: 'hidden', + position: 'relative', + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(120deg, rgba(255,255,255,0.2), rgba(255,255,255,0.1))', + opacity: 0, + transition: 'opacity 0.3s ease-in-out', + zIndex: 0, + }, + + '&:hover': { + border: '1px solid rgba(255, 255, 255, 0.3)', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)', + + '&::before': { + opacity: 1, + }, + }, +})); + +const SearchInput = styled(InputBase)(({ theme }) => ({ + width: '100%', + padding: theme.spacing(1, 2), + position: 'relative', + zIndex: 1, + + '& input': { + color: theme.palette.text.primary, + '&::placeholder': { + color: theme.palette.text.secondary, + opacity: 0.7, + }, + }, +})); + +const FilterContainer = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(255, 255, 255, 0.1)', + position: 'relative', + zIndex: 1, +})); + +const FilterChip = styled(Chip)(({ theme, selected }) => ({ + margin: theme.spacing(0.5), + background: selected ? 'rgba(33, 150, 243, 0.2)' : 'rgba(255, 255, 255, 0.1)', + border: `1px solid ${selected ? theme.palette.primary.main : 'rgba(255, 255, 255, 0.2)'}`, + color: selected ? theme.palette.primary.main : theme.palette.text.primary, + transition: 'all 0.3s ease-in-out', + + '&:hover': { + background: selected ? 'rgba(33, 150, 243, 0.3)' : 'rgba(255, 255, 255, 0.2)', + }, + + '& .MuiChip-icon': { + color: 'inherit', + }, +})); + +const PriceSlider = styled(Slider)(({ theme }) => ({ + color: theme.palette.primary.main, + height: 8, + + '& .MuiSlider-track': { + border: 'none', + background: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)', + }, + + '& .MuiSlider-thumb': { + height: 24, + width: 24, + backgroundColor: '#fff', + border: '2px solid currentColor', + + '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': { + boxShadow: 'inherit', + }, + }, + + '& .MuiSlider-valueLabel': { + background: 'rgba(255, 255, 255, 0.9)', + backdropFilter: 'blur(4px)', + borderRadius: '8px', + padding: '4px 8px', + color: theme.palette.text.primary, + fontSize: '0.75rem', + }, +})); + +const GlassSearch = ({ onSearch, initialFilters = {} }) => { + const [showFilters, setShowFilters] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [filters, setFilters] = useState({ + priceRange: [5000, 50000], + location: [], + amenities: [], + propertyType: [], + ...initialFilters, + }); + + const locations = [ + { label: 'Dhaka', icon: }, + { label: 'Chittagong', icon: }, + { label: 'Sylhet', icon: }, + ]; + + const propertyTypes = [ + { label: 'Apartment', icon: }, + { label: 'House', icon: }, + { label: 'Studio', icon: }, + ]; + + const amenities = [ + 'Furnished', + 'Air Conditioning', + 'Parking', + 'Security', + 'Generator', + 'Elevator', + ]; + + const handleSearch = () => { + onSearch?.({ searchTerm, filters }); + }; + + const handleFilterChange = (type, value) => { + setFilters(prev => ({ + ...prev, + [type]: value, + })); + }; + + const toggleLocation = (location) => { + const newLocations = filters.location.includes(location) + ? filters.location.filter(l => l !== location) + : [...filters.location, location]; + handleFilterChange('location', newLocations); + }; + + const togglePropertyType = (type) => { + const newTypes = filters.propertyType.includes(type) + ? filters.propertyType.filter(t => t !== type) + : [...filters.propertyType, type]; + handleFilterChange('propertyType', newTypes); + }; + + const handleClear = () => { + setSearchTerm(''); + setFilters({ + priceRange: [5000, 50000], + location: [], + amenities: [], + propertyType: [], + }); + }; + + return ( + + + setSearchTerm(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSearch()} + /> + setShowFilters(!showFilters)}> + + + + + + + + + + + + + + Price Range (BDT) + + + + + + handleFilterChange('priceRange', value)} + min={1000} + max={100000} + step={1000} + valueLabelDisplay="auto" + valueLabelFormat={(value) => `৳${value.toLocaleString()}`} + /> + + + + + + Location + + + {locations.map(({ label, icon }) => ( + toggleLocation(label)} + selected={filters.location.includes(label)} + /> + ))} + + + + + + + Property Type + + + {propertyTypes.map(({ label, icon }) => ( + togglePropertyType(label)} + selected={filters.propertyType.includes(label)} + /> + ))} + + + + + + Amenities + + + {amenities.map((amenity) => ( + { + const newAmenities = e.target.checked + ? [...filters.amenities, amenity] + : filters.amenities.filter(a => a !== amenity); + handleFilterChange('amenities', newAmenities); + }} + /> + } + label={amenity} + /> + ))} + + + + + + ); +}; + +export default GlassSearch; diff --git a/frontend/components/Layout.js b/frontend/components/Layout.js index f4a2ec3..720d564 100644 --- a/frontend/components/Layout.js +++ b/frontend/components/Layout.js @@ -175,174 +175,200 @@ function Layout({ children }) { ); return ( - - - - - {isMobile && ( - - - - )} - - - RENT HOUSE BD - + + + + + + {isMobile && ( + + + + )} - {!isMobile && ( - - {navItems.map((item) => ( - (!item.auth || (item.auth && user)) && ( - - ) - ))} - - )} + + RENT HOUSE BD + - - {user ? ( - <> - - - {user.avatar ? ( - - ) : ( - - {user.name.charAt(0).toUpperCase()} - - )} - - - - {menuItems.map((item) => ( - + {navItems.map((item) => ( + (!item.auth || (item.auth && user)) && ( + + ) + ))} + + )} + + + {user ? ( + <> + + + {user.avatar ? ( + + ) : ( + + {user.name.charAt(0).toUpperCase()} + + )} + + + + {menuItems.map((item) => ( + + + {item.icon} + + {item.name} + + ))} + + { + handleCloseUserMenu(); + handleLogout(); + }}> - {item.icon} + - {item.name} + Logout - ))} - - { - handleCloseUserMenu(); - handleLogout(); - }}> - - - - Logout - - - - ) : ( - - )} - - - - + + + ) : ( + + )} + + + + - - {drawer} - + + {drawer} + - {children} - { +const GlassAppBar = styled(AppBar)(({ theme }) => ({ + background: 'rgba(255, 255, 255, 0.01)', + backdropFilter: 'blur(10px)', + WebkitBackdropFilter: 'blur(10px)', + boxShadow: 'none', + position: 'fixed', + width: '100%', + top: 0, + left: 0, + right: 0, + margin: 0, + padding: 0, + zIndex: 1200, + borderBottom: '1px solid rgba(255, 255, 255, 0.1)', + + '&::after': { + content: '""', + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: '1px', + background: 'rgba(255, 255, 255, 0.1)', + }, + + '& .MuiToolbar-root': { + height: '64px', + minHeight: '64px !important', + padding: '0 24px', + }, +})); + +const StyledDrawer = styled(Drawer)(({ theme }) => ({ + '& .MuiDrawer-paper': { + background: 'transparent', + backdropFilter: 'blur(8px)', + WebkitBackdropFilter: 'blur(8px)', + borderRight: 'none', + width: 280, + padding: theme.spacing(2), + position: 'relative', + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%)', + zIndex: -1, + }, + + '&::after': { + content: '""', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + width: '1px', + background: 'linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.2) 50%, transparent 100%)', + }, + }, +})); + +const NavLink = styled(Link)(({ theme }) => ({ + color: theme.palette.text.primary, + textDecoration: 'none', + padding: theme.spacing(1, 2), + borderRadius: '8px', + transition: 'all 0.3s ease-in-out', + position: 'relative', + overflow: 'hidden', + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%)', + opacity: 0, + transition: 'opacity 0.3s ease-in-out', + zIndex: -1, + }, + + '&::after': { + content: '""', + position: 'absolute', + bottom: 0, + left: '50%', + width: 0, + height: '2px', + background: 'linear-gradient(90deg, #2196F3, #21CBF3)', + transition: 'all 0.3s ease-in-out', + transform: 'translateX(-50%)', + }, + + '&:hover': { + color: theme.palette.primary.main, + + '&::before': { + opacity: 1, + }, + + '&::after': { + width: '80%', + }, + }, +})); + +export default function Navbar() { const router = useRouter(); - const { logout } = useAuth(); - const { user, isAuthenticated, notifications, clearNotifications } = useSession(); - const [showNotifications, setShowNotifications] = useState(false); - const [showUserMenu, setShowUserMenu] = useState(false); - - const handleLogout = () => { - logout(); - router.push('/login'); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const { user, logout } = useAuth(); + const [mobileOpen, setMobileOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + + const handleDrawerToggle = () => { + setMobileOpen(!mobileOpen); }; - const toggleNotifications = () => { - setShowNotifications(!showNotifications); - if (showNotifications) { - clearNotifications(); - } + const handleMenuOpen = (event) => { + setAnchorEl(event.currentTarget); }; - const toggleUserMenu = () => { - setShowUserMenu(!showUserMenu); + const handleMenuClose = () => { + setAnchorEl(null); }; - return ( - + const handleLogout = async () => { + try { + await logout(); + router.push('/login'); + } catch (error) { + console.error('Failed to logout', error); + } + }; + + const menuItems = [ + { text: 'Home', icon: , href: '/' }, + { text: 'Search', icon: , href: '/search' }, + { text: 'Add Property', icon: , href: '/add-property' }, + { text: 'Favorites', icon: , href: '/favorites' }, + ]; + + const drawer = ( + + + + Rent House BD + + + + + + + + {menuItems.map((item) => ( + + {item.icon} + + + ))} + + ); -}; -export default Navbar; + return ( + <> + + + {isMobile ? ( + + + + ) : null} + + + Rent House BD + + + {!isMobile && ( + + {menuItems.map((item) => ( + + {item.text} + + ))} + + )} + + {user ? ( + <> + + + + + + + + {user.displayName} + + + {user.email} + + + + + Profile + + + Settings + + + + Logout + + + + ) : ( + + + Login + + + Register + + + )} + + + + {isMobile && ( + + {drawer} + + )} + + ); +} diff --git a/frontend/components/Navbar.module.css b/frontend/components/Navbar.module.css new file mode 100644 index 0000000..a35ac46 --- /dev/null +++ b/frontend/components/Navbar.module.css @@ -0,0 +1,79 @@ +.nav { + @apply bg-white shadow-lg; +} + +.container { + @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8; +} + +.wrapper { + @apply flex justify-between h-16; +} + +.logo { + @apply flex-shrink-0 flex items-center; +} + +.logoImage { + @apply h-8 w-auto; +} + +.navLinks { + @apply hidden sm:ml-6 sm:flex sm:space-x-8; +} + +.navLink { + @apply text-gray-700 hover:text-gray-900 inline-flex items-center px-1 pt-1 text-sm font-medium border-b-2 border-transparent hover:border-gray-300; +} + +.navLinkActive { + @apply text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 border-primary text-sm font-medium; +} + +.authButtons { + @apply flex space-x-4; +} + +.btnPrimary { + @apply bg-primary text-white hover:bg-primary-dark px-4 py-2 rounded-md text-sm font-medium transition-colors duration-200; +} + +.btnSecondary { + @apply text-gray-700 hover:text-gray-900 px-4 py-2 rounded-md text-sm font-medium border border-gray-300 hover:border-gray-400 transition-colors duration-200; +} + +.userSection { + @apply flex items-center; +} + +.notificationBtn { + @apply p-1 rounded-full hover:bg-gray-100 relative; +} + +.notificationIcon { + @apply h-6 w-6 text-gray-500; +} + +.notificationBadge { + @apply absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-400 ring-2 ring-white; +} + +.userMenuBtn { + @apply flex items-center space-x-2 p-2 rounded-full hover:bg-gray-100; +} + +.userAvatar { + @apply h-8 w-8 rounded-full; +} + +.userName { + @apply hidden md:block text-sm font-medium text-gray-700; +} + +.dropdownMenu { + @apply absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 z-50; +} + +.dropdownItem { + @apply block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left; +} diff --git a/frontend/components/PageContainer.js b/frontend/components/PageContainer.js new file mode 100644 index 0000000..5266747 --- /dev/null +++ b/frontend/components/PageContainer.js @@ -0,0 +1,51 @@ +import { Box, Paper } from '@mui/material'; + +const PageContainer = ({ children, maxWidth = 'lg' }) => { + return ( + theme.breakpoints.values[maxWidth], + mx: 'auto', + px: { xs: 2, sm: 3, md: 4 }, + py: { xs: 2, sm: 3 }, + }} + > + *': { + position: 'relative', + zIndex: 1, + }, + }} + > + {children} + + + ); +}; + +export default PageContainer; diff --git a/frontend/components/PropertyCard.js b/frontend/components/PropertyCard.js index 78e36da..6dd363e 100644 --- a/frontend/components/PropertyCard.js +++ b/frontend/components/PropertyCard.js @@ -1,6 +1,5 @@ import React from 'react'; import { - Card, CardMedia, CardContent, Typography, @@ -8,16 +7,19 @@ import { Chip, IconButton, Grid, - Divider + Divider, + Tooltip, } from '@mui/material'; import { LocationOn, AttachMoney, Home, Person, - Share + Share, + CalendarToday, } from '@mui/icons-material'; import SavePostButton from './SavePostButton'; +import GlassCard from './GlassCard'; function PropertyCard({ post, showSaveButton = true }) { const handleShare = () => { @@ -31,27 +33,76 @@ function PropertyCard({ post, showSaveButton = true }) { }; return ( - - - - - - - {post.propertyType} - - - {showSaveButton && } - + + + + + {showSaveButton && } + + - + + + + + + {post.propertyType} + @@ -62,7 +113,13 @@ function PropertyCard({ post, showSaveButton = true }) { - + {post.rent.toLocaleString()} BDT @@ -89,33 +146,51 @@ function PropertyCard({ post, showSaveButton = true }) { - + - + {post.amenities.slice(0, 3).map((amenity) => ( ))} {post.amenities.length > 3 && ( )} {post.availableFrom && ( - - Available from: {new Date(post.availableFrom).toLocaleDateString()} - + + + + Available from: {new Date(post.availableFrom).toLocaleDateString()} + + )} - + ); } diff --git a/frontend/components/auth/withAuth.js b/frontend/components/auth/withAuth.js new file mode 100644 index 0000000..f45576b --- /dev/null +++ b/frontend/components/auth/withAuth.js @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useAuth } from '@/contexts/AuthContext'; + +export function withAuth(WrappedComponent, options = {}) { + return function ProtectedRoute(props) { + const router = useRouter(); + const { user, loading } = useAuth(); + const { requireAuth = true, requiredRole = null } = options; + + useEffect(() => { + if (!loading) { + if (requireAuth && !user) { + router.replace(`/login?redirect=${router.pathname}`); + } else if (requiredRole && (!user || user.role !== requiredRole)) { + router.replace('/'); + } + } + }, [loading, user, router, requireAuth, requiredRole]); + + // Don't show anything while checking auth + if (loading) { + return null; + } + + // If auth is required and user is not logged in, don't render component + if (requireAuth && !user) { + return null; + } + + // If role is required and user doesn't have it, don't render component + if (requiredRole && (!user || user.role !== requiredRole)) { + return null; + } + + // Otherwise, render the protected component + return ; + }; +} + +export default withAuth; diff --git a/frontend/components/common/OptimizedImage.js b/frontend/components/common/OptimizedImage.js new file mode 100644 index 0000000..303d9a9 --- /dev/null +++ b/frontend/components/common/OptimizedImage.js @@ -0,0 +1,36 @@ +import Image from 'next/image'; +import { Box } from '@mui/material'; + +const OptimizedImage = ({ src, alt, width, height, layout, objectFit, ...props }) => { + // For external URLs that don't support optimization + if (src?.startsWith('http') || src?.startsWith('https')) { + return ( + + ); + } + + // For local images that can be optimized + return ( + {alt} + ); +}; + +export default OptimizedImage; diff --git a/frontend/components/layout/index.js b/frontend/components/layout/index.js new file mode 100644 index 0000000..681dcc8 --- /dev/null +++ b/frontend/components/layout/index.js @@ -0,0 +1,17 @@ +import { Box, Container } from '@mui/material'; +import Navbar from '../Navbar'; +import Footer from '../Footer'; + +const Layout = ({ children }) => { + return ( + + + + {children} + +