diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c2058b5 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df61d0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Environment files +.env + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.vite diff --git a/README.md b/README.md index a678080..c544cec 100644 --- a/README.md +++ b/README.md @@ -1,268 +1,216 @@ # RoomMateX - Room & Roommate Discovery Platform -A modern, production-ready room and roommate discovery platform built with React, Vite, and Tailwind CSS. - -## 🌟 Features - -### 🔐 Authentication & Authorization -- **Public Routes**: Landing page, Login, Signup (auto-redirect if logged in) -- **Protected Routes**: Full authentication guard -- **Role-Based Access**: Seeker, Owner, and Admin roles with specific permissions - -### 👤 User Roles - -#### Room Seeker -- Browse and search verified rooms -- Advanced filtering (city, rent, gender, amenities) -- Wishlist functionality -- Message owners -- Leave reviews - -#### Room Owner -- List rooms with comprehensive details -- Photo gallery (min 3, max 10 images) -- View potential roommates -- Manage listings -- Respond to inquiries - -#### Admin -- User management dashboard -- Room moderation -- Report handling system -- Analytics and insights -- Platform-wide controls - -### 🏠 Core Features -- **Room Discovery**: Advanced search and filtering -- **Verified Listings**: Trust badges and verification system -- **Reviews & Ratings**: User and room reviews -- **Trust & Safety**: Report system for users and rooms -- **Responsive Design**: Mobile-first, fully responsive UI -- **Modern UX**: Loading states, empty states, animations - -## 🚀 Quick Start +A modern, production-level room and roommate discovery platform built with React, Vite, and Tailwind CSS. -```bash -# Install dependencies -npm install +## Features -# Run development server -npm run dev +### User Features +- **Dashboard**: Dynamic dashboard with recommended rooms, roommates, and personalized sections +- **Find Rooms**: Browse and filter rooms by city, rent range, room type, and amenities +- **Room Details**: Comprehensive room information with image gallery, amenities, and owner contact +- **Roommate Profiles**: View detailed roommate profiles with preferences and interests +- **Wishlist**: Save favorite rooms for later viewing +- **List Rooms**: Full CRUD functionality for room listings +- **Profile Management**: Edit profile with completion score tracking +- **Contact**: Contact form for support inquiries -# Build for production -npm run build -``` +### Admin Features +- **Admin Dashboard**: Overview of platform statistics +- **Manage Users**: View, verify, and change user roles +- **Manage Rooms**: Activate/deactivate and remove room listings + +### Technical Features +- Fully working navigation with React Router +- Context API for state management +- Protected routes with role-based access +- Responsive design (mobile-first) +- Toast notifications +- Modal dialogs +- LocalStorage persistence +- Professional UI with Tailwind CSS + +## Installation + +### Prerequisites +- Node.js (v16 or higher) +- npm or yarn + +### Setup -The app will be available at `http://localhost:5173` +1. **Install dependencies**: + ```bash + npm install + ``` -## 📁 Project Structure +2. **Run development server**: + ```bash + npm run dev + ``` + +3. **Open browser**: + Navigate to `http://localhost:3000` + +## Demo Credentials + +### Admin Account +- **Email**: admin@roomatex.com +- **Password**: admin123 + +### User Account +- **Email**: any valid email +- **Password**: any password + +## Project Structure ``` roomatex/ ├── src/ │ ├── admin/ # Admin pages -│ │ ├── AdminUsers.jsx -│ │ ├── AdminRooms.jsx -│ │ └── AdminReports.jsx +│ │ ├── AdminDashboard.jsx +│ │ ├── ManageUsers.jsx +│ │ └── ManageRooms.jsx │ ├── components/ # Reusable components -│ │ └── ui/ # UI components -│ │ ├── Button.jsx -│ │ ├── Input.jsx -│ │ ├── Card.jsx -│ │ ├── Badge.jsx -│ │ ├── Alert.jsx -│ │ ├── Modal.jsx -│ │ ├── Loader.jsx -│ │ └── EmptyState.jsx -│ ├── context/ # React contexts +│ │ ├── ui/ +│ │ │ ├── Toast.jsx +│ │ │ └── Modal.jsx +│ │ └── Navbar.jsx +│ ├── context/ # React Context │ │ └── AuthContext.jsx -│ ├── data/ # Mock data -│ │ ├── rooms.js -│ │ ├── reviews.js -│ │ └── admin.js -│ ├── layouts/ # Layout components -│ │ ├── Navbar.jsx -│ │ └── MainLayout.jsx -│ ├── pages/ # Application pages -│ │ ├── Landing.jsx -│ │ ├── Login.jsx -│ │ ├── Signup.jsx -│ │ ├── Dashboard.jsx -│ │ ├── Rooms.jsx -│ │ ├── RoomDetails.jsx -│ │ ├── AddRoom.jsx -│ │ ├── Profile.jsx -│ │ ├── Messages.jsx -│ │ ├── Reviews.jsx -│ │ ├── Wishlist.jsx -│ │ ├── Info.jsx -│ │ └── NotFound.jsx -│ ├── routes/ # Route guards -│ │ ├── PrivateRoute.jsx -│ │ ├── PublicRoute.jsx -│ │ └── RoleRoute.jsx -│ ├── styles/ # Global styles +│ ├── data/ # Mock data +│ │ └── mockData.js +│ ├── pages/ # Page components +│ │ ├── LandingPage.jsx +│ │ ├── LoginPage.jsx +│ │ ├── SignupPage.jsx +│ │ ├── DashboardPage.jsx +│ │ ├── RoomsPage.jsx +│ │ ├── RoomDetailsPage.jsx +│ │ ├── RoommateProfilePage.jsx +│ │ ├── AddRoomPage.jsx +│ │ ├── ProfilePage.jsx +│ │ ├── WishlistPage.jsx +│ │ └── ContactPage.jsx +│ ├── routes/ # Route protection +│ │ └── ProtectedRoute.jsx +│ ├── styles/ # Global styles │ │ └── index.css -│ ├── App.jsx # Main app component -│ └── main.jsx # Entry point +│ ├── App.jsx # Main app component +│ └── main.jsx # Entry point ├── index.html ├── package.json +├── vite.config.js ├── tailwind.config.js -├── postcss.config.js -└── vite.config.js +└── postcss.config.js ``` -## 🎨 Design System +## Design System ### Colors -- **Primary**: Light Green (#22c55e) -- **Dark**: Neutral grays for text and UI elements -- **Semantic**: Success (green), Warning (yellow), Danger (red), Info (blue) +- **Primary**: Blue tones (#0ea5e9) +- **Secondary**: Teal/Emerald (#14b8a6) +- **Accent**: Amber (#f59e0b) +- **Background**: Off-white/Gray (#f9fafb) ### Typography -- **Display Font**: Clash Display (headings) -- **Body Font**: DM Sans (content) +- **Display**: Poppins (headings, titles) +- **Body**: Inter (paragraphs, text) -### Components -- Consistent card-based layouts -- Soft shadows and rounded corners -- Smooth animations and transitions -- Professional color palette +## User Flow -## 🔑 Demo Credentials +### Public Routes +1. **Landing Page** (`/`) - Marketing page with features and testimonials +2. **Login** (`/login`) - User authentication +3. **Signup** (`/signup`) - New user registration + +### Protected Routes (Logged In) +1. **Dashboard** (`/dashboard`) - Personalized home with recommendations +2. **Find Rooms** (`/rooms`) - Browse and filter available rooms +3. **Room Details** (`/room/:id`) - Full room information +4. **Roommate Profile** (`/roommate/:id`) - View roommate details +5. **List Room** (`/add-room`) - Add new room listing +6. **Profile** (`/profile`) - User profile with tabs (Profile, Listings, Wishlist, Settings) +7. **Wishlist** (`/wishlist`) - Saved rooms +8. **Contact** (`/contact`) - Support contact form -```javascript -// Seeker Account -Email: seeker@test.com -Password: password +### Admin Routes +1. **Admin Dashboard** (`/admin`) - Platform overview +2. **Manage Users** (`/admin/users`) - User management +3. **Manage Rooms** (`/admin/rooms`) - Room moderation + +## Key Features + +### Dynamic Dashboard Sections +- Recommended Rooms (always visible) +- Recommended Roommates (always visible) +- My Listings (conditional - only if user has listings) +- Recently Viewed (conditional - only if user has viewed rooms) +- Wishlist (conditional - only if user has wishlist items) + +### Room Management (Full CRUD) +- **Create**: Add new room with validation (min 3 images) +- **Read**: View room details with gallery +- **Update**: Edit existing room listings +- **Delete**: Remove rooms with confirmation modal + +### Profile Completion +- Tracks completion percentage based on: + - Profile photo + - Phone number + - City + - Room listings + - Wishlist activity + +## Technologies + +- **Frontend**: React 18 +- **Build Tool**: Vite +- **Routing**: React Router DOM v6 +- **Styling**: Tailwind CSS 3 +- **State Management**: Context API +- **Storage**: LocalStorage + +## Responsive Design -// Owner Account -Email: owner@test.com -Password: password +- Mobile-first approach +- Breakpoints: sm, md, lg, xl +- Hamburger menu for mobile +- Optimized layouts for all screen sizes -// Admin Account -Email: admin@test.com -Password: password -``` +## Authentication -## 🛣️ Route Structure +- Role-based access control (user, admin) +- Protected routes +- Public routes redirect to dashboard if authenticated +- LocalStorage persistence -### Public Routes -- `/` - Landing page -- `/login` - Login page -- `/signup` - Signup page - -### Protected Routes (All Users) -- `/dashboard` - Role-based dashboard -- `/rooms` - Browse rooms (Seeker) -- `/room/:id` - Room details -- `/profile` - User profile -- `/messages` - Chat/messages -- `/reviews` - Reviews -- `/wishlist` - Saved rooms -- `/info` - Info/FAQ - -### Owner Routes -- `/add-room` - Create new listing +## Production Ready -### Admin Routes -- `/admin/users` - User management -- `/admin/rooms` - Room management -- `/admin/reports` - Handle reports +- No broken links or dead buttons +- All routes fully functional +- Proper error handling +- Professional UI/UX +- Clean code structure +- Ready for deployment -## 🎯 Key Features Implementation +## Notes -### Trust & Safety System -- User verification badges -- Report functionality for rooms and users -- Trust score system -- Review moderation (admin) +- All data is stored in browser LocalStorage +- Mock data includes 6 rooms and 6 roommates +- Contact form submissions show success message (no backend) +- Images use Unsplash and Pravatar for demo purposes -### UX Polish -- **Loading States**: Skeleton screens, spinners -- **Empty States**: Helpful messages when no data -- **Status Alerts**: Success, error, warning notifications -- **Animations**: Smooth transitions and micro-interactions +## Build for Production -### Responsive Design -- Mobile-first approach -- Breakpoints: sm (640px), md (768px), lg (1024px), xl (1280px) -- Touch-friendly interactions -- Adaptive layouts - -## 🔧 Tech Stack - -- **React 18** - UI library -- **Vite** - Build tool -- **React Router v6** - Routing -- **Tailwind CSS** - Styling -- **Lucide React** - Icons -- **Local Storage** - Data persistence - -## 📱 Features by Page - -### Landing Page -- Hero with typing animation -- Feature showcase -- Demo room cards -- Testimonials -- FAQ section -- Footer - -### Dashboard -- Role-specific content -- Quick stats -- Recommended rooms (Seeker) -- Potential roommates (Owner) -- Admin analytics (Admin) - -### Room Listing -- Advanced filters -- Sort options -- Grid/list views -- Pagination ready - -### Room Details -- Image gallery -- Full description -- Amenities list -- House rules -- Owner profile -- Reviews section -- Wishlist toggle -- Report functionality - -### Profile -- View/edit mode -- Trust score display -- Activity stats -- Settings - -### Admin Dashboard -- User management -- Room moderation -- Report handling -- Analytics - -## 🚧 Future Enhancements - -- Real-time messaging -- Payment integration -- Email notifications -- Advanced search filters -- Map integration -- Photo verification -- Calendar for availability -- Mobile app - -## 📄 License - -This is a portfolio/educational project. - -## 🤝 Contributing - -This is a demonstration project. Feel free to fork and customize for your needs. +```bash +npm run build +``` + +This creates an optimized production build in the `dist` folder. + +## License + +This project is for educational purposes. --- -Built with ❤️ using React, Vite, and Tailwind CSS +**Built with care using React + Vite + Tailwind CSS** diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md deleted file mode 100644 index 553ae94..0000000 --- a/SETUP_GUIDE.md +++ /dev/null @@ -1,280 +0,0 @@ -# RoomMateX - Setup & Installation Guide - -## 📦 Quick Setup - -1. **Extract the project folder** to your preferred location - -2. **Navigate to the project directory**: - ```bash - cd roomatex - ``` - -3. **Install dependencies**: - ```bash - npm install - ``` - -4. **Start the development server**: - ```bash - npm run dev - ``` - -5. **Open your browser** and visit: `http://localhost:5173` - -## 🎯 First Steps - -### 1. Login with Demo Accounts - -Use these credentials to explore different user roles: - -**Room Seeker:** -- Email: `seeker@test.com` -- Password: `password` - -**Room Owner:** -- Email: `owner@test.com` -- Password: `password` - -**Admin:** -- Email: `admin@test.com` -- Password: `password` - -### 2. Explore Features - -**As a Seeker:** -1. Browse rooms on the Dashboard -2. Use filters to find specific rooms -3. Click on any room to view details -4. Add rooms to wishlist -5. View your profile and reviews - -**As an Owner:** -1. Click "List Room" to add a new room -2. Fill in all details and upload at least 3 photos -3. View potential roommates on the Dashboard -4. Manage your listings - -**As an Admin:** -1. Access admin dashboard -2. Manage users and rooms -3. Handle reports -4. View platform analytics - -## 🏗️ Project Architecture - -### Component Structure -``` -UI Components (src/components/ui/) -├── Button.jsx - Reusable button with variants -├── Input.jsx - Form input with label and error handling -├── Card.jsx - Container with hover effects -├── Badge.jsx - Status badges -├── Alert.jsx - Notification alerts -├── Modal.jsx - Modal dialogs -├── Loader.jsx - Loading states and skeletons -└── EmptyState.jsx - Empty state messages -``` - -### Authentication Flow -``` -1. User visits Landing page (/) -2. Clicks Login/Signup -3. After authentication -> Redirects to /dashboard -4. Can never access landing page again while logged in -5. Protected routes check authentication -6. Role routes check user role permissions -``` - -### Routing Guard System -``` -PublicRoute (/, /login, /signup) -└── If authenticated -> Redirect to /dashboard - -PrivateRoute (All app pages) -└── If not authenticated -> Redirect to /login - -RoleRoute (Owner/Admin pages) -└── If wrong role -> Redirect to /dashboard -``` - -## 🎨 Customization Guide - -### Changing Colors - -Edit `tailwind.config.js`: -```javascript -colors: { - primary: { - // Change these hex values - 500: '#22c55e', - 600: '#16a34a', - // ... - } -} -``` - -### Adding New Pages - -1. Create page component in `src/pages/` -2. Add route in `src/App.jsx` -3. Add navigation link in `src/layouts/Navbar.jsx` - -Example: -```jsx -// In App.jsx - - - -} /> -``` - -### Modifying Dummy Data - -Edit files in `src/data/`: -- `rooms.js` - Room listings -- `reviews.js` - User reviews -- `admin.js` - Admin statistics - -## 🚀 Deployment - -### Build for Production -```bash -npm run build -``` - -This creates a `dist` folder with optimized files. - -### Deploy to Vercel -```bash -npm install -g vercel -vercel -``` - -### Deploy to Netlify -1. Connect your GitHub repository -2. Build command: `npm run build` -3. Publish directory: `dist` - -## 🐛 Troubleshooting - -### Issue: "npm install" fails -**Solution**: Make sure you have Node.js 16+ installed -```bash -node --version # Should be 16 or higher -``` - -### Issue: Port 5173 already in use -**Solution**: Kill the process or use a different port -```bash -# Use different port -npm run dev -- --port 3000 -``` - -### Issue: Styles not loading -**Solution**: Make sure Tailwind is properly configured -```bash -# Rebuild -rm -rf node_modules -npm install -npm run dev -``` - -### Issue: Images not loading -**Solution**: Check your internet connection (images are from external URLs) - -## 📱 Browser Compatibility - -- Chrome/Edge: ✅ Full support -- Firefox: ✅ Full support -- Safari: ✅ Full support -- Mobile browsers: ✅ Full support - -## 🔧 Development Tips - -### Hot Module Replacement -Vite provides instant HMR. Save any file and see changes immediately. - -### React DevTools -Install React DevTools browser extension for better debugging. - -### VS Code Extensions (Recommended) -- ES7+ React/Redux/React-Native snippets -- Tailwind CSS IntelliSense -- Prettier - Code formatter -- ESLint - -## 📚 Learning Resources - -### React -- [Official React Docs](https://react.dev) -- [React Router Docs](https://reactrouter.com) - -### Tailwind CSS -- [Tailwind Docs](https://tailwindcss.com/docs) -- [Tailwind UI](https://tailwindui.com) - -### Vite -- [Vite Docs](https://vitejs.dev) - -## 🎓 Code Examples - -### Creating a New Component -```jsx -// src/components/ui/NewComponent.jsx -export const NewComponent = ({ children, variant = 'default' }) => { - return ( -
- {children} -
- ); -}; -``` - -### Using Context -```jsx -import { useAuth } from '../context/AuthContext'; - -function MyComponent() { - const { user, logout } = useAuth(); - - return
Welcome, {user.name}!
; -} -``` - -### Protected Route Example -```jsx - - - -} /> -``` - -## 💡 Best Practices - -1. **Always use Tailwind classes** instead of custom CSS -2. **Keep components small** and focused -3. **Use the existing UI components** for consistency -4. **Follow the folder structure** for organization -5. **Test in multiple screen sizes** for responsiveness - -## 🆘 Getting Help - -If you encounter issues: -1. Check the console for error messages -2. Review this setup guide -3. Check the README.md file -4. Inspect the code comments - -## 🎉 Next Steps - -1. ✅ Run the project -2. ✅ Test all demo accounts -3. ✅ Explore all pages -4. 🔄 Customize for your needs -5. 🚀 Deploy to production - ---- - -**Congratulations!** You now have a fully functional room discovery platform. Happy coding! 🎊 diff --git a/index.html b/index.html index 69dc5a0..84e1f52 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,15 @@ - + - + + + - RoomMateX - Find Your Perfect Room & Roommate - - + + RoomMateX - Find Your Perfect Room & Roommate
diff --git a/package-lock.json b/package-lock.json index 4662558..a17f70b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,22 @@ "name": "roomatex", "version": "1.0.0", "dependencies": { - "lucide-react": "^0.263.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.22.0" + "axios": "^1.13.5", + "lucide-react": "^0.563.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-easy-crop": "^5.5.6", + "react-router-dom": "^6.20.0", + "recharts": "^3.7.0" }, "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.3", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "tailwindcss": "^3.4.17", - "vite": "^5.4.11" + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "vite": "^5.0.8" } }, "node_modules/@alloc/quick-lru": { @@ -37,9 +40,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -52,9 +55,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -62,22 +65,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -94,14 +97,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -224,13 +227,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -287,18 +290,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -306,9 +309,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -798,6 +801,42 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -815,9 +854,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", - "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -829,9 +868,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", - "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -843,9 +882,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", - "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -857,9 +896,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", - "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -871,9 +910,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", - "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -885,9 +924,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", - "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -899,9 +938,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", - "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -913,9 +952,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", - "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -927,9 +966,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", - "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -941,9 +980,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", - "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -955,9 +994,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", - "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", "cpu": [ "loong64" ], @@ -969,9 +1008,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", - "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -983,9 +1022,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", - "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", "cpu": [ "ppc64" ], @@ -997,9 +1036,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", - "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -1011,9 +1050,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", - "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -1025,9 +1064,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", - "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -1039,9 +1078,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", - "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -1053,9 +1092,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", - "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -1067,9 +1106,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", - "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -1081,9 +1120,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", - "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", "cpu": [ "x64" ], @@ -1095,9 +1134,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", - "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -1109,9 +1148,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", - "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -1123,9 +1162,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", - "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -1137,9 +1176,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", - "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -1151,9 +1190,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", - "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -1164,6 +1203,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1209,6 +1260,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1220,14 +1334,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -1245,6 +1359,12 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1294,10 +1414,16 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "dev": true, "funding": [ { @@ -1316,7 +1442,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -1331,6 +1457,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -1402,6 +1539,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1413,9 +1563,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001766", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", - "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true, "funding": [ { @@ -1471,6 +1621,27 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1505,9 +1676,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1526,6 +1818,21 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1540,13 +1847,82 @@ "dev": true, "license": "MIT" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.279", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", - "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true, "license": "ISC" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1596,6 +1972,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1649,6 +2031,42 @@ "node": ">=8" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1682,7 +2100,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1698,6 +2115,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1711,11 +2165,49 @@ "node": ">=10.13.0" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1724,6 +2216,25 @@ "node": ">= 0.4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1872,12 +2383,21 @@ } }, "node_modules/lucide-react": { - "version": "0.263.1", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.263.1.tgz", - "integrity": "sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw==", + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, "node_modules/merge2": { @@ -1904,6 +2424,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1959,6 +2500,12 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2190,6 +2737,12 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2238,6 +2791,51 @@ "react": "^18.3.1" } }, + "node_modules/react-easy-crop": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.5.6.tgz", + "integrity": "sha512-Jw3/ozs8uXj3NpL511Suc4AHY+mLRO23rUgipXvNYKqezcFSYHxe4QXibBymkOoY6oOtLVMPO2HNPRHYvMPyTw==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "^2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2303,6 +2901,58 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -2336,9 +2986,9 @@ } }, "node_modules/rollup": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", - "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -2352,31 +3002,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, @@ -2530,6 +3180,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2599,6 +3255,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2630,6 +3292,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2637,6 +3308,28 @@ "dev": true, "license": "MIT" }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/package.json b/package.json index d494a24..91aa37b 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,21 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.22.0", - "lucide-react": "^0.263.1" + "axios": "^1.13.5", + "lucide-react": "^0.563.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-easy-crop": "^5.5.6", + "react-router-dom": "^6.20.0", + "recharts": "^3.7.0" }, "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.3", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "tailwindcss": "^3.4.17", - "vite": "^5.4.11" + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "vite": "^5.0.8" } } diff --git a/public/logos/favacrop.ico b/public/logos/favacrop.ico new file mode 100644 index 0000000..2b35ee6 Binary files /dev/null and b/public/logos/favacrop.ico differ diff --git a/public/logos/icon-512.png b/public/logos/icon-512.png new file mode 100644 index 0000000..88ae1c9 Binary files /dev/null and b/public/logos/icon-512.png differ diff --git a/public/logos/logocrop.ico b/public/logos/logocrop.ico new file mode 100644 index 0000000..c189e8e Binary files /dev/null and b/public/logos/logocrop.ico differ diff --git a/public/logos/logocrop.png b/public/logos/logocrop.png new file mode 100644 index 0000000..515e890 Binary files /dev/null and b/public/logos/logocrop.png differ diff --git a/public/logos/logocrop.svg b/public/logos/logocrop.svg new file mode 100644 index 0000000..b4f483b --- /dev/null +++ b/public/logos/logocrop.svg @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logos/logocrop2.png b/public/logos/logocrop2.png new file mode 100644 index 0000000..3a91fa8 Binary files /dev/null and b/public/logos/logocrop2.png differ diff --git a/src/App.jsx b/src/App.jsx index b0336cf..d59bd62 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,124 +1,118 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; -import { PrivateRoute } from './routes/PrivateRoute'; -import { PublicRoute } from './routes/PublicRoute'; -import { RoleRoute } from './routes/RoleRoute'; +import { ThemeProvider } from './context/ThemeContext'; +import { ToastProvider } from './context/ToastContext'; +import { ProtectedRoute } from './components/ProtectedRoute'; +import { AdminProtectedRoute } from './components/AdminProtectedRoute'; -// Public Pages -import { Landing } from './pages/Landing'; -import { Login } from './pages/Login'; -import { Signup } from './pages/Signup'; - -// Protected Pages -import { Dashboard } from './pages/Dashboard'; -import { Rooms } from './pages/Rooms'; -import { RoomDetails } from './pages/RoomDetails'; -import { AddRoom } from './pages/AddRoom'; -import { Profile } from './pages/Profile'; -import { Messages } from './pages/Messages'; -import { Reviews } from './pages/Reviews'; -import { Wishlist } from './pages/Wishlist'; -import { Info } from './pages/Info'; - -// Admin Pages -import { AdminUsers } from './admin/AdminUsers'; -import { AdminRooms } from './admin/AdminRooms'; -import { AdminReports } from './admin/AdminReports'; - -// 404 -import { NotFound } from './pages/NotFound'; +// Pages +import { LandingPage } from './pages/LandingPage'; +import { LoginPage } from './pages/LoginPage'; +import { SignupPage } from './pages/SignupPage'; +import { VerificationPage } from './pages/VerificationPage'; +import { RoomsPage } from './pages/RoomsPage'; +import { RoomDetailsPage } from './pages/RoomDetailsPage'; +import { AddRoomPage } from './pages/AddRoomPage'; +import { EditRoomPage } from './pages/EditRoomPage'; +import { DashboardPage } from './pages/DashboardPage'; +import { ProfilePage } from './pages/ProfilePage'; +import { WishlistPage } from './pages/WishlistPage'; +import { ContactPage } from './pages/ContactPage'; +import { AdminDashboardPage } from './pages/admin'; +import { CompleteProfilePage } from './pages/CompleteProfilePage'; +import { UserProfileViewPage } from './pages/UserProfileViewPage'; function App() { return ( - - - - {/* Public Routes */} - - - - } /> - - - - } /> - - - - } /> + + + + + + {/* Public Routes */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> - {/* Protected Routes */} - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> + {/* Protected Routes */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* Admin Routes */} - - - - } /> - - - - } /> - - - - } /> + {/* Admin Routes - Protected with AdminProtectedRoute */} + + + + } + /> - {/* 404 */} - } /> - - - + {/* Catch all - redirect to home */} + } /> + + + + + ); } diff --git a/src/admin/AdminReports.jsx b/src/admin/AdminReports.jsx deleted file mode 100644 index a670bf8..0000000 --- a/src/admin/AdminReports.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Button } from '../components/ui/Button'; -import { Badge } from '../components/ui/Badge'; -import { reports } from '../data/admin'; - -export const AdminReports = () => { - const getSeverityColor = (severity) => { - switch (severity) { - case 'critical': return 'danger'; - case 'high': return 'warning'; - case 'medium': return 'info'; - default: return 'info'; - } - }; - - return ( - -
-

Reports

- -
- {reports.map((report) => ( - -
-
-
- - {report.severity.toUpperCase()} - - {report.type} - - {report.status} - -
-

- {report.targetName} -

-

- Reported by {report.reporterName} • {report.date} -

-
-
-
-
- Reason: - {report.reason} -
-
- Description: - {report.description} -
-
- {report.status === 'pending' && ( -
- - - -
- )} -
- ))} -
-
-
- ); -}; diff --git a/src/admin/AdminRooms.jsx b/src/admin/AdminRooms.jsx deleted file mode 100644 index db6fee9..0000000 --- a/src/admin/AdminRooms.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Button } from '../components/ui/Button'; -import { Badge } from '../components/ui/Badge'; -import { rooms } from '../data/rooms'; -import { MapPin, Eye } from 'lucide-react'; - -export const AdminRooms = () => { - return ( - -
-

Manage Rooms

- -
- {rooms.map((room) => ( - -
- {room.title} -
-
-
-

{room.title}

-
- - {room.area}, {room.city} -
-
-
-
₹{room.rent.toLocaleString()}
-
per month
-
-
-

{room.description}

-
- - {room.ownerVerified ? 'Verified Owner' : 'Unverified'} - - {room.roomType} -
- - 0 views -
-
-
-
- - - -
-
-
- ))} -
-
-
- ); -}; diff --git a/src/admin/AdminUsers.jsx b/src/admin/AdminUsers.jsx deleted file mode 100644 index 2b08d96..0000000 --- a/src/admin/AdminUsers.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Button } from '../components/ui/Button'; -import { Badge } from '../components/ui/Badge'; -import { Shield, Search } from 'lucide-react'; - -export const AdminUsers = () => { - const users = [ - { id: '1', name: 'Rahul Kumar', email: 'rahul@example.com', role: 'seeker', verified: true, trustScore: 4.8 }, - { id: '2', name: 'Priya Sharma', email: 'priya@example.com', role: 'owner', verified: true, trustScore: 4.9 }, - { id: '3', name: 'Amit Patel', email: 'amit@example.com', role: 'seeker', verified: false, trustScore: 4.2 }, - ]; - - return ( - -
-
-

Manage Users

-
- - -
-
- - -
- - - - - - - - - - - - {users.map((user) => ( - - - - - - - - ))} - -
UserRoleStatusTrust ScoreActions
-
-
{user.name}
-
{user.email}
-
-
- {user.role} - - {user.verified ? ( - - - Verified - - ) : ( - Unverified - )} - {user.trustScore} -
- - -
-
-
-
-
-
- ); -}; diff --git a/src/assets/logo.png b/src/assets/logo.png deleted file mode 100644 index 38ceb4b..0000000 Binary files a/src/assets/logo.png and /dev/null differ diff --git a/src/components/AdminProtectedRoute.jsx b/src/components/AdminProtectedRoute.jsx new file mode 100644 index 0000000..a704093 --- /dev/null +++ b/src/components/AdminProtectedRoute.jsx @@ -0,0 +1,13 @@ +import { Navigate } from 'react-router-dom'; +import { adminService } from '../services/adminService'; + +export const AdminProtectedRoute = ({ children }) => { + const isAdmin = adminService.isAdmin(); + + if (!isAdmin) { + // Redirect to login if not admin + return ; + } + + return children; +}; diff --git a/src/components/LoadingSpinner.jsx b/src/components/LoadingSpinner.jsx new file mode 100644 index 0000000..cdde6cc --- /dev/null +++ b/src/components/LoadingSpinner.jsx @@ -0,0 +1,15 @@ +export const LoadingSpinner = ({ size = 'md', className = '', text = '' }) => { + const sizeClasses = { + sm: 'h-6 w-6', + md: 'h-8 w-8', + lg: 'h-12 w-12', + xl: 'h-16 w-16' + }; + + return ( +
+
+ {text &&

{text}

} +
+ ); +}; \ No newline at end of file diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx new file mode 100644 index 0000000..a966016 --- /dev/null +++ b/src/components/Navbar.jsx @@ -0,0 +1,209 @@ +import { Link, useNavigate, useLocation } from 'react-router-dom'; +import { useTheme } from '../context/ThemeContext'; +import { useAuth } from '../context/AuthContext'; +import { useState, useEffect } from 'react'; +import { Bell } from 'lucide-react'; +import { getImageUrl } from '../utils/imageUtils'; +import { notificationService } from '../services/notificationService'; +import { NotificationsModal } from './modal/NotificationsModal'; + +export const Navbar = () => { + const { theme } = useTheme(); + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [unreadCount, setUnreadCount] = useState(0); + const [showNotificationsModal, setShowNotificationsModal] = useState(false); + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 20); + window.addEventListener('scroll', onScroll); + return () => window.removeEventListener('scroll', onScroll); + }, []); + + useEffect(() => { + if (user) { + fetchUnreadCount(); + // Poll for new notifications every 30 seconds + const interval = setInterval(fetchUnreadCount, 30000); + return () => clearInterval(interval); + } + }, [user]); + + const fetchUnreadCount = async () => { + try { + const data = await notificationService.getUnreadCount(); + setUnreadCount(data.unread_count); + } catch (error) { + // Silently handle unread count fetch errors + } + }; + + const handleLogout = () => { + logout(); + navigate('/'); + }; + + return ( +
+ + + {/* Notifications Modal */} + setShowNotificationsModal(false)} + onUnreadCountChange={fetchUnreadCount} + /> +
+ ); +}; diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..c17ad02 --- /dev/null +++ b/src/components/ProtectedRoute.jsx @@ -0,0 +1,21 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { LoadingSpinner } from './LoadingSpinner'; + +export const ProtectedRoute = ({ children }) => { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!user) { + return ; + } + + return children; +}; diff --git a/src/components/modal/ConfirmModal.jsx b/src/components/modal/ConfirmModal.jsx new file mode 100644 index 0000000..4b73cee --- /dev/null +++ b/src/components/modal/ConfirmModal.jsx @@ -0,0 +1,74 @@ +import { XCircle, AlertTriangle } from 'lucide-react'; + +export const ConfirmModal = ({ + isOpen, + onClose, + onConfirm, + title = "Confirm Action", + message = "Are you sure you want to proceed?", + confirmText = "Confirm", + cancelText = "Cancel", + type = "warning" // warning, danger, info +}) => { + if (!isOpen) return null; + + const getTypeStyles = () => { + switch (type) { + case 'danger': + return { + iconBg: 'bg-red-100', + iconColor: 'text-red-600', + confirmBtn: 'bg-red-600 hover:bg-red-700 text-white' + }; + case 'info': + return { + iconBg: 'bg-blue-100', + iconColor: 'text-blue-600', + confirmBtn: 'bg-blue-600 hover:bg-blue-700 text-white' + }; + default: // warning + return { + iconBg: 'bg-yellow-100', + iconColor: 'text-yellow-600', + confirmBtn: 'bg-yellow-600 hover:bg-yellow-700 text-white' + }; + } + }; + + const styles = getTypeStyles(); + + return ( +
+
+ {/* Header */} +
+
+ +
+
+

{title}

+
+
+ + {/* Message */} +

{message}

+ + {/* Actions */} +
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/modal/ImageCropModal.jsx b/src/components/modal/ImageCropModal.jsx new file mode 100644 index 0000000..4927034 --- /dev/null +++ b/src/components/modal/ImageCropModal.jsx @@ -0,0 +1,147 @@ +import { useState, useCallback } from 'react'; +import Cropper from 'react-easy-crop'; + +export const ImageCropModal = ({ image, onCropComplete, onCancel }) => { + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + + const onCropChange = (crop) => { + setCrop(crop); + }; + + const onZoomChange = (zoom) => { + setZoom(zoom); + }; + + const onCropCompleteCallback = useCallback((croppedArea, croppedAreaPixels) => { + setCroppedAreaPixels(croppedAreaPixels); + }, []); + + const handleSave = async () => { + if (!croppedAreaPixels) return; + + try { + const croppedImage = await getCroppedImg(image, croppedAreaPixels); + onCropComplete(croppedImage); + } catch (error) { + // Silently handle crop errors + } + }; + + return ( +
+
+ {/* Header */} +
+

Crop Profile Photo

+ +
+ + {/* Cropper Area */} +
+ +
+ + {/* Controls */} +
+
+ + setZoom(parseFloat(e.target.value))} + className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary-600" + /> +
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ ); +}; + +// Helper function to create cropped image +const createImage = (url) => + new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => resolve(image)); + image.addEventListener('error', (error) => reject(error)); + image.setAttribute('crossOrigin', 'anonymous'); + image.src = url; + }); + +const getCroppedImg = async (imageSrc, pixelCrop) => { + const image = await createImage(imageSrc); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + return null; + } + + // Set canvas size to match the cropped area + canvas.width = pixelCrop.width; + canvas.height = pixelCrop.height; + + // Draw the cropped image + ctx.drawImage( + image, + pixelCrop.x, + pixelCrop.y, + pixelCrop.width, + pixelCrop.height, + 0, + 0, + pixelCrop.width, + pixelCrop.height + ); + + // Convert canvas to blob + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Canvas is empty')); + return; + } + resolve(blob); + }, 'image/jpeg', 0.95); + }); +}; diff --git a/src/components/modal/Modal.jsx b/src/components/modal/Modal.jsx new file mode 100644 index 0000000..3c1bcf2 --- /dev/null +++ b/src/components/modal/Modal.jsx @@ -0,0 +1,29 @@ +export const Modal = ({ isOpen, onClose, title, children }) => { + if (!isOpen) return null; + + return ( +
+
+
+ + + +
+
+
+
+

+ {title} +

+
{children}
+
+
+
+
+
+
+ ); +}; diff --git a/src/components/modal/NotificationsModal.jsx b/src/components/modal/NotificationsModal.jsx new file mode 100644 index 0000000..6135fc5 --- /dev/null +++ b/src/components/modal/NotificationsModal.jsx @@ -0,0 +1,247 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Bell, AlertCircle, CheckCircle, XCircle, Clock, X, Check, Home, FileText } from 'lucide-react'; +import { notificationService } from '../../services/notificationService'; + +const StatusBadge = ({ status }) => { + const config = { + fixed: { icon: , label: 'Fixed', cls: 'bg-green-50 text-green-700 border-green-200' }, + rejected: { icon: , label: 'Rejected', cls: 'bg-red-50 text-red-700 border-red-200' }, + pending: { icon: , label: 'Pending', cls: 'bg-yellow-50 text-yellow-700 border-yellow-200' }, + }; + const s = config[status] || config.pending; + return ( + + {s.icon} {s.label} + + ); +}; + +const NotificationCard = ({ report, onMarkAsRead, onViewProperty, isOwner }) => ( +
+ {/* Card Header */} +
+
+
+ {report.is_new && New} + +
+

{report.property_title}

+

+ {isOwner ? `Reported by ${report.reporter_name} · ` : ''} + {new Date(report.created_at).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })} +

+
+ {report.is_new && ( + + )} +
+ + {/* Reason */} +
+

{isOwner ? 'Report Reason' : 'Your Report'}

+

{report.reason}

+
+ + {/* Admin / Owner Notice */} + {(report.admin_notice || report.owner_notice) ? ( +
+

+ {isOwner ? '📋 Admin Notice' : '✅ Admin Response'} +

+

{isOwner ? report.owner_notice : (report.admin_notice || report.owner_notice)}

+

{new Date(report.updated_at).toLocaleString('en-IN', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}

+
+ ) : ( +
+ +

Under review — we'll notify you once admin responds.

+
+ )} + + {/* Footer */} +
+ +
+
+); + +export const NotificationsModal = ({ isOpen, onClose, onUnreadCountChange }) => { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState('reports'); + const [reports, setReports] = useState([]); + const [ownerReports, setOwnerReports] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (isOpen) fetchNotifications(); + }, [isOpen]); + + const fetchNotifications = async () => { + try { + setLoading(true); + const [reportsData, ownerData] = await Promise.all([ + notificationService.getMyReports(), + notificationService.getOwnerReports() + ]); + setReports(reportsData.reports); + setOwnerReports(ownerData.reports); + } catch { + setError('Failed to load notifications'); + } finally { + setLoading(false); + } + }; + + const handleMarkAsRead = async (reportId) => { + try { + await notificationService.markAsRead(reportId); + setReports(prev => prev.map(r => r.id === reportId ? { ...r, is_read: true, is_new: false } : r)); + onUnreadCountChange?.(); + } catch {} + }; + + const handleMarkOwnerAsRead = async (reportId) => { + try { + await notificationService.markOwnerAsRead(reportId); + setOwnerReports(prev => prev.map(r => r.id === reportId ? { ...r, owner_is_read: true, is_new: false } : r)); + onUnreadCountChange?.(); + } catch {} + }; + + const handleMarkAllAsRead = async () => { + try { + if (activeTab === 'reports') { + await notificationService.markAllAsRead(); + setReports(prev => prev.map(r => ({ ...r, is_read: true, is_new: false }))); + } else { + await notificationService.markAllOwnerAsRead(); + setOwnerReports(prev => prev.map(r => ({ ...r, owner_is_read: true, is_new: false }))); + } + onUnreadCountChange?.(); + } catch {} + }; + + const handleViewProperty = (propertyId) => { + onClose(); + navigate(`/rooms/${propertyId}`); + }; + + const currentList = activeTab === 'reports' ? reports : ownerReports; + const unreadCount = currentList.filter(r => r.is_new).length; + + if (!isOpen) return null; + + return ( + <> +
+ +
+
e.stopPropagation()}> + + {/* Header */} +
+
+
+ +
+
+

Notifications

+

Report updates & property alerts

+
+
+ +
+ + {/* Tabs */} +
+ {[ + { key: 'reports', label: 'My Reports', icon: FileText, count: reports.filter(r => r.is_new).length }, + { key: 'owner', label: 'My Properties', icon: Home, count: ownerReports.filter(r => r.is_new).length }, + ].map(tab => ( + + ))} +
+ + {/* Mark all read */} + {unreadCount > 0 && ( +
+

{unreadCount} unread notification{unreadCount > 1 ? 's' : ''}

+ +
+ )} + + {/* Content */} +
+ {error && ( +
+ +

{error}

+
+ )} + + {loading ? ( +
+
+

Loading...

+
+ ) : currentList.length === 0 ? ( +
+
+ {activeTab === 'reports' ? : } +
+

No notifications yet

+

+ {activeTab === 'reports' ? "You haven't reported any properties." : "No reports on your properties."} +

+ {activeTab === 'reports' && ( + + )} +
+ ) : ( + currentList.map(report => ( + + )) + )} +
+
+
+ + ); +}; diff --git a/src/components/modal/Toast.jsx b/src/components/modal/Toast.jsx new file mode 100644 index 0000000..e11a28e --- /dev/null +++ b/src/components/modal/Toast.jsx @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; + +export const Toast = ({ message, type = 'success', onClose, duration = 3000 }) => { + useEffect(() => { + const timer = setTimeout(() => { + onClose(); + }, duration); + + return () => clearTimeout(timer); + }, [duration, onClose]); + + const bgColor = type === 'success' ? 'bg-green-500' : type === 'error' ? 'bg-red-500' : 'bg-blue-500'; + + return ( +
+
+

{message}

+ +
+
+ ); +}; diff --git a/src/components/modal/VerificationWarningModal.jsx b/src/components/modal/VerificationWarningModal.jsx new file mode 100644 index 0000000..736a310 --- /dev/null +++ b/src/components/modal/VerificationWarningModal.jsx @@ -0,0 +1,59 @@ +import { useNavigate } from 'react-router-dom'; + +export const VerificationWarningModal = ({ + isOpen, + onClose, + title = "Verification Required", + message = "To access this feature, you need to complete your personal details and verify your identity.", + primaryAction = "Go to Profile", + primaryPath = "/profile", + secondaryAction = "Back", + secondaryPath = null +}) => { + const navigate = useNavigate(); + + if (!isOpen) return null; + + const handlePrimaryAction = () => { + navigate(primaryPath); + }; + + const handleSecondaryAction = () => { + if (secondaryPath) { + navigate(secondaryPath); + } else { + onClose(); + } + }; + + return ( +
+
+
+
+
+ + + +
+

{title}

+

{message}

+
+ + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/Alert.jsx b/src/components/ui/Alert.jsx deleted file mode 100644 index 31013de..0000000 --- a/src/components/ui/Alert.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react'; - -export const Alert = ({ type = 'info', message, onClose }) => { - const types = { - success: { - bg: 'bg-green-50 border-green-200', - text: 'text-green-800', - icon: CheckCircle, - }, - error: { - bg: 'bg-red-50 border-red-200', - text: 'text-red-800', - icon: AlertCircle, - }, - warning: { - bg: 'bg-yellow-50 border-yellow-200', - text: 'text-yellow-800', - icon: AlertTriangle, - }, - info: { - bg: 'bg-blue-50 border-blue-200', - text: 'text-blue-800', - icon: Info, - }, - }; - - const { bg, text, icon: Icon } = types[type]; - - return ( -
- -

{message}

- {onClose && ( - - )} -
- ); -}; diff --git a/src/components/ui/Badge.jsx b/src/components/ui/Badge.jsx deleted file mode 100644 index 84136fa..0000000 --- a/src/components/ui/Badge.jsx +++ /dev/null @@ -1,14 +0,0 @@ -export const Badge = ({ children, variant = 'info', className = '' }) => { - const variants = { - success: 'badge-success', - warning: 'badge-warning', - danger: 'badge-danger', - info: 'badge-info', - }; - - return ( - - {children} - - ); -}; diff --git a/src/components/ui/Button.jsx b/src/components/ui/Button.jsx deleted file mode 100644 index ba694c7..0000000 --- a/src/components/ui/Button.jsx +++ /dev/null @@ -1,32 +0,0 @@ -export const Button = ({ - children, - variant = 'primary', - size = 'md', - className = '', - ...props -}) => { - const baseStyles = 'btn transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; - - const variants = { - primary: 'btn-primary', - secondary: 'btn-secondary', - outline: 'btn-outline', - ghost: 'hover:bg-dark-50', - danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', - }; - - const sizes = { - sm: 'px-3 py-1.5 text-sm', - md: 'px-4 py-2', - lg: 'px-6 py-3 text-lg', - }; - - return ( - - ); -}; diff --git a/src/components/ui/Card.jsx b/src/components/ui/Card.jsx deleted file mode 100644 index 595c755..0000000 --- a/src/components/ui/Card.jsx +++ /dev/null @@ -1,10 +0,0 @@ -export const Card = ({ children, className = '', hover = false, ...props }) => { - return ( -
- {children} -
- ); -}; diff --git a/src/components/ui/EmptyState.jsx b/src/components/ui/EmptyState.jsx deleted file mode 100644 index 13d8cce..0000000 --- a/src/components/ui/EmptyState.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Button } from './Button'; - -export const EmptyState = ({ - icon: Icon, - title, - description, - action, - actionLabel -}) => { - return ( -
- {Icon && ( -
- -
- )} -

- {title} -

-

- {description} -

- {action && actionLabel && ( - - )} -
- ); -}; diff --git a/src/components/ui/Input.jsx b/src/components/ui/Input.jsx deleted file mode 100644 index c958111..0000000 --- a/src/components/ui/Input.jsx +++ /dev/null @@ -1,23 +0,0 @@ -export const Input = ({ - label, - error, - className = '', - ...props -}) => { - return ( -
- {label && ( - - )} - - {error && ( -

{error}

- )} -
- ); -}; diff --git a/src/components/ui/Loader.jsx b/src/components/ui/Loader.jsx deleted file mode 100644 index dfee799..0000000 --- a/src/components/ui/Loader.jsx +++ /dev/null @@ -1,38 +0,0 @@ -export const Loader = ({ size = 'md', fullScreen = false }) => { - const sizes = { - sm: 'w-4 h-4', - md: 'w-8 h-8', - lg: 'w-12 h-12', - }; - - const spinner = ( -
- ); - - if (fullScreen) { - return ( -
- {spinner} -
- ); - } - - return spinner; -}; - -export const SkeletonCard = () => { - return ( -
-
-
-
-
-
-
-
-
-
-
-
- ); -}; diff --git a/src/components/ui/Modal.jsx b/src/components/ui/Modal.jsx deleted file mode 100644 index 22e204b..0000000 --- a/src/components/ui/Modal.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { X } from 'lucide-react'; -import { useEffect } from 'react'; - -export const Modal = ({ isOpen, onClose, title, children, size = 'md' }) => { - useEffect(() => { - if (isOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'unset'; - } - return () => { - document.body.style.overflow = 'unset'; - }; - }, [isOpen]); - - if (!isOpen) return null; - - const sizes = { - sm: 'max-w-md', - md: 'max-w-2xl', - lg: 'max-w-4xl', - xl: 'max-w-6xl', - }; - - return ( -
-
-
-

{title}

- -
-
- {children} -
-
-
- ); -}; diff --git a/src/constants/amenities.js b/src/constants/amenities.js new file mode 100644 index 0000000..3f78732 --- /dev/null +++ b/src/constants/amenities.js @@ -0,0 +1,14 @@ +export const AMENITIES_LIST = [ + 'wifi', + 'ac', + 'parking', + 'security', + 'gym', + 'swimming_pool', + 'power_backup', + 'elevator', + 'meals', + 'laundry', + 'water_supply', + 'playground', +]; \ No newline at end of file diff --git a/src/constants/cities.js b/src/constants/cities.js new file mode 100644 index 0000000..c2e1926 --- /dev/null +++ b/src/constants/cities.js @@ -0,0 +1,13 @@ +export const GUJARAT_CITIES = [ + 'Ahmedabad', + 'Surat', + 'Vadodara', + 'Rajkot', + 'Navsari', + 'Junagadh', + 'Gandhinagar', + 'Anand', + 'Nadiad', + 'Morbi', + 'Bharuch', +]; diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index 4fe946f..34d4ebc 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -1,4 +1,6 @@ import { createContext, useContext, useState, useEffect } from 'react'; +import { authService } from '../services/authService'; +import { userService } from '../services/userService'; const AuthContext = createContext(null); @@ -15,51 +17,111 @@ export const AuthProvider = ({ children }) => { const [loading, setLoading] = useState(true); useEffect(() => { - // Check if user is logged in on mount + // Check if user is logged in from localStorage const storedUser = localStorage.getItem('roomatex_user'); - if (storedUser) { + const token = localStorage.getItem('access_token'); + + if (storedUser && token) { setUser(JSON.parse(storedUser)); + // Optionally fetch fresh user data + refreshUser(); } setLoading(false); }, []); - const login = (userData) => { - setUser(userData); - localStorage.setItem('roomatex_user', JSON.stringify(userData)); + const refreshUser = async () => { + try { + const userData = await userService.getCurrentUser(); + setUser(userData); + localStorage.setItem('roomatex_user', JSON.stringify(userData)); + } catch (error) { + // If token is invalid, clear storage + if (error.response?.status === 401) { + logout(); + } + } }; - const signup = (userData) => { - const newUser = { - ...userData, - id: Date.now().toString(), - verified: false, - trustScore: 0, - createdAt: new Date().toISOString(), - }; - setUser(newUser); - localStorage.setItem('roomatex_user', JSON.stringify(newUser)); - return newUser; + const login = async (credentials) => { + try { + const { user: userData } = await authService.login(credentials); + setUser(userData); + return { success: true, user: userData }; + } catch (error) { + const message = error.response?.data?.detail || 'Login failed'; + return { success: false, error: message }; + } + }; + + const signup = async (userData) => { + try { + const response = await authService.register(userData); + // After registration, automatically login + const loginResult = await login({ + email: userData.email, + password: userData.password, + }); + return loginResult; + } catch (error) { + const message = error.response?.data?.detail || 'Registration failed'; + return { success: false, error: message }; + } }; const logout = () => { + authService.logout(); setUser(null); - localStorage.removeItem('roomatex_user'); }; - const updateUser = (updates) => { - const updatedUser = { ...user, ...updates }; - setUser(updatedUser); - localStorage.setItem('roomatex_user', JSON.stringify(updatedUser)); + const updateUser = async (updates) => { + try { + const updatedUser = await userService.updateProfile(updates); + setUser(updatedUser); + return { success: true, user: updatedUser }; + } catch (error) { + const message = error.response?.data?.detail || 'Update failed'; + return { success: false, error: message }; + } + }; + + const sendOTP = async (purpose = 'verification') => { + try { + await authService.sendOTP(purpose); + return { success: true }; + } catch (error) { + const message = error.response?.data?.detail || 'Failed to send OTP'; + return { success: false, error: message }; + } + }; + + const verifyOTP = async (otp, purpose = 'verification') => { + try { + await authService.verifyOTP(otp, purpose); + // Refresh user data to get updated verification status + if (purpose === 'verification') { + await refreshUser(); + } + return { success: true }; + } catch (error) { + const message = error.response?.data?.detail || 'OTP verification failed'; + return { success: false, error: message }; + } }; const value = { user, - loading, + setUser, login, signup, logout, updateUser, + sendOTP, + verifyOTP, + refreshUser, + loading, isAuthenticated: !!user, + isAdmin: user?.role === 'admin', + isVerified: user?.is_verified || false, }; return {children}; diff --git a/src/context/ThemeContext.jsx b/src/context/ThemeContext.jsx new file mode 100644 index 0000000..7cfab26 --- /dev/null +++ b/src/context/ThemeContext.jsx @@ -0,0 +1,43 @@ +import { createContext, useContext, useState } from 'react'; + +const ThemeContext = createContext(); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +export const ThemeProvider = ({ children }) => { + const [theme, setTheme] = useState({ + navbarBg: 'bg-white', + navbarText: 'text-gray-700', + navbarBorder: 'border-gray-200', + buttonBg: 'bg-primary-600', + buttonHover: 'hover:bg-primary-700', + buttonText: 'text-white', + }); + + const updateTheme = (newTheme) => { + setTheme((prev) => ({ ...prev, ...newTheme })); + }; + + const resetTheme = () => { + setTheme({ + navbarBg: 'bg-white', + navbarText: 'text-gray-700', + navbarBorder: 'border-gray-200', + buttonBg: 'bg-primary-600', + buttonHover: 'hover:bg-primary-700', + buttonText: 'text-white', + }); + }; + + return ( + + {children} + + ); +}; diff --git a/src/context/ToastContext.jsx b/src/context/ToastContext.jsx new file mode 100644 index 0000000..b4dda64 --- /dev/null +++ b/src/context/ToastContext.jsx @@ -0,0 +1,55 @@ +import { createContext, useContext, useState } from 'react'; +import { Toast } from '../components/modal/Toast'; + +const ToastContext = createContext(); + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +}; + +export const ToastProvider = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const showToast = (message, type = 'success', duration = 3000) => { + const id = Date.now(); + const toast = { id, message, type, duration }; + + setToasts(prev => [...prev, toast]); + + // Auto remove after duration + setTimeout(() => { + removeToast(id); + }, duration); + }; + + const removeToast = (id) => { + setToasts(prev => prev.filter(toast => toast.id !== id)); + }; + + const showSuccess = (message, duration) => showToast(message, 'success', duration); + const showError = (message, duration) => showToast(message, 'error', duration); + const showInfo = (message, duration) => showToast(message, 'info', duration); + + return ( + + {children} + + {/* Render toasts */} +
+ {toasts.map(toast => ( + removeToast(toast.id)} + /> + ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/data/admin.js b/src/data/admin.js deleted file mode 100644 index d7b4140..0000000 --- a/src/data/admin.js +++ /dev/null @@ -1,61 +0,0 @@ -export const reports = [ - { - id: 'rp1', - type: 'room', - targetId: '5', - targetName: 'Budget-Friendly Room for Students', - reporterId: 'u10', - reporterName: 'Sanjay Kumar', - reason: 'Misleading information', - description: 'The photos shown are not of the actual room. The amenities listed are also incorrect.', - status: 'pending', - date: '2025-01-26', - severity: 'high', - }, - { - id: 'rp2', - type: 'user', - targetId: '107', - targetName: 'Unknown User', - reporterId: 'u11', - reporterName: 'Priya Menon', - reason: 'Suspicious behavior', - description: 'This user asked for personal details before showing the room.', - status: 'pending', - date: '2025-01-25', - severity: 'critical', - }, - { - id: 'rp3', - type: 'room', - targetId: '2', - targetName: 'Shared 2BHK near Tech Park', - reporterId: 'u12', - reporterName: 'Ankit Verma', - reason: 'False availability', - description: 'Room was already rented but still showing as available.', - status: 'resolved', - date: '2025-01-20', - severity: 'medium', - }, -]; - -export const adminStats = { - totalUsers: 2847, - totalRooms: 1563, - activeListings: 1205, - pendingReports: 12, - verifiedUsers: 1892, - thisMonthSignups: 234, - thisMonthListings: 156, - avgTrustScore: 4.6, -}; - -export const cityStats = [ - { city: 'Mumbai', users: 652, rooms: 384 }, - { city: 'Bangalore', users: 734, rooms: 425 }, - { city: 'Delhi', users: 489, rooms: 287 }, - { city: 'Pune', users: 321, rooms: 198 }, - { city: 'Hyderabad', users: 298, rooms: 156 }, - { city: 'Chennai', users: 253, rooms: 113 }, -]; diff --git a/src/data/reviews.js b/src/data/reviews.js deleted file mode 100644 index c27d43d..0000000 --- a/src/data/reviews.js +++ /dev/null @@ -1,70 +0,0 @@ -export const reviews = [ - { - id: 'rev1', - roomId: '1', - userId: 'u1', - userName: 'Vikram Shah', - userImage: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400', - rating: 5, - comment: 'Excellent accommodation! The owner is very cooperative and the locality is great. Highly recommended.', - date: '2025-01-15', - helpful: 8, - }, - { - id: 'rev2', - roomId: '1', - userId: 'u2', - userName: 'Sneha Desai', - userImage: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400', - rating: 4, - comment: 'Good place to stay. Well maintained and clean. Slightly expensive but worth it.', - date: '2025-01-10', - helpful: 5, - }, - { - id: 'rev3', - roomId: '3', - userId: 'u3', - userName: 'Anjali Nair', - userImage: 'https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?w=400', - rating: 5, - comment: 'Best PG experience ever! Home-cooked food is delicious and the environment is very safe.', - date: '2025-01-12', - helpful: 12, - }, - { - id: 'rev4', - roomId: '2', - userId: 'u4', - userName: 'Rohan Kapoor', - userImage: 'https://images.unsplash.com/photo-1463453091185-61582044d556?w=400', - rating: 4, - comment: 'Great location near office. Roommates are friendly and cooperative.', - date: '2025-01-08', - helpful: 6, - }, -]; - -export const userReviews = { - given: [ - { - id: 'ur1', - type: 'room', - targetName: 'Cozy Studio in Downtown', - rating: 5, - comment: 'Excellent accommodation! The owner is very cooperative.', - date: '2025-01-15', - }, - ], - received: [ - { - id: 'ur2', - type: 'user', - reviewerName: 'Amit Sharma', - reviewerImage: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400', - rating: 5, - comment: 'Very responsible tenant. Would recommend!', - date: '2025-01-10', - }, - ], -}; diff --git a/src/data/rooms.js b/src/data/rooms.js deleted file mode 100644 index 7b8fcde..0000000 --- a/src/data/rooms.js +++ /dev/null @@ -1,224 +0,0 @@ -export const rooms = [ - { - id: '1', - title: 'Cozy Studio in Downtown', - description: 'Modern studio apartment with all amenities. Perfect for students and young professionals. Walking distance to metro station and shopping centers.', - rent: 12000, - city: 'Mumbai', - area: 'Bandra West', - gender: 'Any', - availability: 'Available', - roomType: 'Private Room', - furnished: true, - amenities: ['WiFi', 'AC', 'Kitchen', 'Parking', 'Security'], - rules: ['No smoking', 'No pets', 'Guest policy: with permission'], - images: [ - 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800', - 'https://images.unsplash.com/photo-1502672260066-6bc2696d1d8e?w=800', - 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?w=800' - ], - ownerId: '101', - ownerName: 'Priya Sharma', - ownerVerified: true, - ownerRating: 4.8, - reviews: 12, - postedDate: '2025-01-20', - preferredAge: '21-30', - occupants: 1, - maxOccupants: 2, - deposit: 24000, - featured: true, - }, - { - id: '2', - title: 'Shared 2BHK near Tech Park', - description: 'Spacious 2BHK apartment with one room available. Great for IT professionals. Close to major tech parks and cafes.', - rent: 15000, - city: 'Bangalore', - area: 'Koramangala', - gender: 'Male', - availability: 'Available from Feb 1', - roomType: 'Shared Room', - furnished: true, - amenities: ['WiFi', 'AC', 'Washing Machine', 'Gym', 'Power Backup'], - rules: ['No smoking', 'No drinking', 'Maintain cleanliness'], - images: [ - 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=800', - 'https://images.unsplash.com/photo-1556909212-d5b604d0c90d?w=800' - ], - ownerId: '102', - ownerName: 'Rahul Patel', - ownerVerified: true, - ownerRating: 4.5, - reviews: 8, - postedDate: '2025-01-22', - preferredAge: '22-35', - occupants: 2, - maxOccupants: 3, - deposit: 30000, - featured: false, - }, - { - id: '3', - title: 'PG with Home-cooked Meals', - description: 'Clean and comfortable PG accommodation with home-cooked vegetarian meals included. Family-like environment.', - rent: 9000, - city: 'Pune', - area: 'Kothrud', - gender: 'Female', - availability: 'Available', - roomType: 'Shared Room', - furnished: true, - amenities: ['WiFi', 'Meals', 'Laundry', 'Hot Water', 'Security'], - rules: ['No non-veg', 'No guests after 9 PM', 'No smoking/drinking'], - images: [ - 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800', - 'https://images.unsplash.com/photo-1560449752-8b4d462d2873?w=800' - ], - ownerId: '103', - ownerName: 'Lakshmi Iyer', - ownerVerified: true, - ownerRating: 4.9, - reviews: 24, - postedDate: '2025-01-15', - preferredAge: '18-28', - occupants: 3, - maxOccupants: 4, - deposit: 9000, - featured: true, - }, - { - id: '4', - title: 'Luxury 3BHK Apartment', - description: 'Premium apartment with modern interiors and scenic views. Perfect for executives. Swimming pool and gym facilities.', - rent: 25000, - city: 'Delhi', - area: 'South Delhi', - gender: 'Any', - availability: 'Available', - roomType: 'Private Room', - furnished: true, - amenities: ['WiFi', 'AC', 'Gym', 'Swimming Pool', 'Security', 'Parking'], - rules: ['Professional tenants only', 'No pets'], - images: [ - 'https://images.unsplash.com/photo-1502672023488-70e25813eb80?w=800', - 'https://images.unsplash.com/photo-1493809842364-78817add7ffb?w=800' - ], - ownerId: '104', - ownerName: 'Amit Khanna', - ownerVerified: true, - ownerRating: 4.7, - reviews: 15, - postedDate: '2025-01-18', - preferredAge: '25-40', - occupants: 1, - maxOccupants: 2, - deposit: 50000, - featured: true, - }, - { - id: '5', - title: 'Budget-Friendly Room for Students', - description: 'Affordable accommodation near university campus. Perfect for students. Peaceful study environment.', - rent: 6000, - city: 'Chennai', - area: 'Guindy', - gender: 'Any', - availability: 'Available', - roomType: 'Shared Room', - furnished: true, - amenities: ['WiFi', 'Study Table', 'Hot Water', 'Security'], - rules: ['Maintain silence after 10 PM', 'No smoking'], - images: [ - 'https://images.unsplash.com/photo-1555854877-bab0e564b8d5?w=800' - ], - ownerId: '105', - ownerName: 'Suresh Kumar', - ownerVerified: false, - ownerRating: 4.3, - reviews: 6, - postedDate: '2025-01-25', - preferredAge: '18-25', - occupants: 3, - maxOccupants: 4, - deposit: 6000, - featured: false, - }, - { - id: '6', - title: 'Modern 1BHK in IT Hub', - description: 'Newly renovated 1BHK apartment in the heart of IT corridor. All modern amenities and 24/7 security.', - rent: 18000, - city: 'Hyderabad', - area: 'Gachibowli', - gender: 'Any', - availability: 'Available from Feb 15', - roomType: 'Private Room', - furnished: true, - amenities: ['WiFi', 'AC', 'Modular Kitchen', 'Parking', 'Power Backup'], - rules: ['Professional tenants preferred', 'No loud music'], - images: [ - 'https://images.unsplash.com/photo-1522771739844-6a9f6d5f14af?w=800', - 'https://images.unsplash.com/photo-1540518614846-7eded433c457?w=800' - ], - ownerId: '106', - ownerName: 'Divya Reddy', - ownerVerified: true, - ownerRating: 4.6, - reviews: 10, - postedDate: '2025-01-19', - preferredAge: '23-35', - occupants: 0, - maxOccupants: 2, - deposit: 36000, - featured: false, - }, -]; - -export const roommates = [ - { - id: 'rm1', - name: 'Arjun Singh', - age: 24, - gender: 'Male', - occupation: 'Software Engineer', - city: 'Bangalore', - lookingFor: 'Shared 2BHK', - budget: '10000-15000', - preferences: 'Non-smoker, working professional', - verified: true, - rating: 4.7, - image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400', - about: 'Working professional looking for clean and peaceful accommodation.', - }, - { - id: 'rm2', - name: 'Neha Gupta', - age: 22, - gender: 'Female', - occupation: 'Student', - city: 'Delhi', - lookingFor: 'PG or Shared Room', - budget: '8000-12000', - preferences: 'Female roommates only, vegetarian', - verified: true, - rating: 4.9, - image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400', - about: 'MBA student seeking accommodation near campus.', - }, - { - id: 'rm3', - name: 'Karthik Menon', - age: 28, - gender: 'Male', - occupation: 'Designer', - city: 'Mumbai', - lookingFor: 'Private Room', - budget: '15000-20000', - preferences: 'Near metro, quiet area', - verified: false, - rating: 4.5, - image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400', - about: 'Freelance designer looking for a creative space.', - }, -]; diff --git a/src/layouts/MainLayout.jsx b/src/layouts/MainLayout.jsx deleted file mode 100644 index 754d942..0000000 --- a/src/layouts/MainLayout.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Navbar } from './Navbar'; - -export const MainLayout = ({ children }) => { - return ( -
- -
- {children} -
-
- ); -}; diff --git a/src/layouts/Navbar.jsx b/src/layouts/Navbar.jsx deleted file mode 100644 index 46cc132..0000000 --- a/src/layouts/Navbar.jsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; -import { Home, Search, Info, Star, User, LogOut, Menu, X, Shield } from 'lucide-react'; -import { useState } from 'react'; -import { Button } from '../components/ui/Button'; -// import {logo} from "../assets/logo.png"; - -export const Navbar = () => { - const { user, logout } = useAuth(); - const navigate = useNavigate(); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - - const handleLogout = () => { - logout(); - navigate('/login'); - }; - - const getNavItems = () => { - if (user?.role === 'admin') { - return [ - { to: '/dashboard', label: 'Dashboard', icon: Home }, - { to: '/admin/users', label: 'Users', icon: User }, - { to: '/admin/rooms', label: 'Rooms', icon: Search }, - { to: '/admin/reports', label: 'Reports', icon: Shield }, - ]; - } - - if (user?.role === 'owner') { - return [ - { to: '/dashboard', label: 'Home', icon: Home }, - { to: '/add-room', label: 'List Room', icon: Search }, - { to: '/info', label: 'Info', icon: Info }, - { to: '/reviews', label: 'Reviews', icon: Star }, - ]; - } - - // seeker - return [ - { to: '/dashboard', label: 'Home', icon: Home }, - { to: '/rooms', label: 'Find Room', icon: Search }, - { to: '/info', label: 'Info', icon: Info }, - { to: '/reviews', label: 'Reviews', icon: Star }, - ]; - }; - - const navItems = getNavItems(); - - return ( - - ); -}; diff --git a/src/main.jsx b/src/main.jsx index 32fcc3f..2be1bec 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.jsx'; import './styles/index.css'; ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/src/pages/AddRoom.jsx b/src/pages/AddRoom.jsx deleted file mode 100644 index d68329f..0000000 --- a/src/pages/AddRoom.jsx +++ /dev/null @@ -1,244 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Button } from '../components/ui/Button'; -import { Input } from '../components/ui/Input'; -import { Alert } from '../components/ui/Alert'; -import { Upload, X } from 'lucide-react'; - -export const AddRoom = () => { - const navigate = useNavigate(); - const [alert, setAlert] = useState(null); - const [images, setImages] = useState([]); - const [formData, setFormData] = useState({ - title: '', - description: '', - rent: '', - city: '', - area: '', - gender: 'Any', - roomType: 'Private Room', - furnished: true, - amenities: [], - rules: '', - deposit: '', - preferredAge: '', - }); - - const amenitiesList = ['WiFi', 'AC', 'Kitchen', 'Parking', 'Security', 'Washing Machine', 'Gym', 'Power Backup']; - - const handleSubmit = (e) => { - e.preventDefault(); - if (images.length < 3) { - setAlert({ type: 'error', message: 'Please upload at least 3 photos' }); - return; - } - setAlert({ type: 'success', message: 'Room listed successfully!' }); - setTimeout(() => navigate('/dashboard'), 1500); - }; - - const handleImageUpload = (e) => { - const files = Array.from(e.target.files); - if (images.length + files.length > 10) { - setAlert({ type: 'error', message: 'Maximum 10 images allowed' }); - return; - } - setImages([...images, ...files.map(file => URL.createObjectURL(file))]); - }; - - const toggleAmenity = (amenity) => { - setFormData({ - ...formData, - amenities: formData.amenities.includes(amenity) - ? formData.amenities.filter(a => a !== amenity) - : [...formData.amenities, amenity], - }); - }; - - return ( - -
-
-

List Your Room

-

Fill in the details to create your listing

-
- - {alert && setAlert(null)} />} - -
- -

Basic Information

-
- setFormData({ ...formData, title: e.target.value })} - /> -
- - +
+
+ + +
+
+ )} + + {step === 4 && ( +
+
+

Review your listing

+
+
+ Property Title + {formData.title} +
+
+ Rent + ₹{formData.rent}/mo +
+
+ Location + {formData.area}, {formData.city} +
+
+ Amenities + {formData.amenities.length} Selected +
+
+
+
+ )} + +
+ +
+ {step > 1 ? ( + + ) : ( +
+ )} + + {step < 4 ? ( + + ) : ( + + )} +
+ +
+
+
+
+
+ + {/* Verification Warning Modal */} + setShowVerificationWarning(false)} + message="To list a room, you need to complete your personal details and verify your identity." + secondaryAction="Back to Dashboard" + secondaryPath="/dashboard" + /> +
+ ); +}; diff --git a/src/pages/CompleteProfilePage.jsx b/src/pages/CompleteProfilePage.jsx new file mode 100644 index 0000000..f7a1597 --- /dev/null +++ b/src/pages/CompleteProfilePage.jsx @@ -0,0 +1,12 @@ +import { Navbar } from '../components/Navbar'; + +export const CompleteProfilePage = () => { + return ( +
+ +
+

Complete Profile - Coming Soon

+
+
+ ); +}; diff --git a/src/pages/ContactPage.jsx b/src/pages/ContactPage.jsx new file mode 100644 index 0000000..5dfa0be --- /dev/null +++ b/src/pages/ContactPage.jsx @@ -0,0 +1,401 @@ +import { useState } from 'react'; +import { Navbar } from '../components/Navbar'; +import api from '../services/api'; + +export const ContactPage = () => { + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '' + }); + const [status, setStatus] = useState('idle'); + const [openFaq, setOpenFaq] = useState(null); + + const faqs = [ + { + question: 'How do I list my room on RoomMateX?', + answer: 'Simply sign up, verify your account, and click on "List Your Room" from the dashboard. Fill in the property details, upload photos, and your listing will be live!' + }, + { + question: 'Is RoomMateX free to use?', + answer: 'Yes! Creating an account, browsing rooms, and contacting owners is completely free. We believe in making housing accessible to everyone.' + }, + { + question: 'How does the verification process work?', + answer: 'After signing up, you\'ll receive an OTP via email. Enter the code to verify your account. Verified users get a blue badge and can list properties.' + }, + { + question: 'How do I contact room owners?', + answer: 'You can contact property owners directly using the contact information provided in the property listing. All contact details are visible to verified users.' + }, + { + question: 'How do I know if a listing is genuine?', + answer: 'All listings are from verified users. Look for the verified badge, check reviews, and always visit the property before making any payment.' + }, + { + question: 'What if I face issues with a listing?', + answer: 'You can report any listing or user through the report button. Our team reviews all reports and takes appropriate action within 24 hours.' + } + ]; + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setStatus('submitting'); + + try { + const response = await api.post('/contact/send', formData); + + if (response.data.success) { + setStatus('success'); + setFormData({ name: '', email: '', subject: '', message: '' }); + } + } catch (error) { + setStatus('error'); + setTimeout(() => setStatus('idle'), 3000); + } + }; + + return ( +
+ +
+
+ + {/* FAQ Section */} +
+
+

Frequently Asked Questions

+

Find quick answers to common questions

+
+ +
+ {faqs.map((faq, index) => ( +
+ + {openFaq === index && ( +
+ {faq.answer} +
+ )} +
+ ))} +
+
+ + {/* Scroll Down Nudge */} +
+

Confused?? Scroll down

+ + + +
+ + {/* Contact Section */} +
+
+

Still Have Questions?

+

+ We're here to help! Choose your preferred way to reach us - fill out the form below for a quick response, + or contact us directly using the information provided. +

+
+ + {/* Instructions Banner */} +
+
+
+
+ + + +
+
+

How to Contact Us

+
+
+ 1. +

Quick Response: Fill out the contact form below and we'll get back to you within 24 hours.

+
+
+ 2. +

Direct Contact: Email us directly at roommatex0help@gmail.com or call us at +91 82002 56660.

+
+
+ 3. +

Business Hours: We're available Monday to Saturday, 9:00 AM - 6:00 PM IST. Messages received outside business hours will be responded to on the next working day.

+
+
+
+
+
+
+ +
+ {/* Contact Info */} +
+
+
+

Contact Information

+

Reach us directly

+
+ + +
+ +
+
+

Email us

+

roommatex0help@gmail.com

+
+
+ + +
+ +
+
+

Call us

+

+91 82002 56660

+
+
+ +
+
+ +
+
+

Visit us

+

Ahmedabad, Gujarat

+
+
+ +
+
+ +
+
+

Working Hours

+

Mon-Sat: 9AM-6PM

+
+
+
+ + {/* Quick Tips */} +
+
+
+ + + +
+
+

Quick Tip

+

For faster response, use the contact form. We'll reply within 24 hours!

+
+
+
+
+ + {/* Contact Form */} +
+
+
+

Send Us a Message

+

+ Fill out the form below and we'll get back to you within 24 hours. All fields are required. +

+
+ + {status === 'success' ? ( +
+
+ +
+

Message Sent Successfully!

+

Thank you for contacting us, {formData.name || 'there'}!

+

We've received your message and will respond to {formData.email || 'your email'} within 24 hours.

+
+

+ What's next?
+ • Check your email inbox (and spam folder)
+ • We'll reply from roommatex0help@gmail.com
+ • Response time: Within 24 hours +

+
+ +
+ ) : status === 'error' ? ( +
+
+ +
+

Failed to Send Message

+

Something went wrong. Please try again or contact us directly via email.

+
+ + + Email Directly + +
+
+ ) : ( +
+
+
+ + +
+
+ + +

+ + + + Please enter a valid email address - we'll send our reply to this email +

+
+
+
+ + +
+
+ + +
+ + + {/* Form Footer Note */} +
+

+ By submitting this form, you agree to receive a response from RoomMateX via email. +

+
+ + {/* Alternative Contact */} +
+

+ Prefer to contact us directly? +

+ +
+
+ )} +
+
+
+
+
+
+
+ ); +}; diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx deleted file mode 100644 index 49ef7ff..0000000 --- a/src/pages/Dashboard.jsx +++ /dev/null @@ -1,343 +0,0 @@ -import { useAuth } from '../context/AuthContext'; -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Button } from '../components/ui/Button'; -import { Link } from 'react-router-dom'; -import { rooms, roommates } from '../data/rooms'; -import { MapPin, IndianRupee, Users, Home, Star, TrendingUp, AlertCircle } from 'lucide-react'; -import { Badge } from '../components/ui/Badge'; -import { adminStats } from '../data/admin'; - -export const Dashboard = () => { - const { user } = useAuth(); - - const renderSeekerDashboard = () => ( -
-
-

- Welcome back, {user.name}! -

-

- Discover your perfect room today -

-
- - {/* Quick Stats */} -
- -
-
-

Available Rooms

-

1,205

-
-
- -
-
-
- -
-
-

Wishlist

-

0

-
-
- -
-
-
- -
-
-

Messages

-

0

-
-
- -
-
-
- -
-
-

Profile Views

-

12

-
-
- -
-
-
-
- - {/* Recommended Rooms */} -
-
-

Recommended for You

- - - -
-
- {rooms.slice(0, 6).map((room) => ( - - -
- {room.title} - {room.featured && ( - - Featured - - )} - {room.ownerVerified && ( - - ✓ Verified - - )} -
-
-

- {room.title} -

-
- - {room.area}, {room.city} -
-
-
- ₹{room.rent.toLocaleString()} - /mo -
-
- - {room.ownerRating} -
-
-
-
- - ))} -
-
-
- ); - - const renderOwnerDashboard = () => ( -
-
-

- Welcome back, {user.name}! -

-

- Manage your listings and find great roommates -

-
- - {/* Quick Stats */} -
- -
-
-

My Listings

-

0

-
-
- -
-
-
- -
-
-

Total Views

-

0

-
-
- -
-
-
- -
-
-

Inquiries

-

0

-
-
- -
-
-
- -
-
-

Rating

-

4.8

-
-
- -
-
-
-
- - {/* Quick Actions */} - -
-
- -
-
-

- List Your First Room -

-

- Start finding great roommates by listing your available room today. -

- - - -
-
-
- - {/* Potential Roommates */} -
-
-

Potential Roommates

-
-
- {roommates.map((roommate) => ( - -
- {roommate.name} -
-
-

{roommate.name}

- {roommate.verified && ( - - )} -
-

{roommate.occupation}

-
- - {roommate.rating} -
-
-
-

{roommate.about}

-
- Budget: {roommate.budget} - -
-
- ))} -
-
-
- ); - - const renderAdminDashboard = () => ( -
-
-

- Admin Dashboard -

-

- Platform overview and management -

-
- - {/* Stats Grid */} -
- -
-
-

Total Users

-

{adminStats.totalUsers.toLocaleString()}

-

+{adminStats.thisMonthSignups} this month

-
-
- -
-
-
- -
-
-

Total Rooms

-

{adminStats.totalRooms.toLocaleString()}

-

+{adminStats.thisMonthListings} this month

-
-
- -
-
-
- -
-
-

Active Listings

-

{adminStats.activeListings.toLocaleString()}

-
-
- -
-
-
- -
-
-

Pending Reports

-

{adminStats.pendingReports}

-
-
- -
-
-
-
- - {/* Quick Actions */} -
- - - -

Manage Users

-

View and manage user accounts

-
- - - - -

Manage Rooms

-

Review and moderate listings

-
- - - - -

View Reports

-

Handle reported content

-
- -
-
- ); - - return ( - - {user?.role === 'admin' && renderAdminDashboard()} - {user?.role === 'owner' && renderOwnerDashboard()} - {user?.role === 'seeker' && renderSeekerDashboard()} - - ); -}; diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx new file mode 100644 index 0000000..36adc7b --- /dev/null +++ b/src/pages/DashboardPage.jsx @@ -0,0 +1,385 @@ +import { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { useToast } from '../context/ToastContext'; +import { Navbar } from '../components/Navbar'; +import { ConfirmModal } from '../components/modal/ConfirmModal'; +import { dashboardService } from '../services/dashboardService'; +import { propertyService } from '../services/propertyService'; +import { getImageUrl } from '../utils/imageUtils'; + +export const DashboardPage = () => { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const { showSuccess, showError } = useToast(); + + const [loading, setLoading] = useState(true); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [roomToDelete, setRoomToDelete] = useState(null); + const [summary, setSummary] = useState({ + my_listings_count: 0, + wishlist_count: 0, + total_rooms_count: 0 + }); + const [myListings, setMyListings] = useState([]); + const [wishlistRooms, setWishlistRooms] = useState([]); + const [recommendedRooms, setRecommendedRooms] = useState([]); + + useEffect(() => { + if (user) { + fetchDashboardData(); + } + }, [user]); + + const fetchDashboardData = async () => { + setLoading(true); + try { + const [summaryData, listingsData, wishlistData, recommendedData, totalRoomsCount] = await Promise.all([ + dashboardService.getSummary(), + dashboardService.getMyListings(), + dashboardService.getWishlist(), + dashboardService.getRecommended(), + dashboardService.getTotalRoomsCount() + ]); + + setSummary({ ...summaryData, total_rooms_count: totalRoomsCount }); + setMyListings(listingsData); + setWishlistRooms(wishlistData); + setRecommendedRooms(recommendedData); + } catch (err) { + // Silently handle dashboard data fetch errors + } finally { + setLoading(false); + } + }; + + const handleLogout = () => { + logout(); + navigate('/'); + }; + + const handleDeleteRoom = async (id) => { + setRoomToDelete(id); + setShowDeleteConfirm(true); + }; + + const confirmDeleteRoom = async () => { + if (!roomToDelete) return; + + try { + await propertyService.deleteProperty(roomToDelete); + setMyListings(prev => prev.filter(room => room.id !== roomToDelete)); + // Refresh summary + const summaryData = await dashboardService.getSummary(); + setSummary(summaryData); + showSuccess('Property deleted successfully'); + } catch (err) { + showError('Failed to delete property. Please try again.'); + } finally { + setShowDeleteConfirm(false); + setRoomToDelete(null); + } + }; + + const handleToggleActive = async (id, currentStatus) => { + try { + const response = await propertyService.togglePropertyActive(id); + // Update the listing in state + setMyListings(prev => prev.map(room => + room.id === id ? { ...room, is_active: response.is_active } : room + )); + showSuccess(response.message); + } catch (err) { + showError('Failed to update property status. Please try again.'); + } + }; + + return ( +
+ + +
+ + {/* Greeting */} +
+
+
+

+ Welcome back, {user?.full_name || 'User'}! + {/* */} +

+

Find your perfect room or roommate today

+
+
+ + {/* Display Sections (Stats) */} +
+
+
+

My Listings

+

+ {loading ? '...' : summary.my_listings_count} +

+
+
+ +
+
+ +
+
+

Wishlist

+

{loading ? '...' : summary.wishlist_count}

+
+
+ +
+
+ +
+
+

Total Rooms Available

+

{loading ? '...' : summary.total_rooms_count}

+
+
+ +
+
+
+ + {/* Two Big Cards: Quick Actions */} +
+ +
+
+ +
+

List Your Room

+

Find the perfect tenant for your property. Quick and easy listing process.

+ + Start Listing + +
+
+ + + +
+
+ +
+

Find Rooms

+

Explore available rooms in your city with advanced filters and verified listings.

+ + Browse Available Rooms + +
+
+ +
+ + {/* My Listings Section */} +
+

+ My Listings {loading ? '...' : myListings.length} +

+ + {loading ? ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+ ) : myListings.length > 0 ? ( +
+ {myListings.map((room) => ( +
+
+
+ {room.property_title} + {/* Status Badge */} +
+ {room.is_active ? 'Active' : 'Inactive'} +
+
+ +
+
+

{room.property_title}

+

+ + {room.city} +

+
+ +
+
+ ₹{room.monthly_rent.toLocaleString()}/mo +
+
+ + View + + + Edit + + +
+
+
+
+ ))} +
+ ) : ( +
+
+ +
+

No listings yet

+

Share your property with thousands of potential tenants.

+ + Create Listing + +
+ )} +
+ + {/* Wishlist Section */} + {wishlistRooms.length > 0 && ( +
+
+

+ My Wishlist {wishlistRooms.length} +

+ + View All + +
+ +
+ {wishlistRooms.map((room) => ( + +
+
+ {room.property_title} +
+ +
+
+ +
+
+

{room.property_title}

+

+ + {room.city} +

+
+ +
+
+ ₹{room.monthly_rent.toLocaleString()}/mo +
+ + View Details + +
+
+ + ))} +
+
+ )} + + {/* Recommended Rooms */} + {!loading && recommendedRooms.length > 0 && ( +
+
+

Recommended For You

+
+ +
+ {recommendedRooms.slice(0, 6).map((room) => ( + +
+
+ {room.property_title} +
+ New +
+
+ +
+
+

{room.property_title}

+

+ + {room.city} +

+
+ +
+
+ ₹{room.monthly_rent.toLocaleString()}/mo +
+ + View + +
+
+ + ))} +
+ +
+ + Explore More Rooms + + +
+
+ )} + +
+ + {/* Delete Confirmation Modal */} + setShowDeleteConfirm(false)} + onConfirm={confirmDeleteRoom} + title="Delete Property" + message="Are you sure you want to delete this property? This action cannot be undone." + confirmText="Delete" + cancelText="Cancel" + type="danger" + /> +
+ ); +}; diff --git a/src/pages/EditRoomPage.jsx b/src/pages/EditRoomPage.jsx new file mode 100644 index 0000000..117a114 --- /dev/null +++ b/src/pages/EditRoomPage.jsx @@ -0,0 +1,646 @@ +import { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Navbar } from '../components/Navbar'; +import { useAuth } from '../context/AuthContext'; +import { useToast } from '../context/ToastContext'; +import { GUJARAT_CITIES } from '../constants/cities'; +import { propertyService } from '../services/propertyService'; +import { LoadingSpinner } from '../components/LoadingSpinner'; +import { getImageUrl } from '../utils/imageUtils'; + +export const EditRoomPage = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const { user } = useAuth(); + const { showSuccess, showError } = useToast(); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [formData, setFormData] = useState({ + title: '', + propertyType: 'apartment', + city: '', + area: '', + rent: '', + deposit: '', + availableFrom: '', + genderPreference: 'any', + amenities: [], + rules: '', + description: '' + }); + const [existingImages, setExistingImages] = useState([]); + const [newImages, setNewImages] = useState([]); + const [imagesToRemove, setImagesToRemove] = useState([]); + + useEffect(() => { + fetchPropertyData(); + }, [id]); + + const fetchPropertyData = async () => { + try { + const property = await propertyService.getPropertyById(id); + + // Convert amenities array to lowercase with underscores + const amenitiesFormatted = property.amenities?.map(a => + a.amenity_name || a + ) || []; + + // Convert house rules array to string + const rulesString = property.house_rules?.map(r => + r.rule_text || r + ).join('\n') || ''; + + setFormData({ + title: property.property_title || '', + propertyType: property.property_type || 'apartment', + city: property.city || '', + area: property.area_locality || '', + rent: property.monthly_rent || '', + deposit: property.deposit || '', + availableFrom: property.available_from || '', + genderPreference: property.preferred_tenant || 'any', + amenities: amenitiesFormatted, + rules: rulesString, + description: property.description || '' + }); + + // Set existing images + setExistingImages(property.images || []); + } catch (error) { + showError('Failed to load property data'); + navigate('/dashboard'); + } finally { + setLoading(false); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleAmenityToggle = (amenity) => { + setFormData(prev => { + const newAmenities = prev.amenities.includes(amenity) + ? prev.amenities.filter(a => a !== amenity) + : [...prev.amenities, amenity]; + return { ...prev, amenities: newAmenities }; + }); + }; + + const handleImageUpload = (e) => { + const files = Array.from(e.target.files); + const totalImages = existingImages.length - imagesToRemove.length + newImages.length + files.length; + + if (totalImages > 5) { + showError('Maximum 5 images allowed'); + return; + } + + // Validate file types + const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + const invalidFiles = files.filter(file => !validTypes.includes(file.type)); + if (invalidFiles.length > 0) { + showError('Please upload only JPG, PNG, or WebP images'); + return; + } + + // Validate file sizes (max 5MB per file) + const oversizedFiles = files.filter(file => file.size > 5 * 1024 * 1024); + if (oversizedFiles.length > 0) { + showError('Each image must be less than 5MB'); + return; + } + + setNewImages(prev => [...prev, ...files]); + }; + + const removeExistingImage = (imageUrl) => { + setImagesToRemove(prev => [...prev, imageUrl]); + }; + + const restoreExistingImage = (imageUrl) => { + setImagesToRemove(prev => prev.filter(url => url !== imageUrl)); + }; + + const removeNewImage = (index) => { + setNewImages(prev => prev.filter((_, i) => i !== index)); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!user) { + navigate('/login'); + return; + } + + // Validate minimum images + const totalImages = existingImages.length - imagesToRemove.length + newImages.length; + if (totalImages < 3) { + showError('At least 3 images are required'); + return; + } + + setSubmitting(true); + + try { + // Convert rules string to array + const rulesArray = formData.rules + .split('\n') + .map(rule => rule.trim()) + .filter(rule => rule.length > 0); + + // Prepare property data + const propertyData = { + property_title: formData.title, + property_type: formData.propertyType, + city: formData.city, + area_locality: formData.area, + monthly_rent: parseInt(formData.rent), + deposit: parseInt(formData.deposit), + available_from: formData.availableFrom, + preferred_tenant: formData.genderPreference, + amenities: formData.amenities, + house_rules: rulesArray, + description: formData.description, + images: newImages, // New images to upload + images_to_remove: imagesToRemove // Existing images to remove + }; + + await propertyService.updateProperty(id, propertyData); + showSuccess('Property updated successfully!'); + navigate('/dashboard'); + } catch (error) { + showError(error.response?.data?.detail || 'Failed to update property. Please try again.'); + } finally { + setSubmitting(false); + } + }; + + if (loading) { + return ( +
+ +
+ +
+
+ ); + } + + const amenitiesList = [ + { value: 'wifi', label: 'WiFi' }, + { value: 'ac', label: 'AC' }, + { value: 'parking', label: 'Parking' }, + { value: 'power_backup', label: 'Power Backup' }, + { value: 'gym', label: 'Gym' }, + { value: 'swimming_pool', label: 'Swimming Pool' }, + { value: 'security', label: 'Security' }, + { value: 'lift', label: 'Lift' } + ]; + + return ( +
+ + +
+
+ +
+
+ + + +
+
+

Edit Property

+

Update your property listing details

+
+
+
+ +
+ {/* Basic Information */} +
+
+
+ + + +
+

Basic Information

+
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Property Images */} +
+
+
+ + + +
+

Property Images

+
+
+
+ {/* Current Images */} + {existingImages.length > 0 && ( +
+

Current Images

+
+ {existingImages.map((image, idx) => ( +
+
+ {`Property +
+ {imagesToRemove.includes(image) ? ( + + ) : ( + + )} + {imagesToRemove.includes(image) && ( +
+ REMOVED +
+ )} +
+ ))} +
+
+ )} + + {/* Add New Images */} +
+

+ {existingImages.length > 0 ? 'Add More Images' : 'Upload Images'} + + (Minimum 3 images total required) + +

+ +
+ +
+ + + +

Click to upload new photos

+

or drag and drop here

+

+ 📸 JPG, PNG, WebP • Max 5MB each • Up to 5 images total +

+
+
+ + {/* New Images Preview */} + {newImages.length > 0 && ( +
+

New Images to Upload

+
+ {newImages.map((file, idx) => ( +
+
+ {`New +
+ +
+ NEW +
+
+ ))} +
+
+ )} + + {/* Image Count Summary */} +
+
+ + Total Images: {existingImages.length - imagesToRemove.length + newImages.length} / 5 + + = 3 + ? 'text-green-600' + : 'text-red-600' + }`}> + {existingImages.length - imagesToRemove.length + newImages.length >= 3 + ? '✓ Minimum requirement met' + : `Need ${3 - (existingImages.length - imagesToRemove.length + newImages.length)} more image(s)` + } + +
+
+
+
+ + {/* Pricing & Availability */} +
+
+
+ + + +
+

Pricing & Availability

+
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Tenant Preference */} +
+
+
+ + + +
+

Tenant Preference

+
+
+
+ +
+ + {/* Amenities */} +
+
+
+ + + +
+

Amenities

+
+
+
+
+ {amenitiesList.map(amenity => ( + + ))} +
+
+ + {/* Description */} +
+
+
+ + + +
+

Description

+
+
+
+ +
+
+ )} + + {/* Preferences Tab */} + {activeTab === 'preferences' && ( +
+
+
+ +
+ + - + +
+
+
+ + +
+
+ +
+ +
+ {lifestyleOptions.map(option => ( + + ))} +
+
+ +
+ +
+ {interestOptions.map(option => ( + + ))} +
+
+
+ )} + + +
+
+ + + + + {/* Profile Preview Modal */} + {showPreview && ( +
setShowPreview(false)}> +
e.stopPropagation()}> + {/* Modal Header */} +
+
+

👁️ This is how others see you

+

Profile Preview

+
+ +
+ + {/* Profile Card */} +
+ {/* Avatar + Name */} +
+
+ {user?.profile_photo ? ( + {user.full_name} + ) : ( +
+ {user?.full_name?.[0]?.toUpperCase() || 'U'} +
+ )} +
+
+
+

{user?.full_name || 'No Name'}

+ {user?.is_verified && ( + + + Verified + + )} +
+
+ {formData.occupation && {formData.occupation}} + {formData.age && · {formData.age} yrs} + {formData.city && ( + + + {formData.city} + + )} +
+
+
+ + {/* Bio */} + {formData.about && ( +

{formData.about}

+ )} + + {/* Budget */} + {(formData.budgetMin || formData.budgetMax) && ( +
+ Budget: + + {formData.budgetMin && formData.budgetMax + ? `₹${formData.budgetMin} – ₹${formData.budgetMax}` + : formData.budgetMin ? `From ₹${formData.budgetMin}` : `Up to ₹${formData.budgetMax}`} + + {formData.gender && formData.gender !== '' && {formData.gender} preferred} +
+ )} + + {/* Lifestyle */} + {formData.lifestyle?.length > 0 && ( +
+

Lifestyle

+
+ {formData.lifestyle.map(tag => ( + {tag} + ))} +
+
+ )} + + {/* Interests */} + {formData.interests?.length > 0 && ( +
+

Interests

+
+ {formData.interests.map(tag => ( + {tag} + ))} +
+
+ )} + + {/* Empty state */} + {!formData.about && !formData.lifestyle?.length && !formData.interests?.length && ( +
+

Your profile looks a bit empty.

+

Add a bio, lifestyle tags & interests to stand out!

+
+ )} +
+
+
+ )} + + {/* Image Crop Modal */} + {showCropModal && selectedImage && ( + + )} + + ); +}; diff --git a/src/pages/Reviews.jsx b/src/pages/Reviews.jsx deleted file mode 100644 index 686f2a4..0000000 --- a/src/pages/Reviews.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Badge } from '../components/ui/Badge'; -import { Star } from 'lucide-react'; -import { userReviews } from '../data/reviews'; - -export const Reviews = () => { - return ( - -
-

My Reviews

- - -

Reviews Given

- {userReviews.given.length === 0 ? ( -

No reviews given yet

- ) : ( -
- {userReviews.given.map((review) => ( -
-
-

{review.targetName}

-
- {[...Array(5)].map((_, i) => ( - - ))} -
-
-

{review.comment}

-

{review.date}

-
- ))} -
- )} -
- - -

Reviews Received

- {userReviews.received.length === 0 ? ( -

No reviews received yet

- ) : ( -
- {userReviews.received.map((review) => ( -
-
- {review.reviewerName} -
-
-

{review.reviewerName}

-
- {[...Array(5)].map((_, i) => ( - - ))} -
-
-

{review.comment}

-

{review.date}

-
-
-
- ))} -
- )} -
-
-
- ); -}; diff --git a/src/pages/RoomDetails.jsx b/src/pages/RoomDetails.jsx deleted file mode 100644 index f87f020..0000000 --- a/src/pages/RoomDetails.jsx +++ /dev/null @@ -1,360 +0,0 @@ -import { useState } from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Button } from '../components/ui/Button'; -import { Badge } from '../components/ui/Badge'; -import { Modal } from '../components/ui/Modal'; -import { Alert } from '../components/ui/Alert'; -import { rooms } from '../data/rooms'; -import { reviews } from '../data/reviews'; -import { - MapPin, - Star, - Heart, - Flag, - MessageCircle, - Home, - Wifi, - Wind, - Car, - Shield, - Users, - Calendar, - IndianRupee, - CheckCircle, -} from 'lucide-react'; - -export const RoomDetails = () => { - const { id } = useParams(); - const room = rooms.find((r) => r.id === id); - const roomReviews = reviews.filter((r) => r.roomId === id); - - const [currentImage, setCurrentImage] = useState(0); - const [showReportModal, setShowReportModal] = useState(false); - const [reportReason, setReportReason] = useState(''); - const [reportDescription, setReportDescription] = useState(''); - const [alert, setAlert] = useState(null); - const [wishlist, setWishlist] = useState(false); - - if (!room) { - return ( - -
-

Room not found

- - - -
-
- ); - } - - const amenityIcons = { - WiFi: Wifi, - AC: Wind, - Parking: Car, - Security: Shield, - Kitchen: Home, - Gym: Users, - }; - - const handleReport = () => { - if (!reportReason || !reportDescription) { - setAlert({ type: 'error', message: 'Please fill in all fields' }); - return; - } - setAlert({ type: 'success', message: 'Report submitted successfully' }); - setShowReportModal(false); - setReportReason(''); - setReportDescription(''); - }; - - const toggleWishlist = () => { - setWishlist(!wishlist); - setAlert({ - type: 'success', - message: wishlist ? 'Removed from wishlist' : 'Added to wishlist', - }); - }; - - return ( - -
- {alert && ( - setAlert(null)} /> - )} - - {/* Back Button */} - - - - - {/* Image Gallery */} -
-
-
- {room.title} - {room.featured && ( - - Featured - - )} -
-
-
- {room.images.map((image, index) => ( - - ))} -
-
- -
- {/* Main Content */} -
- {/* Title & Actions */} -
-
-
-

{room.title}

-
-
- - {room.area}, {room.city} -
-
- - {room.ownerRating} - ({room.reviews} reviews) -
-
-
-
- - -
-
- -
- {room.roomType} - {room.gender} - {room.availability} - {room.furnished && Furnished} -
-
- - {/* Description */} - -

About this room

-

{room.description}

-
- - {/* Amenities */} - -

Amenities

-
- {room.amenities.map((amenity) => { - const Icon = amenityIcons[amenity] || CheckCircle; - return ( -
- - {amenity} -
- ); - })} -
-
- - {/* House Rules */} - -

House Rules

-
    - {room.rules.map((rule, index) => ( -
  • - - {rule} -
  • - ))} -
-
- - {/* Reviews */} - -

- Reviews ({roomReviews.length}) -

- {roomReviews.length === 0 ? ( -

No reviews yet

- ) : ( -
- {roomReviews.map((review) => ( -
-
- {review.userName} -
-
-
-

{review.userName}

-

{review.date}

-
-
- {[...Array(5)].map((_, i) => ( - - ))} -
-
-

{review.comment}

- -
-
-
- ))} -
- )} -
-
- - {/* Sidebar */} -
- {/* Price Card */} - -
- ₹{room.rent.toLocaleString()} -
-

per month

- -
-
- Deposit - ₹{room.deposit.toLocaleString()} -
-
- Room Type - {room.roomType} -
-
- Occupancy - - {room.occupants}/{room.maxOccupants} - -
-
- Available from - {room.availability} -
-
- - - -
- - {/* Owner Card */} - -

Hosted by

-
-
- {room.ownerName[0]} -
-
-
-

{room.ownerName}

- {room.ownerVerified && ( - - )} -
-
- - {room.ownerRating} - • {room.reviews} reviews -
-
-
- - - -
-
-
-
- - {/* Report Modal */} - setShowReportModal(false)} - title="Report this listing" - size="md" - > -
-
- - -
-
- - +
+
+ + +
+ +
+ + )} + + {/* Review Modal */} + {showReviewModal && ( +
+
setShowReviewModal(false)}>
+
+
+

Write a Review

+ +
+
+
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+
+ + +
+
+ + +
+
+
+
+ )} + + {/* Verification Warning Modal */} + setShowVerificationWarning(false)} + message="To access room details, you need to complete your personal details and verify your identity." + secondaryAction="Back to Rooms" + secondaryPath="/rooms" + /> + + {/* Contact Details Modal */} + {showContactModal && ( +
+
setShowContactModal(false)}>
+
+ + {/* Header */} +
+

Owner Profile

+ +
+ + {/* Profile */} +
+
+
+ {room.owner.profile_photo ? ( + {room.owner.full_name} + ) : ( +
+ {room.owner.full_name?.[0] || 'U'} +
+ )} +
+
+
+

{room.owner.full_name}

+ {room.owner.is_verified && ( + + + Verified + + )} +
+
+ {room.owner.occupation && {room.owner.occupation}} + {room.owner.occupation && room.owner.city && ·} + {room.owner.city && {room.owner.city}} +
+
+
+ + {room.owner.bio && ( +

{room.owner.bio}

+ )} + + {/* Contact rows */} +
+
+
+
+ +
+
+

Email

+

{room.owner.email}

+
+
+ + Send Mail + +
+ + {(room.owner.phone_number || room.owner.phone) && ( +
+
+
+ +
+
+

Phone

+

{room.owner.phone_number || room.owner.phone}

+
+
+ + Call Now + +
+ )} +
+
+
+
+ )} + + ); +}; diff --git a/src/pages/Rooms.jsx b/src/pages/Rooms.jsx deleted file mode 100644 index 6a168b6..0000000 --- a/src/pages/Rooms.jsx +++ /dev/null @@ -1,230 +0,0 @@ -import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { MainLayout } from '../layouts/MainLayout'; -import { Card } from '../components/ui/Card'; -import { Button } from '../components/ui/Button'; -import { Badge } from '../components/ui/Badge'; -import { rooms } from '../data/rooms'; -import { MapPin, Star, Filter, Search } from 'lucide-react'; - -export const Rooms = () => { - const [filters, setFilters] = useState({ - city: '', - minRent: '', - maxRent: '', - gender: '', - roomType: '', - search: '', - }); - - const [showFilters, setShowFilters] = useState(false); - - const filteredRooms = rooms.filter((room) => { - if (filters.city && room.city !== filters.city) return false; - if (filters.minRent && room.rent < parseInt(filters.minRent)) return false; - if (filters.maxRent && room.rent > parseInt(filters.maxRent)) return false; - if (filters.gender && filters.gender !== 'Any' && room.gender !== filters.gender) return false; - if (filters.roomType && room.roomType !== filters.roomType) return false; - if (filters.search && !room.title.toLowerCase().includes(filters.search.toLowerCase())) return false; - return true; - }); - - return ( - -
-
-
-

Find Your Room

-

{filteredRooms.length} rooms available

-
- -
- -
- {/* Filters Sidebar */} -
- -

Filters

- -
-
- - setFilters({ ...filters, search: e.target.value })} - className="w-full pl-10 input" - /> -
- -
- - -
- -
- -
- setFilters({ ...filters, minRent: e.target.value })} - className="input" - /> - setFilters({ ...filters, maxRent: e.target.value })} - className="input" - /> -
-
- -
- - -
- -
- - -
- - -
-
-
- - {/* Rooms Grid */} -
- {filteredRooms.length === 0 ? ( - -
- -
-

No rooms found

-

Try adjusting your filters

- -
- ) : ( -
- {filteredRooms.map((room) => ( - - -
- {room.title} - {room.featured && ( - - Featured - - )} - {room.ownerVerified && ( - - ✓ Verified - - )} -
-
-

- {room.title} -

-
- - {room.area}, {room.city} -
-

- {room.description} -

-
-
-
- ₹{room.rent.toLocaleString()} -
-
per month
-
-
- - {room.ownerRating} - ({room.reviews}) -
-
-
-
- - ))} -
- )} -
-
-
-
- ); -}; diff --git a/src/pages/RoomsPage.jsx b/src/pages/RoomsPage.jsx new file mode 100644 index 0000000..3bdc31f --- /dev/null +++ b/src/pages/RoomsPage.jsx @@ -0,0 +1,334 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Navbar } from '../components/Navbar'; +import { GUJARAT_CITIES } from '../constants/cities'; +import { AMENITIES_LIST } from '../constants/amenities'; +import { propertyService } from '../services/propertyService'; +import { getImageUrl } from '../utils/imageUtils'; + +export const RoomsPage = () => { + const [filters, setFilters] = useState({ + search: '', + city: '', + rentMin: 0, + rentMax: 100000, + gender: '', + amenities: [], + }); + const [sortBy, setSortBy] = useState('newest'); + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + // Fetch properties from API + useEffect(() => { + const timer = setTimeout(() => { + fetchProperties(); + }, 500); + return () => clearTimeout(timer); + }, [filters.city, filters.rentMin, filters.rentMax, filters.amenities]); + + const fetchProperties = async () => { + setError(''); + + try { + const apiFilters = { + city: filters.city || undefined, + minRent: filters.rentMin > 0 ? filters.rentMin : undefined, + maxRent: filters.rentMax || undefined, + amenities: filters.amenities.length > 0 ? filters.amenities : undefined, + }; + + const response = await propertyService.getProperties(apiFilters); + setRooms(response.data || []); + } catch (err) { + setError('Failed to load properties. Please try again.'); + setRooms([]); + } finally { + setLoading(false); + } + }; + + const handleAmenityChange = (amenity) => { + setFilters(prev => { + if (prev.amenities.includes(amenity)) { + return { ...prev, amenities: prev.amenities.filter(a => a !== amenity) }; + } else { + return { ...prev, amenities: [...prev.amenities, amenity] }; + } + }); + }; + + // Client-side filtering for search and sorting + const filteredRooms = rooms.filter((room) => { + const matchesSearch = filters.search === '' || + room.property_title.toLowerCase().includes(filters.search.toLowerCase()) || + room.area_locality.toLowerCase().includes(filters.search.toLowerCase()); + + const matchesGender = filters.gender === '' || filters.gender === 'Any' || + (filters.gender === 'Male' && (room.preferred_tenant === 'male' || room.preferred_tenant === 'any')) || + (filters.gender === 'Female' && (room.preferred_tenant === 'female' || room.preferred_tenant === 'any')) || + (filters.gender === 'Family' && (room.preferred_tenant === 'family' || room.preferred_tenant === 'any')); + + return matchesSearch && matchesGender; + }).sort((a, b) => { + if (sortBy === 'priceLow') return a.monthly_rent - b.monthly_rent; + if (sortBy === 'priceHigh') return b.monthly_rent - a.monthly_rent; + return 0; + }); + + return ( +
+ + {/* Mobile Filter Toggle (Visible only on small screens) */} +
+ +
+ +
+ {/* Sidebar Filters (Desktop) */} + + + {/* Main Content */} +
+ {/* Search Bar + Sort */} +
+
+ setFilters({ ...filters, search: e.target.value })} + className="w-full px-4 py-2 pl-9 rounded-xl border border-gray-200 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-100 outline-none transition-all text-sm" + /> + + + +
+ +
+ + {/* Error Message */} + {error && ( +
+ + + + {error} +
+ )} + + {/* Loading State */} + {loading && ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+
+
+
+
+
+
+
+ ))} +
+ )} + + {/* Grid */} + {!loading && ( + <> +
+ {filteredRooms.map((room) => ( + +
+
+ 0 ? room.images[0] : null)} + alt={room.property_title} + className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" + /> +
+ {room.owner_verified && ( +
+ + Verified +
+ )} +
+
+ +
+
+

{room.property_title}

+

+ + {room.area_locality}, {room.city} +

+
+ +
+ {room.amenities && room.amenities.slice(0, 3).map((amenity, index) => ( + + {amenity.replace('_', ' ')} + + ))} + {room.amenities && room.amenities.length > 3 && ( + + +{room.amenities.length - 3} more + + )} +
+ +
+ + {room.preferred_tenant === 'any' ? 'Any' : room.preferred_tenant.charAt(0).toUpperCase() + room.preferred_tenant.slice(1)} + +
+ +
+
+ ₹{room.monthly_rent.toLocaleString()}/mo +
+ +
+
+ + ))} + + {filteredRooms.length === 0 && !loading && ( +
+
+ +
+

No matches found

+

We couldn't find any rooms matching your filters.

+ +
+ )} +
+ {filteredRooms.length > 0 && ( +

{filteredRooms.length} result{filteredRooms.length !== 1 ? 's' : ''} found

+ )} + + )} +
+
+
+ ); +}; diff --git a/src/pages/Signup.jsx b/src/pages/Signup.jsx deleted file mode 100644 index 39473a6..0000000 --- a/src/pages/Signup.jsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; -import { Button } from '../components/ui/Button'; -import { Input } from '../components/ui/Input'; -import { Alert } from '../components/ui/Alert'; -import { Eye, EyeOff } from 'lucide-react'; - -export const Signup = () => { - const navigate = useNavigate(); - const { signup } = useAuth(); - const [showPassword, setShowPassword] = useState(false); - const [alert, setAlert] = useState(null); - const [formData, setFormData] = useState({ - name: '', - email: '', - password: '', - phone: '', - city: '', - role: 'seeker', - }); - - const handleSubmit = (e) => { - e.preventDefault(); - - if (!formData.name || !formData.email || !formData.password || !formData.phone || !formData.city) { - setAlert({ type: 'error', message: 'Please fill in all fields' }); - return; - } - - if (formData.password.length < 6) { - setAlert({ type: 'error', message: 'Password must be at least 6 characters' }); - return; - } - - signup(formData); - setAlert({ type: 'success', message: 'Account created successfully!' }); - setTimeout(() => navigate('/dashboard'), 500); - }; - - return ( -
- {/* Left Side - Image */} -
-
-
-

- Start Your Journey Today -

-

- Create an account and discover amazing living spaces in your city. -

-
-
- - {/* Right Side - Form */} -
-
-
- -
- R -
- - RoomMateX - - -

- Create Account -

-

- Join our community to find your perfect room -

-
- - {alert && ( -
- setAlert(null)} /> -
- )} - -
- setFormData({ ...formData, name: e.target.value })} - /> - - setFormData({ ...formData, email: e.target.value })} - /> - -
- -
- setFormData({ ...formData, password: e.target.value })} - className="input pr-12" - /> - -
-
- - setFormData({ ...formData, phone: e.target.value })} - /> - - setFormData({ ...formData, city: e.target.value })} - /> - -
- -
- - -
-
- - - - -
- -
-

- Already have an account?{' '} - - Sign in - -

-
-
-
-
- ); -}; diff --git a/src/pages/SignupPage.jsx b/src/pages/SignupPage.jsx new file mode 100644 index 0000000..6ab4e62 --- /dev/null +++ b/src/pages/SignupPage.jsx @@ -0,0 +1,293 @@ +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { GUJARAT_CITIES } from '../constants/cities'; + +export const SignupPage = () => { + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + phone: '', + city: '', + role: '', + agreedToTerms: false, + }); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const { signup } = useAuth(); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + if (!formData.name || !formData.email || !formData.password || !formData.phone || !formData.city || !formData.role) { + setError('Please fill in all fields'); + return; + } + + if (!/^\d{10}$/.test(formData.phone)) { + setError('Phone number must be exactly 10 digits'); + return; + } + + if (!formData.agreedToTerms) { + setError('Please agree to Terms and Conditions'); + return; + } + + setLoading(true); + + const result = await signup({ + fullName: formData.name, + email: formData.email, + password: formData.password, + phone: formData.phone, + city: formData.city, + role: formData.role === 'Room Seeker' ? 'room_seeker' : 'room_owner', + }); + + setLoading(false); + + if (result.success) { + navigate('/dashboard'); + } else { + setError(result.error || 'Registration failed'); + } + }; + + return ( +
+ {/* Left Side: Themed/Animated Content & Benefits */} +
+ {/* Simplified Background */} +
+
+
+
+
+ +
+ + RoomMateX Logo + + Room + MateX + + + +

+ Join Our Trusted + Community +

+ +

+ Create an account to unlock full access to verified rooms and roommates. +

+ +
+
+
+ +
+
+

Verified Community

+

Every member is verified for security and trust.

+
+
+
+
+ +
+
+

Direct Communication

+

Connect directly with room owners and seekers.

+
+
+
+
+ +
+
+

Smart Listings

+

Advanced filters to find exactly what you need.

+
+
+
+
+
+ + {/* Right Side: Form */} +
+
+
+ + RoomMateX Logo + + Room + MateX + + +
+ +
+

Create Account

+

Fill in your details to get started.

+
+ + {error && ( +
+ + + + {error} +
+ )} + +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + /> +
+
+ + { + const value = e.target.value.replace(/\D/g, ''); + if (value.length <= 10) setFormData({ ...formData, phone: value }); + }} + /> +
+
+ +
+ + setFormData({ ...formData, email: e.target.value })} + /> +
+ +
+ +
+ setFormData({ ...formData, password: e.target.value })} + /> + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ + + + +
+ +

+ Already have an account?{' '} + Sign in +

+
+
+
+ ); +}; diff --git a/src/pages/UserProfileViewPage.jsx b/src/pages/UserProfileViewPage.jsx new file mode 100644 index 0000000..121b67f --- /dev/null +++ b/src/pages/UserProfileViewPage.jsx @@ -0,0 +1,256 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { useState, useEffect } from 'react'; +import { Navbar } from '../components/Navbar'; +import api from '../services/api'; +import { LoadingSpinner } from '../components/LoadingSpinner'; +import { getImageUrl } from '../utils/imageUtils'; + +export const UserProfileViewPage = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + fetchUserProfile(); + }, [id]); + + const fetchUserProfile = async () => { + setLoading(true); + setError(''); + + try { + const response = await api.get(`/users/${id}`); + setUser(response.data); + } catch (err) { + setError('Failed to load user profile.'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+ +
+ +
+
+ ); + } + + if (error || !user) { + return ( +
+ +
+
+

{error || 'User not found'}

+ +
+
+
+ ); + } + + return ( +
+ +
+ + +
+ {/* Header with Avatar and Name */} +
+ {user.profile_photo ? ( + {user.full_name} + ) : ( +
+ {user.full_name?.[0]?.toUpperCase() || 'U'} +
+ )} +
+
+

{user.full_name}

+ {user.is_verified && ( + + + + + Verified + + )} +
+ {user.role && ( +

{user.role.replace('_', ' ')}

+ )} +
+
+ + {/* Contact Information */} +
+

Contact Information

+
+ {user.email && ( +
+ + + +
+

Email

+

{user.email}

+
+
+ )} + + {user.phone_number && ( +
+ + + +
+

Phone

+

{user.phone_number}

+
+
+ )} + + {user.city && ( +
+ + + + +
+

City

+

{user.city}

+
+
+ )} +
+
+ + {/* Personal Information */} +
+

Personal Information

+
+ {user.age && ( +
+

Age

+

{user.age} years

+
+ )} + + {user.occupation && ( +
+

Occupation

+

{user.occupation}

+
+ )} +
+
+ + {/* Preferences */} + {(user.gender_preference || user.budget_min || user.budget_max || user.lifestyle || user.interests) && ( +
+

Preferences

+
+ {user.gender_preference && ( +
+

Gender Preference

+

{user.gender_preference}

+
+ )} + + {(user.budget_min || user.budget_max) && ( +
+

Budget Range

+

+ ₹{user.budget_min || 0} - ₹{user.budget_max || 'No limit'} +

+
+ )} + + {user.lifestyle && (() => { + try { + const lifestyleArray = JSON.parse(user.lifestyle); + return lifestyleArray.length > 0 && ( +
+

Lifestyle

+
+ {lifestyleArray.map((item, index) => ( + + {item} + + ))} +
+
+ ); + } catch (e) { + return null; + } + })()} + + {user.interests && (() => { + try { + const interestsArray = JSON.parse(user.interests); + return interestsArray.length > 0 && ( +
+

Interests

+
+ {interestsArray.map((item, index) => ( + + {item} + + ))} +
+
+ ); + } catch (e) { + return null; + } + })()} +
+
+ )} + + {/* Bio */} + {user.bio && ( +
+

About

+
+

{user.bio}

+
+
+ )} + + {/* Empty State for Bio */} + {!user.bio && ( +
+

About

+
+

No bio available

+
+
+ )} +
+
+
+ ); +}; diff --git a/src/pages/VerificationPage.jsx b/src/pages/VerificationPage.jsx new file mode 100644 index 0000000..57bd82d --- /dev/null +++ b/src/pages/VerificationPage.jsx @@ -0,0 +1,576 @@ +import { useState, useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { Navbar } from '../components/Navbar'; +import { authService } from '../services/authService'; +import { userService } from '../services/userService'; +import { checkProfileCompleteness, getProfileCompletionMessage } from '../utils/profileUtils'; + +export const VerificationPage = () => { + const { user, verifyOTP, sendOTP, refreshUser } = useAuth(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const action = searchParams.get('action') || 'verify'; + const [step, setStep] = useState(1); + const [verificationType, setVerificationType] = useState(''); + const [otp, setOtp] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const [profileCompleteness, setProfileCompleteness] = useState(null); + const [passwordData, setPasswordData] = useState({ + currentPassword: '', + newPassword: '', + confirmPassword: '' + }); + + // Check profile completeness when component mounts or user changes + useEffect(() => { + if (user && action === 'verify') { + const completeness = checkProfileCompleteness(user); + setProfileCompleteness(completeness); + + // If profile is not complete, show error immediately + if (!completeness.isComplete) { + setError(getProfileCompletionMessage(completeness)); + } + } + }, [user, action]); + + const handleSendOtp = async (type) => { + // Check profile completeness for verification action + if (action === 'verify') { + const completeness = checkProfileCompleteness(user); + if (!completeness.isComplete) { + setError(getProfileCompletionMessage(completeness)); + return; + } + } + + setVerificationType(type); + setLoading(true); + setError(''); + + try { + if (action === 'verify') { + const result = await sendOTP('verification'); + if (result.success) { + setStep(2); + } else { + setError(result.error || 'Failed to send OTP'); + } + } else if (action === 'changePassword') { + // Send OTP for password change + const result = await sendOTP('password_change'); + if (result.success) { + setStep(2); + } else { + setError(result.error || 'Failed to send OTP'); + } + } else if (action === 'deleteAccount') { + // Send OTP for account deletion + const result = await sendOTP('account_delete'); + if (result.success) { + setStep(2); + } else { + setError(result.error || 'Failed to send OTP'); + } + } + } catch (err) { + setError('Failed to send OTP. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleVerifyOtp = async () => { + if (!otp || otp.length < 6) { + setError('Please enter a valid 6-digit OTP'); + return; + } + + setLoading(true); + setError(''); + + try { + if (action === 'verify') { + const result = await verifyOTP(otp, 'verification'); + if (result.success) { + setStep(3); + setTimeout(() => { + navigate('/profile'); + }, 2000); + } else { + setError(result.error || 'Invalid OTP'); + } + } else if (action === 'changePassword') { + // Verify OTP for password change, then move to password form + const result = await verifyOTP(otp, 'password_change'); + if (result.success) { + setStep(4); // Move to password change form + } else { + setError(result.error || 'Invalid OTP'); + } + } else if (action === 'deleteAccount') { + // Verify OTP for account deletion, then delete account + const result = await verifyOTP(otp, 'account_delete'); + if (result.success) { + // Delete account + try { + await userService.deleteAccount(); + setStep(3); + setTimeout(() => { + authService.logout(); + navigate('/'); + }, 2000); + } catch (err) { + setError(err.response?.data?.detail || 'Failed to delete account'); + } + } else { + setError(result.error || 'Invalid OTP'); + } + } + } catch (err) { + setError('Verification failed. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handlePasswordChange = (e) => { + const { name, value } = e.target; + setPasswordData(prev => ({ ...prev, [name]: value })); + }; + + const handleChangePassword = async () => { + if (!passwordData.currentPassword || !passwordData.newPassword || !passwordData.confirmPassword) { + setError('Please fill all fields'); + return; + } + if (passwordData.newPassword !== passwordData.confirmPassword) { + setError('New passwords do not match'); + return; + } + if (passwordData.newPassword.length < 6) { + setError('Password must be at least 6 characters'); + return; + } + + setLoading(true); + setError(''); + + try { + await authService.changePassword(passwordData.currentPassword, passwordData.newPassword); + setStep(5); + setTimeout(() => { + navigate('/profile'); + }, 2000); + } catch (err) { + setError(err.response?.data?.detail || 'Failed to change password'); + } finally { + setLoading(false); + } + }; + + const getTitle = () => { + if (action === 'verify') return 'Verify Your Identity'; + if (action === 'changePassword') return 'Change Password'; + if (action === 'deleteAccount') return 'Delete Account'; + return 'Verification'; + }; + + const getDescription = () => { + if (action === 'verify') return 'Complete the verification process to access all features'; + if (action === 'changePassword') return 'Verify your identity to change your password'; + if (action === 'deleteAccount') return 'Verify your identity to delete your account'; + return ''; + }; + + const getSuccessMessage = () => { + if (action === 'verify') return "You're Verified!"; + if (action === 'deleteAccount') return 'Account Deleted!'; + return 'Verified!'; + }; + + return ( +
+ + +
+
+
+ {/* Left Side - Information */} +
+
+
+ {action === 'verify' && ( +
+ +
+ )} + {action === 'changePassword' && ( +
+ +
+ )} + {action === 'deleteAccount' && ( +
+ +
+ )} +

{getTitle()}

+

{getDescription()}

+
+ +
+
+
+ 1 +
+
+

Choose Method

+

Select email or phone verification

+
+
+
+
+ 2 +
+
+

Enter OTP

+

Verify with the code sent to you

+
+
+
+
+ 3 +
+
+

Complete

+

+ {action === 'verify' && 'Get verified badge on your profile'} + {action === 'changePassword' && 'Set your new password'} + {action === 'deleteAccount' && 'Confirm account deletion'} +

+
+
+
+
+
+ + {/* Right Side - Form */} +
+ + {step === 1 && ( +
+

Choose Verification Method

+ + {/* Profile Completeness Check for Verification */} + {action === 'verify' && profileCompleteness && !profileCompleteness.isComplete && ( +
+
+
+ + + +
+
+

Complete Your Profile First

+

+ To verify your account, you need to complete all profile information. This helps us ensure account security and provides better matches. +

+
+
+ Profile Progress + {profileCompleteness.filledFields}/{profileCompleteness.totalFields} fields completed +
+
+
+
+

+ {profileCompleteness.completionPercentage}% complete - + Missing: {profileCompleteness.missingFields.slice(0, 2).join(', ')} + {profileCompleteness.missingFields.length > 2 && ` and ${profileCompleteness.missingFields.length - 2} more`} +

+
+ +
+
+
+ )} + + {action === 'verify' && profileCompleteness && profileCompleteness.isComplete && ( +
+
+
+ + + +
+
+

Profile Complete!

+

Your profile is 100% complete. You can now verify your account.

+
+
+
+ )} + + {/* Notice */} + {(!profileCompleteness || profileCompleteness.isComplete) && ( +
+

+ + + + Click once and wait. It may take 2-3 seconds to send the OTP. +

+
+ )} + + + +

Phone verification is currently under process. Please use email verification.

+ + +
+ )} + + {step === 2 && ( +
+

Enter OTP Code

+
+

We sent an OTP to your {verificationType}

+

{verificationType === 'email' ? user?.email : user?.phone || '+91 98765 XXXXX'}

+
+ +
+ + { setOtp(e.target.value); setError(''); }} + className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all text-center tracking-[0.5em] font-bold text-lg" + placeholder="000000" + maxLength={6} + /> + {error &&

{error}

} +

+
+ + + + + + {(action === 'changePassword' || action === 'deleteAccount') && ( + + )} +
+ )} + + {step === 3 && action !== 'changePassword' && ( +
+
+ +
+

{getSuccessMessage()}

+

+ {action === 'verify' && 'Your profile now has the blue tick badge.'} + {action === 'deleteAccount' && 'Your account has been permanently deleted.'} +

+

Redirecting to profile...

+
+ )} + + {step === 3 && action === 'changePassword' && ( +
+
+ +
+

Verified!

+

Now you can change your password.

+
+ )} + + {step === 4 && action === 'changePassword' && ( +
+

Set New Password

+ {error &&
{error}
} +
+ + { handlePasswordChange(e); setError(''); }} + className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all" + placeholder="Enter current password" + /> +
+
+ + { handlePasswordChange(e); setError(''); }} + className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all" + placeholder="Enter new password" + /> +
+
+ + { handlePasswordChange(e); setError(''); }} + className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all" + placeholder="Confirm new password" + /> +
+

Password must be at least 6 characters long

+ + +
+ )} + + {step === 5 && action === 'changePassword' && ( +
+
+ +
+

Password Changed!

+

Your password has been updated successfully.

+

Redirecting to profile...

+
+ )} +
+
+
+
+
+ ); +}; diff --git a/src/pages/Wishlist.jsx b/src/pages/Wishlist.jsx deleted file mode 100644 index b3db3fe..0000000 --- a/src/pages/Wishlist.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import { MainLayout } from '../layouts/MainLayout'; -import { EmptyState } from '../components/ui/EmptyState'; -import { Heart } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; - -export const Wishlist = () => { - const navigate = useNavigate(); - - return ( - - navigate('/rooms')} - actionLabel="Browse Rooms" - /> - - ); -}; diff --git a/src/pages/WishlistPage.jsx b/src/pages/WishlistPage.jsx new file mode 100644 index 0000000..21a6c50 --- /dev/null +++ b/src/pages/WishlistPage.jsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Navbar } from '../components/Navbar'; +import { wishlistService } from '../services/wishlistService'; +import { dashboardService } from '../services/dashboardService'; +import { useAuth } from '../context/AuthContext'; +import { useToast } from '../context/ToastContext'; +import { getImageUrl } from '../utils/imageUtils'; + +export const WishlistPage = () => { + const { user } = useAuth(); + const { showError } = useToast(); + const [wishlist, setWishlist] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + if (user) { + fetchWishlist(); + } else { + setLoading(false); + } + }, [user]); + + const fetchWishlist = async () => { + setLoading(true); + setError(''); + + try { + const data = await dashboardService.getWishlist(); + setWishlist(data || []); + } catch (err) { + setError('Failed to load wishlist.'); + } finally { + setLoading(false); + } + }; + + const handleRemoveFromWishlist = async (propertyId) => { + try { + await wishlistService.removeFromWishlist(propertyId); + setWishlist(prev => prev.filter(item => item.id !== propertyId)); + } catch (err) { + showError('Failed to remove from wishlist. Please try again.'); + } + }; + + if (loading) { + return ( +
+ +
+
+
+

Loading wishlist...

+
+
+
+ ); + } + + return ( +
+ +
+
+

Your Wishlist

+

Properties you've saved for later

+
+ + {error && ( +
+ {error} +
+ )} + + {wishlist.length === 0 ? ( +
+
+ + + +
+

Your wishlist is empty

+

Start adding rooms you like!

+ + Browse Rooms + +
+ ) : ( +
+ {wishlist.map((item) => ( +
+
+
+ {item.property_title} + +
+ +
+
+

+ {item.property_title} +

+

+ + + + {item.city} +

+
+ +
+
+ ₹{item.monthly_rent?.toLocaleString()}/mo +
+ + View Details + + + + +
+
+
+ ))} +
+ )} +
+
+ ); +}; diff --git a/src/pages/admin/AdminDashboard.jsx b/src/pages/admin/AdminDashboard.jsx new file mode 100644 index 0000000..107b1f7 --- /dev/null +++ b/src/pages/admin/AdminDashboard.jsx @@ -0,0 +1,599 @@ +import { Users, UserCheck, Building2, Flame, TrendingUp, Star, Heart, FileText, AlertTriangle, CheckCircle, XCircle, Clock, Activity, Eye, MessageSquare, MapPin } from 'lucide-react'; +import { + LineChart, + Line, + BarChart, + Bar, + PieChart, + Pie, + Cell, + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + RadialBarChart, + RadialBar, + ComposedChart +} from 'recharts'; + +const COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']; + +export const AdminDashboard = ({ analytics }) => { + if (!analytics) return null; + + // Prepare data for radial chart + const verificationData = [ + { + name: 'Verified', + value: analytics.summary.verified_users, + fill: '#10B981' + }, + { + name: 'Unverified', + value: analytics.summary.unverified_users, + fill: '#F59E0B' + } + ]; + + const propertyStatusData = [ + { + name: 'Active', + value: analytics.summary.active_properties, + fill: '#3B82F6' + }, + { + name: 'Inactive', + value: analytics.summary.inactive_properties, + fill: '#9CA3AF' + } + ]; + + // Combined trend data for comparison + const combinedTrendData = analytics.users_trend.map((item, index) => ({ + date: item.date, + users: item.users, + properties: analytics.properties_trend[index]?.properties || 0 + })); + + return ( +
+ {/* Overview Section */} +
+

Platform Overview

+
+
+
+
+ +
+ +
+

{analytics.summary.total_users}

+

Total Users

+
+

{analytics.summary.verified_users} verified ({((analytics.summary.verified_users / analytics.summary.total_users) * 100).toFixed(1)}%)

+
+
+ +
+
+
+ +
+ +
+

{analytics.summary.total_properties}

+

Total Properties

+
+

{analytics.summary.active_properties} active ({((analytics.summary.active_properties / analytics.summary.total_properties) * 100).toFixed(1)}%)

+
+
+ +
+
+
+ +
+ +
+

{analytics.summary.total_reports}

+

Total Reports

+
+

{analytics.summary.pending_reports} pending review

+
+
+ +
+
+
+ +
+ +
+

{analytics.summary.average_rating}

+

Average Rating

+
+

{analytics.summary.total_reviews} total reviews

+
+
+
+
+ + {/* Key Metrics with Radial Charts */} +
+

Key Performance Indicators

+
+
+

User Verification Rate

+ + + + {[ + , + + ]} + + + {((analytics.summary.verified_users / analytics.summary.total_users) * 100).toFixed(1)}% + + + +
+

{analytics.summary.verified_users} of {analytics.summary.total_users} users

+
+
+ +
+

Property Active Rate

+ + + + {[ + , + + ]} + + + {((analytics.summary.active_properties / analytics.summary.total_properties) * 100).toFixed(1)}% + + + +
+

{analytics.summary.active_properties} of {analytics.summary.total_properties} properties

+
+
+ +
+

Report Resolution Rate

+ + + + {[ + , + + ]} + + + {analytics.summary.total_reports > 0 ? (((analytics.summary.fixed_reports + analytics.summary.rejected_reports) / analytics.summary.total_reports) * 100).toFixed(1) : 0}% + + + +
+

{analytics.summary.fixed_reports + analytics.summary.rejected_reports} of {analytics.summary.total_reports} resolved

+
+
+ +
+

Engagement Score

+ + + + {[ + , + + ]} + + + {Math.min(((analytics.summary.total_wishlists + analytics.summary.total_reviews) / analytics.summary.total_users) * 20, 100).toFixed(1)}% + + + +
+

{analytics.summary.total_wishlists + analytics.summary.total_reviews} interactions

+
+
+
+
+ + {/* Reports Section */} +
+

Reports & Complaints

+
+
+
+
+ +
+
+

{analytics.summary.pending_reports}

+

Pending

+
+
+
+
0 ? (analytics.summary.pending_reports / analytics.summary.total_reports) * 100 : 0}%` }} + >
+
+
+ +
+
+
+ +
+
+

{analytics.summary.fixed_reports}

+

Fixed

+
+
+
+
0 ? (analytics.summary.fixed_reports / analytics.summary.total_reports) * 100 : 0}%` }} + >
+
+
+ +
+
+
+ +
+
+

{analytics.summary.rejected_reports}

+

Rejected

+
+
+
+
0 ? (analytics.summary.rejected_reports / analytics.summary.total_reports) * 100 : 0}%` }} + >
+
+
+
+
+ + {/* Activity Trends */} +
+

Activity Trends (Last 30 Days)

+
+
+

Combined Growth Overview

+ + + + + + + + + + + + + + + + + + +
+
+
+ + {/* Distribution Analytics */} +
+

Distribution Analytics

+
+
+

User Roles

+ + + `${(percent * 100).toFixed(0)}%`} + outerRadius={70} + fill="#8884d8" + dataKey="value" + > + {analytics.user_roles.map((entry, index) => ( + + ))} + + + + +
+ {analytics.user_roles.map((role, index) => ( +
+
+
+ {role.name} +
+ {role.value} +
+ ))} +
+
+ +
+

Property Types

+ + + `${(percent * 100).toFixed(0)}%`} + outerRadius={70} + fill="#8884d8" + dataKey="value" + > + {analytics.property_types.map((entry, index) => ( + + ))} + + + + +
+ {analytics.property_types.map((type, index) => ( +
+
+
+ {type.name} +
+ {type.value} +
+ ))} +
+
+ +
+

Report Status

+ + + `${(percent * 100).toFixed(0)}%`} + outerRadius={70} + fill="#8884d8" + dataKey="value" + > + {analytics.report_status.map((entry, index) => { + const colors = { 'Pending': '#F59E0B', 'Fixed': '#10B981', 'Rejected': '#EF4444' }; + return ; + })} + + + + +
+ {analytics.report_status.map((status, index) => { + const colors = { 'Pending': '#F59E0B', 'Fixed': '#10B981', 'Rejected': '#EF4444' }; + return ( +
+
+
+ {status.name} +
+ {status.value} +
+ ); + })} +
+
+
+
+ + {/* Geographic & Pricing Analytics */} +
+

Geographic & Pricing Analytics

+
+
+
+

Top Cities by Properties

+ +
+ + + + + + + + {analytics.city_distribution.map((entry, index) => ( + + ))} + + + +
+ +
+

Rent Price Distribution

+ + + + + + + + + + + + + + + +
+
+
+ + {/* Additional Metrics */} +
+

Engagement Metrics

+
+
+
+
+ +
+
+

{analytics.summary.total_wishlists}

+

Total Wishlists

+
+
+
+

Avg {(analytics.summary.total_wishlists / analytics.summary.total_users).toFixed(1)} per user

+
+
+ +
+
+
+ +
+
+

{analytics.summary.verified_users}

+

Verified Users

+
+
+
+

{((analytics.summary.verified_users / analytics.summary.total_users) * 100).toFixed(1)}% of total

+
+
+ +
+
+
+ +
+
+

{analytics.summary.active_properties}

+

Active Properties

+
+
+
+

{((analytics.summary.active_properties / analytics.summary.total_properties) * 100).toFixed(1)}% of total

+
+
+ +
+
+
+ +
+
+

{analytics.summary.total_reviews}

+

Total Reviews

+
+
+
+

Avg {(analytics.summary.total_reviews / analytics.summary.total_properties).toFixed(1)} per property

+
+
+
+
+
+ ); +}; diff --git a/src/pages/admin/AdminSidebar.jsx b/src/pages/admin/AdminSidebar.jsx new file mode 100644 index 0000000..fee67e4 --- /dev/null +++ b/src/pages/admin/AdminSidebar.jsx @@ -0,0 +1,67 @@ +import { BarChart3, Users, Building2, ChevronLeft, ChevronRight, AlertTriangle } from 'lucide-react'; + +export const AdminSidebar = ({ sidebarOpen, setSidebarOpen, activeSection, setActiveSection, onLogout }) => { + const menuItems = [ + { id: 'dashboard', label: 'Dashboard', icon: BarChart3 }, + { id: 'users', label: 'Users Management', icon: Users }, + { id: 'properties', label: 'Properties/Listings', icon: Building2 }, + { id: 'reports', label: 'Reports/Complaints', icon: AlertTriangle }, + ]; + + return ( + + ); +}; diff --git a/src/pages/admin/NotifyOwnerModal.jsx b/src/pages/admin/NotifyOwnerModal.jsx new file mode 100644 index 0000000..b31c7ae --- /dev/null +++ b/src/pages/admin/NotifyOwnerModal.jsx @@ -0,0 +1,118 @@ +import { XCircle } from 'lucide-react'; + +export const NotifyOwnerModal = ({ + report, + ownerMessage, + setOwnerMessage, + onSend, + onClose, + actionLoading +}) => { + if (!report) return null; + + return ( +
+
+ {/* Modal Header */} +
+

Notify Property Owner

+ +
+ + {/* Modal Body */} +
+ {/* Report Details */} +
+
Report Information
+
+

Property: {report.property_title}

+

Property ID: {report.property_id}

+

Reported By: {report.user_name}

+

Report Reason: {report.reason}

+

Report Date: {new Date(report.created_at).toLocaleDateString()}

+
+
+ + {/* Warning Notice */} +
+

⚠️ Important Notice

+

+ This message will be sent to the property owner. Please be professional and clear about the issue and any required actions. +

+
+ + {/* Owner Message */} +
+ +