diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..4b9e2081 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,73 @@ +# Dependencies +node_modules +*/node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +*/.env* + +# IDE files +.vscode +.idea +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore +README.md + +# Testing +coverage +.nyc_output +*/coverage +*/test +*/tests + +# Logs +logs +*.log +*/logs +*/*.log + +# Docker files +Dockerfile +.dockerignore +docker-compose.yml +docker-compose.*.yml + +# Documentation +*.md +docs/ + +# Cache directories +.cache +.parcel-cache +*/.cache + +# Build artifacts +dist/ +build/ +*/dist +*/build + +# Temporary folders +tmp/ +temp/ \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..67ccbbb1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +name: "🐞 Bug Report" +about: "Report a bug to help improve the project" +title: "πŸ› : " +labels: "" +assignees: "" + +--- + +### πŸ” Have You Searched Existing Issues? + +- [ ] I have searched the existing issues to avoid duplicates + +--- + +### 🐞 Describe the Bug +What is happening and what should happen instead? + +--- + +### ▢️ Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Scroll to '...' +4. See error + +--- + +### βœ… Expected Behavior +What should have happened? + +--- + +### πŸ–ΌοΈ Screenshots (If applicable) +Add screenshots to help explain the issue. + +--- + +### πŸ“˜ Additional Context +Any other details that might help. + +--- + +### πŸ™Œ Contributor Checklist + +- [ ] I agree to follow this project's Code of Conduct +- [ ] I am a GSSOC'25 contributor +- [ ] I want to work on this issue \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/custom_issue.md b/.github/ISSUE_TEMPLATE/custom_issue.md new file mode 100644 index 00000000..928eae02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom_issue.md @@ -0,0 +1,59 @@ +--- +name: "βš™οΈ Custom Issue" +about: "Submit a custom issue or suggestion" +title: 'βš™οΈ :' +labels: '' +assignees: '' + +--- + +Thank you for submitting a custom issue for **Foodie**! +This template is intended for any requests or suggestions that don't fit into the bug report, feature request, or documentation categories. + +--- + +### πŸ” Have You Searched Existing Issues? + +- [ ] I have searched the existing issues to avoid duplicates + +--- + +### πŸ“ Issue Summary +Provide a short summary of the custom issue. +_Example: Enhancement suggestion for the search functionality._ + +--- + +### πŸ” Issue Description +Describe the issue or suggestion in detail. +Include any relevant context, use case, or scenario where this issue occurs or is important. + +--- + +### πŸ’‘ Proposed Solution (Optional) +If you have any ideas or suggestions for solving this issue, describe them here. + +--- + +### πŸ“‚ Category +_Select the category that best describes the issue:_ + +- [ ] Enhancement +- [ ] Refactor +- [ ] Security +- [ ] Design +- [ ] Other + +--- + +### πŸ“˜ Additional Context (Optional) +Add any other context, links, screenshots, or information that might be relevant. + +--- + +### πŸ™Œ Contributor Checklist (Optional) + +- [ ] I have searched existing issues to avoid duplicates +- [ ] I agree to follow this project's Code of Conduct +- [ ] I am a GSSOC'25 contributor +- [ ] I want to work on this issue \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation_update.md b/.github/ISSUE_TEMPLATE/documentation_update.md new file mode 100644 index 00000000..d9ea02a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_update.md @@ -0,0 +1,31 @@ +--- +name: "πŸ“‘ Documentation Update" +about: "Suggest updates or improvements to documentation" +title: "πŸ“‘ : " +labels: "" +assignees: "" + +--- + +### πŸ” Have You Searched Existing Issues? + +- [ ] I have searched the existing issues to avoid duplicates + +--- + +### πŸ“ What's wrong with the existing documentation? +Describe what needs to be fixed, added, or removed. + +--- + +### πŸ“Ž Supporting Material +Attach screenshots, videos, or links that help explain your documentation update. + +--- + +### πŸ™Œ Contributor Checklist + +- [ ] I have reviewed the existing documentation +- [ ] I agree to follow this project's Code of Conduct +- [ ] I am a GSSOC'25 contributor +- [ ] I want to work on this issue \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bd40367c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,46 @@ +--- +name: "✨ Feature Request" +about: "Suggest a new feature or enhancement" +title: "✨ : " +labels: "" +assignees: "" + +--- + +### πŸ” Have You Searched Existing Issues? + +- [ ] I have searched the existing issues to avoid duplicates + +--- + +### πŸ’‘ Problem Description +What problem are you facing that this feature would solve? + +--- + +### βœ… Proposed Solution +Describe the feature you'd like to see added. + +--- + +### πŸ”„ Alternatives Considered +Are there other ways you thought about solving this? + +--- + +### πŸ–ΌοΈ Screenshots or Diagrams (Optional) +Attach visuals or examples to support your idea. + +--- + +### πŸ“˜ Additional Context +Include any additional information, links, or references. + +--- + +### πŸ™Œ Contributor Checklist + +- [ ] I have checked for similar feature requests +- [ ] I agree to follow this project's Code of Conduct +- [ ] I am a GSSOC'25 contributor +- [ ] I want to work on this issue \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/performance_issue.md b/.github/ISSUE_TEMPLATE/performance_issue.md new file mode 100644 index 00000000..66c2619b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance_issue.md @@ -0,0 +1,40 @@ +--- +name: "⚑ Performance Issue" +about: "Report a performance slowdown or bottleneck" +title: "⚑: " +labels: "" +assignees: "" + +--- + +### πŸ” Have You Searched Existing Issues? + +- [ ] I have searched the existing issues to avoid duplicates + +--- + +### πŸ“‰ Describe the Performance Issue +What is slow or lagging? When does it happen? + +--- + +### πŸ§ͺ Environment Details +OS, browser, device, version, etc. + +--- + +### πŸ” Steps to Reproduce +Explain how someone else can experience this issue. + +--- + +### πŸ“‹ Logs / Screenshots (Optional) +Paste logs or upload visuals that show the issue. + +--- + +### πŸ™Œ Contributor Checklist + +- [ ] I agree to follow this project's Code of Conduct +- [ ] I want to work on this issue +- [ ] I am a GSSOC'25 contributor \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..0aa8415c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,29 @@ +--- +name: "❓ Question / Help" +about: "Ask a question or request support" +title: "❓: " +labels: "" +assignees: "" + +--- + +### πŸ” Have You Searched Existing Issues for the same Question? + +- [ ] I have searched the existing issues to avoid duplicates + +--- + +### 🧠 What's Your Question? +Be specific so others can help you quickly. + +--- + +### πŸ“˜ Context +Include screenshots, links, or any other relevant info. + +--- + +### πŸ™Œ Contributor Checklist + +- [ ] I have searched existing issues +- [ ] I agree to follow this project's Code of Conduct \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/ui_ux_suggestion.md b/.github/ISSUE_TEMPLATE/ui_ux_suggestion.md new file mode 100644 index 00000000..c84a734d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ui_ux_suggestion.md @@ -0,0 +1,32 @@ +--- +name: "🎨 UI/UX Suggestion" +about: "Propose improvements to UI or user experience" +title: "🎨 : " +labels: "" +assignees: "" +--- + +Suggest improvements for the user interface or experience. + +--- + +### πŸ–ΌοΈ Current UI/UX Behavior +Describe how the interface currently behaves or looks. + +--- + +### ✨ Suggested Improvement +What would you like to change or add? + +--- + +### πŸ“Ž Screenshots / Visual Aids +Mockups or screenshots help others understand your suggestion. + +--- + +### πŸ™Œ Contributor Checklist + +- [ ] I agree to follow this project's Code of Conduct +- [ ] I am a GSSOC'25 contributor +- [ ] I want to work on this issue diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..475e8672 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,55 @@ +## πŸ“Œ Pull Request Summary + +`A short description of the changes you're making. +Example: +Fixed a bug where the food items can be deleted.` + + +## πŸ› οΈ Type of Change + +Please select options that are relevant to your pull request + +- [ ] πŸ› Bug fix +- [ ] ✨ New feature +- [ ] 🧹 Code refactor +- [ ] πŸ§ͺ Tests added/updated +- [ ] πŸ“„ Documentation update +- [ ] πŸš€ Performance improvement +- [ ] πŸ”§ Other (please describe): + +## πŸ”— Related Issue + +Closes #[issue-number] + + + + +## βœ… Checklist + +Please confirm the following before submitting: + +- [ ] I have **tested** my changes locally +- [ ] I have run `npm run dev` or similar to check code style +- [ ] I have updated documentation (if necessary) +- [ ] My code follows the project’s coding conventions +- [ ] I have merged the latest `main` or `dev` branch +- [ ] I have performed a **self-review** of my own code. +- [ ] My changes **generate** no new warnings or errors. +- [ ] I have **commented** my code, especially in hard-to-understand areas +- [ ] I have **linked the related issue** + + + +## πŸ“Έ Screenshots (if applicable) + + +* Add before/after screenshots or UI changes. + + + +--- +## 🧠 Additional Notes + +- Anything else you want the reviewer to know. + + diff --git a/.gitignore b/.gitignore index da619ebd..0fe9db90 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules/ dist dist-ssr *.local +package-lock.json # Editor directories and files .vscode/* @@ -27,3 +28,6 @@ dist-ssr .env */ENV .env.* +# Ignore uploaded files from multer +/backend/uploads + diff --git a/.vite/deps/_metadata.json b/.vite/deps/_metadata.json new file mode 100644 index 00000000..9f54dc44 --- /dev/null +++ b/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "8c22f5da", + "configHash": "f7f324f3", + "lockfileHash": "e3b0c442", + "browserHash": "2d425869", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/.vite/deps/package.json b/.vite/deps/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/.vite/deps/vercel.json b/.vite/deps/vercel.json new file mode 100644 index 00000000..6a926487 --- /dev/null +++ b/.vite/deps/vercel.json @@ -0,0 +1,5 @@ +{ + "rewrites": [ + { "source": "/(.*)", "destination": "/" } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..effb56f1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ +# 🀝 Contributor Covenant Code of Conduct + +## ✨ Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. πŸŒπŸ’¬ + +## βœ… Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## πŸ›‘οΈ Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## 🌐 Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## πŸ“© Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +πŸ“§ email. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## πŸ“Š Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1️⃣ Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2️⃣ Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3️⃣ Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4️⃣ Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## πŸ“˜ Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..acfc9d52 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,70 @@ +## πŸ§‘β€πŸ’» Contributing + +Welcome! πŸ‘‹ +We're excited you're here and interested in contributing to **Foodie**, a simple and helpful MERN Project. + +This project is in its early stage β€” so **you’re welcome to help build it from the ground up!** + +> **First time here?** +> πŸ‘‰ Start with [LEARN.md](./LEARN.md) – your fast track to understanding Foodie’s structure and contributions! +--- + +### πŸš€ Getting Started + +1. **Fork the repo** +2. **Clone your fork** + + ```bash + git clone https://github.com/YOUR-USERNAME/Foodie.git + cd Foodie + ``` +3. **Create a new branch** + + ```bash + git checkout -b my-feature + ``` +4. **Start coding!** Edit files, add features, fix bugs. +5. **Commit and push** + + ```bash + git commit -m "Add: my feature" + git push origin my-feature + ``` +6. **Open a Pull Request** on the main repository + +--- + +### πŸ›  What Can You Work On? + +Since the project is new, feel free to contribute: + +* 🧱 UI improvements (card design, filters, responsiveness) +* πŸ—ƒοΈ JSON-based data structure improvements +* πŸ” Search and tag filters +* πŸŒ™ Theme switcher +* πŸ“₯ Upload form (simulated) +* 🐞 Bug fixes +* πŸ“„ Documentation and tutorials + +We’ll also add `good first issue` and `help wanted` labels as the project evolves. + +--- + +### πŸ“š Guidelines + +* Keep code clean and readable +* Use meaningful commit messages +* Comment your code when needed +* Respect the [Code of Conduct](CODE_OF_CONDUCT.md) + +--- + +### πŸ™Œ Need Help? + +If you're stuck or unsure, feel free to open an issue or ask in a discussion. We're here to help! + +--- + +Let’s build something useful and open together. πŸ’‘ +Happy coding! + diff --git a/LEARN.md b/LEARN.md new file mode 100644 index 00000000..a7ac78ae --- /dev/null +++ b/LEARN.md @@ -0,0 +1,448 @@ +# βœ¨πŸ”πŸš€ LEARN.md – The Ultimate Guide to the Foodie Codebase πŸ”πŸ•βœ¨ + +## Table of Contents πŸ“šπŸ—ΊοΈπŸ‘€ + +1. [Project Overview](#1-project-overview-) +2. [High-Level Architecture](#2-high-level-architecture-) +3. [Tech Stack Breakdown](#3-tech-stack-breakdown-) +4. [Folder-by-Folder Deep Dive](#4-folder-by-folder-deep-dive-) +5. [Contributor Pathways](#5-contributor-pathways-) +6. [Getting Started (Setup & Dev)](#6-getting-started-setup--dev-) +7. [Feature How-Tos (Front, Back, Admin, Chatbot)](#7-feature-how-tos-front-back-admin-chatbot-) +8. [API Documentation](#8-api-documentation-) +9. [Debugging, Testing, and Best Practices](#9-debugging-testing-and-best-practices-) +10. [Advanced Learning Resources](#10-advanced-learning-resources-) +11. [FAQ](#11-faq-) + +--- + +## 1. Project Overview πŸ§‘β€πŸ³πŸ“¦πŸ’‘ + +**Foodie** is a modern, open-source, full-stack food ordering application with a focus on scalability, modularity, and real-world learning. πŸ½οΈπŸŒπŸ› οΈ It provides: + +* A rich user-facing web app (frontend) +* A robust backend API (Node.js/Express) +* A dedicated admin panel +* Containerized, OSS-friendly workflows + +Foodie aims to bridge the gap between beginner contributions and real-world production code, making it a playground for developers who want to upskill and ship. πŸŽ“βš‘πŸŽ― + +**Live Demo:** [Coming Soon] | **Staging:** [Coming Soon] + +--- + +## 2. High-Level Architecture πŸ—οΈπŸ–₯οΈπŸ”§ + +```mermaid +graph TD; + A[Frontend User] -->|REST/API| C[Backend API] + B[Admin Panel] -->|REST/API| C + C -->|DB Connection| D[(Database)] + C -->|File Storage| E[Uploads/Images] + C -->|Integration| F[Chatbot Module] + C -->|Authentication| G[JWT/Auth] + C -->|Email Service| H[Email Notifications] +``` + +**Key Flows:** πŸƒβ€β™‚οΈπŸ”€πŸŒŠ + +* **Frontend & Admin** communicate with backend via REST APIs +* **Backend** handles business logic, database operations, file uploads, and chatbot integration +* **Docker Compose** manages orchestration for local development and deployment +* **Authentication** flows through JWT tokens for secure access + +--- + +## 3. Tech Stack Breakdown πŸ–₯οΈπŸ› οΈπŸŒˆ + +| Layer | Tech | +| ------------ | --------------------------------------------------------------- | +| Frontend | Vite + React 18+ | +| Backend | Node.js 18+, Express.js 4+ | +| Admin | Vite + React 18+ | +| Database | MongoDB with Mongoose ODM | +| Authentication| JWT, bcrypt | +| File Storage | Multer (local), Cloudinary (production) | +| Container | Docker, Docker Compose | +| Linting | ESLint, Prettier | +| Testing | Jest, React Testing Library | +| Others | Chatbot integration, Nodemailer, Socket.io | + +*Want to confirm versions? Check `package.json` files in each folder!* πŸ€“πŸ”πŸ—ƒοΈ + +--- + +## 4. Folder-by-Folder Deep Dive πŸ“πŸ”ŽπŸ‘“ + +### `/frontend` πŸš¦πŸŒπŸ›‹οΈ + +* **Purpose:** User-facing web application +* **Key Files:** + * `src/components/`: Reusable React components + * `src/pages/`: Main application pages + * `src/hooks/`: Custom React hooks + * `src/utils/`: Helper functions and utilities + * `src/api/`: API service functions + * `src/context/`: React Context providers + * `public/`: Static assets + * `index.html`: SPA entry point + * `package.json`: Dependencies & scripts + * `CHATBOT_FEATURE.md`: Guide to the built-in chatbot + +* **Pro Tips:** + * Use Vite for fast dev server and builds + * Follow component composition patterns + * Implement proper error boundaries + * Use environment variables for API endpoints + +### `/backend` πŸ› οΈπŸ§ πŸ“‘ + +* **Purpose:** API server, business logic, database operations +* **Key Files:** + * `server.js`: Application entry point + * `routes/`: API endpoint definitions + * `controllers/`: Business logic handlers + * `models/`: Database schemas (Mongoose) + * `middlewares/`: Authentication, validation, error handling + * `utils/`: Helper functions + * `config/`: Database and environment configuration + * `uploads/`: Local file storage + * `tests/`: API tests + +* **Pro Tips:** + * Follow MVC architecture pattern + * Use async/await consistently + * Implement proper error handling + * Add comprehensive API documentation + +### `/admin` πŸ‘©β€πŸ’ΌπŸ“ŠπŸŽ›οΈ + +* **Purpose:** Administrative dashboard for restaurant management +* **Key Files:** + * Similar structure to frontend + * `src/components/admin/`: Admin-specific components + * `src/pages/dashboard/`: Dashboard views + * `src/services/`: Admin API services + +* **Pro Tips:** + * Implement role-based access control + * Create intuitive data visualization + * Test all CRUD operations thoroughly + +### `/images`, `/uploads` πŸ–ΌοΈπŸ“‚πŸ“Έ + +* Store static images, menu pictures, and user uploads +* Configured in `.gitignore` to exclude from version control +* Use cloud storage (Cloudinary) for production + +### `.github/` πŸ™βš™οΈπŸ“ + +* GitHub Actions workflows for CI/CD +* Issue and PR templates +* Contributing guidelines + +--- + +## 5. Contributor Pathways πŸπŸ‘£πŸŒ± + +### How to pick your journey: + +| Contributor Type | Start Here | Skills Needed | +| -------------------- | --------------------------------------------- | -------------------- | +| Frontend Developer | `/frontend`, UI/UX issues | React, CSS, API integration | +| Backend Developer | `/backend`, API endpoints, database | Node.js, Express, MongoDB | +| Admin Dashboard Dev | `/admin`, management features | React, data visualization | +| Chatbot/AI Dev | `CHATBOT_FEATURE.md`, integration | API integration, NLP | +| DevOps/Docker | Docker configs, deployment | Docker, CI/CD | +| QA/Testing | Test coverage, bug fixes | Jest, testing frameworks | +| Documentation | README updates, API docs | Technical writing | +| Full Stack | Cross-component features | Multiple technologies | + +### Finding issues: πŸ·οΈπŸ”Žβœ¨ + +* Look for labels: `good first issue`, `help wanted`, `documentation`, `bug` +* Check the project board for prioritized tasks +* Ask maintainers for guidance in discussions +* Review closed PRs for contribution examples + +--- + +## 6. Getting Started (Setup & Dev) πŸƒβ€β™‚οΈπŸ’»β© + +### Prerequisites +* Node.js 18+ and npm/yarn +* Docker and Docker Compose (recommended) +* Git +* MongoDB (if running locally) + +### Quick Start + +1. **Clone the repository** + ```bash + git clone https://github.com/yourusername/Foodie.git + cd Foodie + ``` + +2. **Docker Setup (Recommended)** + ```bash + # Copy environment files + cp .env.example .env + cp frontend/.env.example frontend/.env + cp backend/.env.example backend/.env + + # Start all services + docker-compose up --build + ``` + + **Access Points:** + - Frontend: http://localhost:3000 + - Backend API: http://localhost:5000 + - Admin Panel: http://localhost:3001 + - Database: localhost:27017 + +3. **Manual Setup (Development)** + + **Backend:** + ```bash + cd backend + npm install + npm run dev # Uses nodemon for auto-reload + ``` + + **Frontend:** + ```bash + cd frontend + npm install + npm run dev # Vite dev server + ``` + + **Admin:** + ```bash + cd admin + npm install + npm run dev + ``` + +4. **Environment Configuration** + ```bash + # Backend .env + NODE_ENV=development + PORT=5000 + MONGODB_URI=mongodb://localhost:27017/foodie + JWT_SECRET=your_jwt_secret_here + + # Frontend .env + VITE_API_URL=http://localhost:5000/api + VITE_SOCKET_URL=http://localhost:5000 + ``` + +--- + +## 7. Feature How-Tos (Front, Back, Admin, Chatbot) πŸ§©πŸ”¨πŸŒŸ + +### πŸ”Ή **Frontend Development** + +**Adding a new component:** +```javascript +// src/components/NewComponent.jsx +import React from 'react'; +import './NewComponent.css'; + +const NewComponent = ({ prop1, prop2 }) => { + return ( +
+ {/* Component logic */} +
+ ); +}; + +export default NewComponent; +``` + +**API integration:** +```javascript +// src/api/apiService.js +const API_BASE = process.env.VITE_API_URL; + +export const fetchMenuItems = async () => { + const response = await fetch(`${API_BASE}/menu`); + return response.json(); +}; +``` + +### πŸ”Ή **Backend Development** + +**Creating new API endpoints:** +```javascript +// routes/menu.js +const express = require('express'); +const { getMenuItems, createMenuItem } = require('../controllers/menuController'); +const auth = require('../middlewares/auth'); + +const router = express.Router(); + +router.get('/', getMenuItems); +router.post('/', auth, createMenuItem); + +module.exports = router; +``` + +**Database models:** +```javascript +// models/MenuItem.js +const mongoose = require('mongoose'); + +const menuItemSchema = new mongoose.Schema({ + name: { type: String, required: true }, + price: { type: Number, required: true }, + category: { type: String, required: true }, + image: { type: String }, + createdAt: { type: Date, default: Date.now } +}); + +module.exports = mongoose.model('MenuItem', menuItemSchema); +``` + +### πŸ”Ή **Admin Dashboard** + +* Focus on data management interfaces +* Implement proper form validation +* Add confirmation dialogs for destructive actions +* Use charts and analytics components + +### πŸ”Ή **Chatbot Integration** + +* Refer to `CHATBOT_FEATURE.md` for detailed implementation +* Extend conversation flows in backend logic +* Test different user scenarios + +--- + +## 8. API Documentation πŸ“‹πŸ”—πŸ“‘ + +### Base URL +- Development: `http://localhost:5000/api` +- Production: `https://your-domain.com/api` + +### Authentication +All protected routes require JWT token in header: +``` +Authorization: Bearer +``` + +### Core Endpoints + +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| POST | `/auth/register` | User registration | No | +| POST | `/auth/login` | User login | No | +| GET | `/menu` | Get menu items | No | +| POST | `/menu` | Create menu item | Yes (Admin) | +| GET | `/orders` | Get user orders | Yes | +| POST | `/orders` | Create new order | Yes | + +*For complete API documentation, see `/docs` or Postman collection* + +--- + +## 9. Debugging, Testing, and Best Practices πŸ§ͺπŸ“πŸ”¬ + +### Code Quality +* **Linting:** Run `npm run lint` before commits +* **Formatting:** Use Prettier configuration +* **Testing:** Maintain >80% code coverage +* **Type Safety:** Consider TypeScript migration + +### Testing Strategy +```bash +# Backend tests +cd backend +npm test # Run all tests +npm run test:watch # Watch mode +npm run test:coverage # Coverage report + +# Frontend tests +cd frontend +npm test # Jest + React Testing Library +npm run test:e2e # End-to-end tests (if configured) +``` + +### Best Practices +* Write meaningful commit messages +* Keep PRs focused and small (<400 lines) +* Add JSDoc comments for functions +* Update documentation with feature changes +* Follow semantic versioning for releases + +### Common Issues & Solutions +* **CORS errors:** Check backend CORS configuration +* **Environment variables:** Ensure all required vars are set +* **Database connection:** Verify MongoDB is running +* **Port conflicts:** Check if ports 3000, 5000, 3001 are available + +--- + +## 10. Advanced Learning Resources πŸ“šπŸ§‘β€πŸ’»πŸŒ + +### Essential Reading +* [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices) +* [React Patterns](https://reactpatterns.com/) +* [Express.js Guide](https://expressjs.com/en/guide/) +* [MongoDB University](https://university.mongodb.com/) + +### Development Tools +* [Postman](https://www.postman.com/) - API testing +* [MongoDB Compass](https://www.mongodb.com/products/compass) - Database GUI +* [React Developer Tools](https://react.dev/learn/react-developer-tools) +* [Docker Desktop](https://www.docker.com/products/docker-desktop/) + +### Community & Support +* [Stack Overflow - MERN Stack](https://stackoverflow.com/questions/tagged/mern) +* [React Community Discord](https://discord.gg/react) +* [Node.js Community](https://nodejs.org/en/get-involved/) + +--- + +## 11. FAQ β“πŸ—£οΈπŸ™‹β€β™‚οΈ + +**Q: Do I need Docker to contribute?** +A: No, but it's the fastest way to get all services running. You can run each service individually with npm commands. πŸ’‘πŸš„πŸ‘ + +**Q: How do I reset my local database?** +A: Run `docker-compose down -v` to remove volumes, then `docker-compose up --build` to restart fresh. + +**Q: Where are the environment variables?** +A: Check for `.env.example` files in root, frontend, and backend folders. Copy and rename to `.env` with your values. πŸ§πŸ“„πŸ”‘ + +**Q: How can I add new payment methods?** +A: Payment integration is in `backend/services/payment.js`. Follow the existing Stripe pattern for new providers. + +**Q: How do I extend the chatbot?** +A: See `CHATBOT_FEATURE.md` for implementation details. Backend logic is in `backend/services/chatbot.js`. πŸ€–πŸ“„πŸ†˜ + +**Q: Can I use this project commercially?** +A: Check the LICENSE file for specific terms. Generally, open-source licenses allow commercial use with attribution. + +**Q: How do I deploy this to production?** +A: See `DEPLOYMENT.md` for detailed production deployment guides for various platforms. + +**Q: Who do I contact for help?** +A: Open a GitHub Discussion, comment on relevant issues, or join our Discord community. Everyone here started as a beginner! πŸ’¬πŸ€πŸŒ± + +--- + +## πŸš€ **Your Open Source Journey Starts Here!** πŸŒŸπŸ½οΈπŸ™Œ + +Ready to contribute? Here's your action plan: + +1. ⭐ **Star this repository** +2. 🍴 **Fork the project** +3. πŸ“₯ **Clone your fork** +4. 🐳 **Run with Docker** +5. πŸ› **Find an issue to work on** +6. πŸ’» **Code your solution** +7. πŸ§ͺ **Test thoroughly** +8. πŸ“ **Submit a PR** + +Dive in, ask questions, and help make Foodie better for everyone. The community grows stronger with each new contributorβ€”let's build something delicious together! πŸ•βœ¨πŸ‘ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8036231b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Abhishek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b81c7a5a..51f4aed4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,377 @@ -# Foodie +# 🍽️ Foodie – Full-Stack Restaurant App +A full-stack web application for browsing, listing, and managing a variety of food items. Built using React (Frontend), Express.js (Backend), and MongoDB. + +![Foodie Homepage](images/foodie-home-light.png) +Homepage – Light Mode + +--- + +## πŸš€ Quick Navigation + +> **πŸ“š New to Foodie? Complete Developer Guide** +> πŸ‘‰ **[LEARN.md](./LEARN.md)** – Architecture, setup, contribution pathways, and everything you need to get started! + +> **⚑ Want to jump right in?** +> Skip to [Getting Started](#-getting-started) for quick setup instructions. + +--- + +## πŸ“‘ Table of Contents + +* [πŸ”§ Tech Stack](#-tech-stack) + + * [πŸ–₯️ Frontend](#️-frontend) + * [🌐 Backend](#-backend) + * [πŸ—„οΈ Database](#️-database) +* [πŸš€ Getting Started](#-getting-started) + + * [Prerequisites](#prerequisites) + * [πŸ“¦ Installation](#-installation) + * [🐳 Docker Setup (Recommended)](#-docker-setup-recommended) + * [πŸ“¦ Manual Installation](#-manual-installation) + * [πŸ”§ Development Setup](#-development-setup) +* [πŸ“ Project Structure](#-project-structure) +* [🐳 Docker Commands](#-docker-commands) +* [πŸ§ͺ Linting](#-linting) +* [🧰 Scripts](#-scripts) +* [πŸ“ Notes](#-notes) +* [🀝 Contributing](#-contributing) +* [πŸ“„ License](#-license) +* [πŸ”— References](#-references) + +--- + +## πŸ”§ Tech Stack + +### πŸ–₯️ Frontend + +* **React 18.3** – User interface +* **Vite** – Fast build tool and dev server +* **React Router DOM** – Client-side routing +* **ESLint** – Linting and code style enforcement + +### 🌐 Backend + +* **Node.js + Express** – REST API server +* **CORS + JSON Middleware** – Cross-origin requests +* **Multer** – File upload handling +* **Modular API Routing** – Organized route structure + +### πŸ—„οΈ Database + +* **MongoDB** – NoSQL database for data storage + +### 🐳 DevOps + +* **Docker** – Containerization for all services +* **Docker Compose** – Multi-service orchestration + +--- + +## πŸš€ Getting Started + +### Prerequisites + +Ensure you have the following installed: + +**For Docker Setup (Recommended):** + +* Docker Desktop +* Docker Compose + +**For Manual Setup:** + +* Node.js (v16 or above) +* npm or yarn +* MongoDB (local or cloud) + +--- + +### πŸ“¦ Installation + +#### 🐳 Docker Setup (Recommended) + +**One-command setup for the entire application:** + +```bash +# Clone the repository +git clone https://github.com/your-username/foodie.git +cd foodie +npm install + +# Start all services with Docker +docker-compose up --build +``` + +**Access the application:** + +* 🌐 **Frontend**: [http://localhost:3000](http://localhost:3000) +* πŸ› οΈ **Admin Panel**: [http://localhost:5173](http://localhost:5173) +* πŸ”Œ **Backend API**: [http://localhost:4000](http://localhost:4000) +* πŸ—„οΈ **MongoDB**: localhost:27017 + +**Docker Services:** + +* **foodie-frontend**: React app (Port 3000) +* **foodie-admin**: Admin panel (Port 5173) +* **foodie-backend**: Express API (Port 4000) +* **foodie-mongodb**: MongoDB database (Port 27017) + +--- + +#### πŸ“¦ Manual Installation + +```bash +# Clone the repository +git clone https://github.com/your-username/foodie.git +cd foodie + +# Install dependencies for all services +cd frontend && npm install && cd .. +cd backend && npm install && cd .. +cd admin && npm install && cd .. +``` + +--- + +### πŸ”§ Development Setup + +#### Docker Development + +```bash +# Start all services +docker-compose up + +# Start in detached mode +docker-compose up -d + +# View logs for specific service +docker-compose logs frontend +docker-compose logs backend +docker-compose logs admin +``` + +#### Manual Development + +**Start Frontend:** + +```bash +cd frontend +npm run dev +``` + +**Start Admin Panel:** + +```bash +cd admin +npm run dev +``` + +**Start Backend:** + +```bash +cd backend +npm run server +``` + +Server runs on `http://localhost:4000` + +**Start MongoDB:** + +```bash +# Make sure MongoDB is running locally +mongod +``` + +--- + +## πŸ“ Project Structure + +``` +Foodie/ +β”œβ”€β”€ frontend/ # React frontend application +β”‚ β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”œβ”€β”€ .dockerignore +β”‚ └── package.json +β”œβ”€β”€ backend/ # Express.js backend API +β”‚ β”œβ”€β”€ routes/ +β”‚ β”œβ”€β”€ config/ +β”‚ β”œβ”€β”€ uploads/ +β”‚ β”œβ”€β”€ server.js +β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”œβ”€β”€ .dockerignore +β”‚ └── package.json +β”œβ”€β”€ admin/ # React admin panel +β”‚ β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ Dockerfile +β”‚ β”œβ”€β”€ .dockerignore +β”‚ └── package.json +β”œβ”€β”€ docker-compose.yml # Multi-service orchestration +β”œβ”€β”€ .dockerignore # Root Docker ignore file +β”œβ”€β”€ README.md +└── CONTRIBUTING.md +``` + +--- + +## 🐳 Docker Commands + +### Basic Operations + +```bash +# Build and start all services +docker-compose up --build + +# Start services in background +docker-compose up -d + +# Stop all services +docker-compose down + +# Stop and remove volumes (⚠️ deletes database data) +docker-compose down -v + +# Restart specific service +docker-compose restart backend + +# View running containers +docker-compose ps +``` + +### Development Commands + +```bash +# View logs for all services +docker-compose logs + +# View logs for specific service +docker-compose logs -f frontend + +# Execute commands in running container +docker-compose exec backend npm install new-package + +# Rebuild specific service +docker-compose build backend +``` + +### Database Management + +```bash +# Access MongoDB shell +docker-compose exec mongodb mongosh + +# Backup database +docker-compose exec mongodb mongodump --out /backup + +# View MongoDB logs +docker-compose logs mongodb +``` + +--- + +## πŸ§ͺ Linting + +ESLint is pre-configured with React and Hooks rules for frontend and admin. + +```bash +# Frontend linting +cd frontend && npm run lint + +# Admin linting +cd admin && npm run lint +``` + +--- + +## 🧰 Scripts + +### Frontend & Admin Scripts + +| Command | Description | +| ----------------- | ----------------------------- | +| `npm run dev` | Start Vite development server | +| `npm run build` | Build for production | +| `npm run preview` | Preview production build | +| `npm run lint` | Run ESLint checks | + +### Backend Scripts + +| Command | Description | +| ---------------- | ------------------------------------- | +| `npm start` | Start production server | +| `npm run server` | Start development server with nodemon | + +--- + +## πŸ“ Notes + +* Make sure MongoDB is running locally or update `connectDB()` in `config/db.js` accordingly. +* You can update the backend routes via `routes/foodRoute.js`. + +### Environment Variables + +The application uses the following environment variables: + +**Backend:** + +* `MONGODB_URI`: MongoDB connection string +* `JWT_SECRET`: Secret key for JWT tokens +* `PORT`: Server port (default: 4000) + +**Frontend:** + +* `REACT_APP_API_URL`: Backend API URL + +**Admin:** + +* `VITE_API_URL`: Backend API URL for Vite + +### Database Configuration + +* **Docker**: MongoDB runs automatically with authentication + + * Username: `admin` + * Password: `password123` + * Database: `foodie` +* **Manual**: Update `connectDB()` in `backend/config/db.js` + +### File Uploads + +* Backend handles file uploads via Multer +* Files are stored in `backend/uploads/` directory +* Docker setup includes volume mounting for persistence + +--- + +## 🀝 Contributing + +We welcome contributions to the Foodie project! If you find this project helpful, consider starring the repo or opening an issue. + +* πŸ“– Help improve documentation +* πŸš€ For more info go to [CONTRIBUTING.md](CONTRIBUTING.md) + +### Development Workflow + +1. Fork the repository +2. Create a feature branch +3. Use Docker for consistent development environment +4. Test your changes with `docker-compose up --build` +5. Submit a pull request + +--- + +## πŸ“„ License + +This project is licensed under the MIT License. + +--- + +## πŸ”— References + +* [React](https://reactjs.org/) +* [Vite](https://vitejs.dev/) +* [Express](https://expressjs.com/) +* [MongoDB](https://www.mongodb.com/) +* [Docker](https://www.docker.com/) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..3dc70ef6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,57 @@ +# πŸ” Security Policy + +## Supported Versions + +We actively maintain the latest version of the **Foodie** project. Please ensure you're always using the most recent release to benefit from the latest features and security updates. + +| Version | Supported | +|---------|-----------| +| latest | βœ… | +| older | ❌ | + +--- + +## πŸ“’ Reporting a Vulnerability + +If you discover a security vulnerability in this project, **do not open an issue directly on GitHub**. + +Instead, please follow these steps: + +1. **Contact us directly** via email. + +2. Provide a clear and detailed description of the issue, including: + - Steps to reproduce the vulnerability + - The impact it could have + - Any recommended fixes (if available) +3. Allow us a reasonable time to investigate and fix the vulnerability before disclosing it publicly. + +--- + +## πŸ” Security Best Practices + +To help keep the Foodie app and its users secure, please follow these best practices: + +- Never share sensitive credentials or API keys in public repositories. +- Use environment variables to store secrets and tokens. +- Always run the latest version of dependencies. +- Perform regular audits using tools like `npm audit` or `yarn audit`. + +--- + +## πŸ›‘οΈ Scope of Security + +This security policy applies to: + +- The source code in this repository +- Configuration files +- Dependencies defined in `package.json` + +**Note:** Issues related to third-party services or APIs used by the app (e.g., payment gateways, external APIs) should be reported to those service providers. + +--- + +## 🀝 Responsible Disclosure + +We appreciate responsible disclosure of any vulnerability. All security reports will be acknowledged and investigated promptly. We are committed to maintaining a secure and safe environment for all contributors and users. + +Thank you for helping keep **Foodie** secure! 🍽️ diff --git a/admin/.dockerignore b/admin/.dockerignore new file mode 100644 index 00000000..15831c25 --- /dev/null +++ b/admin/.dockerignore @@ -0,0 +1,95 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Production build +dist +dist-ssr +build + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +*.local + +# IDE files +.vscode/* +!.vscode/extensions.json +.idea +*.swp +*.swo +*~ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore +README.md + +# Testing +coverage +.nyc_output + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# ESLint cache +.eslintcache + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Yarn Integrity file +.yarn-integrity + +# Docker +Dockerfile +.dockerignore + +# Documentation +*.md +docs/ + +# Cache directories +.cache +.parcel-cache + +# ESLint config (if you want to exclude it) +eslint.config.js + +# Vite config (optional - exclude if not needed in production) +vite.config.js + +# Lock files (we copy package*.json separately) +package-lock.json +yarn.lock \ No newline at end of file diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/admin/Dockerfile b/admin/Dockerfile new file mode 100644 index 00000000..563ee005 --- /dev/null +++ b/admin/Dockerfile @@ -0,0 +1,26 @@ +# Use Node.js Alpine image for lightweight container +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package.json and package-lock.json (if available) +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Install serve to serve the built app +RUN npm install -g serve + +# Expose port 5173 (default Vite preview port) +EXPOSE 5173 + +# Command to serve the built application +CMD ["serve", "-s", "dist", "-l", "5173"] \ No newline at end of file diff --git a/admin/README.md b/admin/README.md new file mode 100644 index 00000000..7059a962 --- /dev/null +++ b/admin/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/admin/eslint.config.js b/admin/eslint.config.js new file mode 100644 index 00000000..cee1e2c7 --- /dev/null +++ b/admin/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 00000000..725e6f4c --- /dev/null +++ b/admin/index.html @@ -0,0 +1,13 @@ + + + + + + + Admin Panel + + +
+ + + diff --git a/admin/package-lock.json b/admin/package-lock.json new file mode 100644 index 00000000..18f0979f --- /dev/null +++ b/admin/package-lock.json @@ -0,0 +1,3134 @@ +{ + "name": "admin", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "admin", + "version": "0.0.0", + "dependencies": { + "axios": "^1.11.0", + "react": "^19.1.0", + "react-router-dom": "^7.7.1", + "react-toastify": "^11.0.5" + }, + "devDependencies": { + "@eslint/js": "^9.30.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "eslint": "^9.30.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "vite": "^7.0.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "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/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.191", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", + "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", + "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/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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/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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz", + "integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.1.tgz", + "integrity": "sha512-bavdk2BA5r3MYalGKZ01u8PGuDBloQmzpBZVhDLrOOv1N943Wq6dcM9GhB3x8b7AbqPMEezauv4PeGkAJfy7FQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.7.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 00000000..95067b10 --- /dev/null +++ b/admin/package.json @@ -0,0 +1,30 @@ +{ + "name": "admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview", + "start": "serve -s dist -l 5173" + }, + "dependencies": { + "axios": "^1.11.0", + "react": "^19.1.0", + "react-router-dom": "^7.7.1", + "react-toastify": "^11.0.5" + }, + "devDependencies": { + "@eslint/js": "^9.30.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "eslint": "^9.30.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "vite": "^7.0.4" + } +} diff --git a/admin/public/vite.svg b/admin/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/admin/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/src/App.jsx b/admin/src/App.jsx new file mode 100644 index 00000000..5377b0be --- /dev/null +++ b/admin/src/App.jsx @@ -0,0 +1,28 @@ +import Navbar from './components/Navbar/Navbar' +import Sidebar from './components/Sidebar/Sidebar' +import {Routes, Route} from 'react-router-dom' +import Add from './pages/Add/Add' +import List from './pages/List/List' +import Orders from './pages/Orders/Orders' +import {ToastContainer} from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css' +const App = () => { + const url = 'http://localhost:4000/'; + return ( +
+ + +
+
+ + + }> + }> + }> + +
+
+ ) +} + +export default App diff --git a/admin/src/assets/add_icon.png b/admin/src/assets/add_icon.png new file mode 100644 index 00000000..9646c157 Binary files /dev/null and b/admin/src/assets/add_icon.png differ diff --git a/admin/src/assets/assets.js b/admin/src/assets/assets.js new file mode 100644 index 00000000..1cd3c67b --- /dev/null +++ b/admin/src/assets/assets.js @@ -0,0 +1,17 @@ +import logo from './logo.png' +import add_icon from './add_icon.png' +import order_icon from './order_icon.png' +import profile_image from './profile_image.png' +import upload_area from './upload_area.png' +import parcel_icon from './parcel_icon.png' + +export const assets ={ + logo, + add_icon, + order_icon, + profile_image, + upload_area, + parcel_icon +} + +export const url = 'http://localhost:4000' \ No newline at end of file diff --git a/admin/src/assets/logo.png b/admin/src/assets/logo.png new file mode 100644 index 00000000..267f602f Binary files /dev/null and b/admin/src/assets/logo.png differ diff --git a/admin/src/assets/order_icon.png b/admin/src/assets/order_icon.png new file mode 100644 index 00000000..6ba4422e Binary files /dev/null and b/admin/src/assets/order_icon.png differ diff --git a/admin/src/assets/parcel_icon.png b/admin/src/assets/parcel_icon.png new file mode 100644 index 00000000..2bbeb327 Binary files /dev/null and b/admin/src/assets/parcel_icon.png differ diff --git a/admin/src/assets/profile_image.png b/admin/src/assets/profile_image.png new file mode 100644 index 00000000..57295775 Binary files /dev/null and b/admin/src/assets/profile_image.png differ diff --git a/admin/src/assets/upload_area.png b/admin/src/assets/upload_area.png new file mode 100644 index 00000000..8c20a11c Binary files /dev/null and b/admin/src/assets/upload_area.png differ diff --git a/admin/src/components/Navbar/Navbar.css b/admin/src/components/Navbar/Navbar.css new file mode 100644 index 00000000..dc5fee97 --- /dev/null +++ b/admin/src/components/Navbar/Navbar.css @@ -0,0 +1,21 @@ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 4%; +} + +.navbar .logo { + width : max(10%, 80px); + +} + +.navbar .profile { + width: 40px; +} + +.brand-text { + color: black; + font-size: 20px; + margin: 0 10px; +} diff --git a/admin/src/components/Navbar/Navbar.jsx b/admin/src/components/Navbar/Navbar.jsx new file mode 100644 index 00000000..a0a9310b --- /dev/null +++ b/admin/src/components/Navbar/Navbar.jsx @@ -0,0 +1,15 @@ +import './Navbar.css' +import {assets} from '../../assets/assets' + + +const Navbar = () => { + return ( +
+ +

Welcome to Foodie!

+ +
+ ) +} + +export default Navbar diff --git a/admin/src/components/Sidebar/Sidebar.css b/admin/src/components/Sidebar/Sidebar.css new file mode 100644 index 00000000..09f3128d --- /dev/null +++ b/admin/src/components/Sidebar/Sidebar.css @@ -0,0 +1,37 @@ +.sidebar { + width : 18%; + min-height: 100vh; + border: 1.5px solid #a9a9a9; + border-top: 0; + font-size: max(1vw, 10px); +} + +.sidebar-options { + padding-top: 50px; + padding-left: 20%; + display: flex; + flex-direction: column; + gap: 20px; +} + +.sidebar-option { + display: flex; + align-items: center; + gap: 12px; + border: 1px solid #a9a9a9; + border-right: 0; + padding: 8px 10px; + border-radius: 3px 0 3px 0; + cursor: pointer; +} + +.sidebar-option.active { + background-color: #fff0ed; + border: tomato; +} + +@media (max-width : 900px) { + .sidebar-option p{ + display: none; + } +} \ No newline at end of file diff --git a/admin/src/components/Sidebar/Sidebar.jsx b/admin/src/components/Sidebar/Sidebar.jsx new file mode 100644 index 00000000..cc7ad941 --- /dev/null +++ b/admin/src/components/Sidebar/Sidebar.jsx @@ -0,0 +1,27 @@ +import React from 'react' +import './Sidebar.css' +import {assets} from '../../assets/assets' +import { NavLink } from 'react-router-dom' +const Sidebar = () => { + return ( +
+
+ + +

Add Items

+
+ + +

List Items

+
+ + +

Orders

+
+
+ +
+ ) +} + +export default Sidebar diff --git a/admin/src/index.css b/admin/src/index.css new file mode 100644 index 00000000..fe783742 --- /dev/null +++ b/admin/src/index.css @@ -0,0 +1,35 @@ +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap'); + +* { + padding : 0; + margin : 0; + box-sizing : border-box; + font-family: Outfit; +} + +body { + min-height: 100vh; + background-color: #fcfcfc; + +} + +a { + text-decoration : none; + color : inherit; +} + +hr { + border : none; + height : 1px; + background-color: #a9a9a9; +} + +.app-content { + display: flex; +} + +.flex-col { + display: flex; + flex-direction: column; + gap: 10px; +} \ No newline at end of file diff --git a/admin/src/main.jsx b/admin/src/main.jsx new file mode 100644 index 00000000..3ab2ca2c --- /dev/null +++ b/admin/src/main.jsx @@ -0,0 +1,12 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' +import {BrowserRouter} from 'react-router-dom' + + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/admin/src/pages/Add/Add.css b/admin/src/pages/Add/Add.css new file mode 100644 index 00000000..7b6209f8 --- /dev/null +++ b/admin/src/pages/Add/Add.css @@ -0,0 +1,63 @@ +/* Add.css */ + +.add { + width: 70%; + margin-left: max(5vw, 25px); + margin-top: 50px; + color: #6d6d6d; + font-size: 16px; +} + +/* This adds vertical space between all form elements */ +.add form { + display: flex; + flex-direction: column; + gap: 20px; /* This will now work correctly */ +} + +.add-img-upload img { + width: 120px; + cursor: pointer; +} + +.add-product-name, .add-product-description { + width: max(40%, 280px); +} + +.add-product-name input, .add-product-description textarea { + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; +} + +/* This is the new rule to place category and price side-by-side */ +.add-category-price { + display: flex; + flex-direction: row; /* Aligns children horizontally */ + gap: 30px; /* Adds space between category and price */ + align-items: flex-start; +} + +.add-category select, .add-price input { + max-width: 120px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.add-btn { + max-width: 120px; + border: none; + padding: 10px; + background-color: black; + color: white; + cursor: pointer; + border-radius: 4px; +} + +/* Optional: if your flex-col class is not defined elsewhere */ +.flex-col { + display: flex; + flex-direction: column; + gap: 10px; +} diff --git a/admin/src/pages/Add/Add.jsx b/admin/src/pages/Add/Add.jsx new file mode 100644 index 00000000..2d74ec82 --- /dev/null +++ b/admin/src/pages/Add/Add.jsx @@ -0,0 +1,98 @@ +import { assets } from '../../assets/assets'; // Adjust path if needed +import './Add.css'; // Make sure to import the CSS file +import { useState, useEffect } from 'react'; +import axios from 'axios'; +import { toast } from 'react-toastify'; // Ensure you have react-hot-toast installed for +const Add = ({url}) => { + const [image, setImage] = useState(false); + const [data, setData] = useState({ + name: '', + description: '', + category: 'Salad', + price: '' + }) + + useEffect(() => { + console.log(data) + }, [data]) + + const onChangeHandler = (e) => { + const name = e.target.name; + const value = e.target.value; + setData(data => ({...data, [name]: value })); + } + + const onSubmitHandler = async (e) => { + e.preventDefault(); + const formData = new FormData(); + formData.append('image', image); + formData.append('name', data.name); + formData.append('description', data.description); + formData.append('category', data.category); + formData.append('price', Number(data.price)); + + const response = await axios.post(`${url}/api/food/add`, formData); + if(response.data.success) { + toast.success(response.data.message); + setData({ + name: '', + description: '', + category: 'Salad', + price: '' + }); + setImage(false); + }else { + toast.error(response.data.message); + } + } + return ( +
+
+ {/* The extra
wrapper has been removed from here */} + +
+

Upload Image

+ + setImage(e.target.files[0])} type="file" id="image" hidden required /> +
+ +
+

Product Name

+ +
+ +
+

Product Description

+ + + +
+ ); +}; + +export default ContactForm; diff --git a/frontend/src/components/ExploreMenu/ExploreMenu.css b/frontend/src/components/ExploreMenu/ExploreMenu.css index f9a3c218..b77f2726 100644 --- a/frontend/src/components/ExploreMenu/ExploreMenu.css +++ b/frontend/src/components/ExploreMenu/ExploreMenu.css @@ -1,66 +1,402 @@ -.explore-menu{ - margin-top: 3vh; - display: flex; - flex-direction: column; - gap: 20px; - /* width: 90%; */ +/* Enhanced ExploreMenu.css */ +.explore-menu { + margin-top: 5vh; + display: flex; + flex-direction: column; + gap: 30px; + padding: 0 20px; + margin: 50px; + position: relative; + /* background-color: ; */ +} + +@media (max-width: 768px) { + .explore-menu { + margin: 0; + } +} + + +.explore-menu h1 { + color: var(--text-color, #262626); + font-weight: 700; + font-size: clamp(2rem, 4vw, 3.5rem); + text-align: center; + margin-bottom: 10px; + background: linear-gradient(135deg, var(--accent-color, #ff6347), #ff8c69); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + position: relative; +} + +.explore-menu h1::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 80px; + height: 4px; + background: linear-gradient(90deg, var(--accent-color, #ff6347), #ff8c69); + border-radius: 2px; +} + +.explore-menu-text { + position: relative; + max-width: 70%; + margin: 0 auto; /* center horizontally */ + color: var(--text-secondary, #808080); + font-size: clamp(14px, 1.2vw, 18px); + line-height: 1.6; + text-align: center; + font-weight: 400; +} + + +.explore-menu-wrapper { + border-radius: 25px; + overflow: hidden; + position: relative; + width: 100%; + background: var(--glass-bg, rgba(255, 255, 255, 0.1)); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.2)); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; +} + +.explore-menu-wrapper:hover { + transform: translateY(-2px); + box-shadow: + 0 12px 40px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.3); +} + +.explore-menu-list { + display: flex; + align-items: center; + gap: 40px; + text-align: center; + margin: 0; + overflow-x: hidden; + scroll-behavior: smooth; + padding: 30px 80px; + position: relative; + background: linear-gradient(90deg, + transparent 0%, + var(--menu-gradient, rgba(255, 99, 71, 0.02)) 20%, + var(--menu-gradient, rgba(255, 99, 71, 0.02)) 80%, + transparent 100% + ); +} + +.explore-menu-list::-webkit-scrollbar { + display: none; +} + +.explore-menu-list-item { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; + padding: 20px; + border-radius: 20px; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + cursor: pointer; + position: relative; + background: var(--item-bg, rgba(255, 255, 255, 0.05)); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border: 1px solid var(--item-border, rgba(255, 255, 255, 0.1)); +} + +.explore-menu-list-item::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, + rgba(255, 99, 71, 0.1) 0%, + rgba(255, 140, 105, 0.05) 100% + ); + border-radius: 20px; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 0; +} + +.explore-menu-list-item:hover::before { + opacity: 1; +} + +.explore-menu-list-item:hover { + transform: translateY(-8px) scale(1.05); + box-shadow: + 0 15px 35px rgba(255, 99, 71, 0.2), + 0 5px 15px rgba(0, 0, 0, 0.1); + background: var(--item-hover-bg, rgba(255, 255, 255, 0.1)); +} + +.explore-menu-list-item img { + width: clamp(70px, 7vw, 100px); + height: clamp(70px, 7vw, 100px); + border-radius: 50%; + object-fit: cover; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + border: 3px solid transparent; + background: linear-gradient(white, white) padding-box, + linear-gradient(135deg, var(--accent-color, #ff6347), #ff8c69) border-box; + position: relative; + z-index: 1; +} + +.explore-menu-list-item img.active { + border: 4px solid var(--accent-color, #ff6347); + box-shadow: + 0 0 0 4px rgba(255, 99, 71, 0.2), + 0 8px 25px rgba(255, 99, 71, 0.3); + transform: scale(1.1); +} + +.explore-menu-list-item img:hover { + transform: scale(1.1) rotate(5deg); + box-shadow: 0 10px 30px rgba(255, 99, 71, 0.3); +} + +.explore-menu-list-item p { + margin: 0; + font-size: clamp(14px, 1.2vw, 16px); + color: var(--text-color, #747474); + font-weight: 600; + text-transform: capitalize; + letter-spacing: 0.5px; + position: relative; + z-index: 1; + transition: all 0.3s ease; +} + +.explore-menu-list-item:hover p { + color: var(--accent-color, #ff6347); + transform: translateY(-2px); +} + +/* Enhanced Arrow Styling */ +.arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: var(--arrow-bg, rgba(255, 255, 255, 0.9)); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 2px solid var(--arrow-border, rgba(255, 99, 71, 0.2)); + border-radius: 50%; + width: 55px; + height: 55px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 20; + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + font-size: 20px; + font-weight: bold; + color: var(--accent-color, #ff6347); + box-shadow: + 0 4px 15px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.5); } -.explore-menu h1{ - color: #262626; - font-weight: 500; +.arrow:hover { + background: var(--accent-color, #ff6347); + color: white; + transform: translateY(-50%) scale(1.1); + box-shadow: + 0 8px 25px rgba(255, 99, 71, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + border-color: var(--accent-color, #ff6347); } -.explore-menu-text{ - max-width: 60%; - color: #808080; +.arrow:active { + transform: translateY(-50%) scale(0.95); } -.explore-menu-list{ - display: flex; - justify-content: space-between; - align-items: center; - gap: 58px; - text-align: center; - margin: 20px 0px; - overflow-x: scroll; +.arrow.left { + left: 15px; +} + +.arrow.right { + right: 15px; +} +/* HR Styling */ +.explore-menu hr { + margin: 40px auto 20px; + height: 1px; + width: 80%; + background: linear-gradient(90deg, + transparent 0%, + var(--accent-color, #ff6347) 20%, + var(--accent-color, #ff6347) 80%, + transparent 100% + ); + border: none; + opacity: 0.3; } -.explore-menu-list::-webkit-scrollbar{ - display: none; +/* Animation for continuous scroll */ +.explore-menu-list.scrolling { + animation: none; } -.explore-menu-list-item img{ - width: 7.5vw; - min-width: 80px; - cursor: pointer; - border-radius: 50%; - transition: 0.7s; +/* Responsive Design */ +@media (max-width: 1200px) { + .explore-menu-list { + gap: 30px; + padding: 25px 70px; + } + + .arrow { + width: 50px; + height: 50px; + font-size: 18px; + } } -.explore-menu-list-item p{ - margin-top: 10px; - font-size: max(1.4vw,16px); - color: #747474; - cursor: pointer; +@media (max-width: 1050px) { + .explore-menu-text { + max-width: 85%; + font-size: 16px; + } + + .explore-menu-list { + gap: 25px; + padding: 20px 60px; + } + + .explore-menu-list-item { + padding: 15px; + } } -.explore-menu hr{ - margin: 10px 0px; - height: 2px; - background-color: #e2e2e2; - border: none; +@media (max-width: 768px) { + .explore-menu { + padding: 0 15px; + gap: 25px; + } + + .explore-menu-text { + max-width: 95%; + } + + .explore-menu-wrapper { + border-radius: 20px; + } + + .explore-menu-list { + gap: 20px; + padding: 20px 50px; + } + + .explore-menu-list-item { + padding: 12px; + gap: 10px; + } + + .arrow { + width: 45px; + height: 45px; + font-size: 16px; + } + + .arrow.left { + left: 10px; + } + + .arrow.right { + right: 10px; + } +} + +@media (max-width: 480px) { + .explore-menu-list { + padding: 15px 40px; + gap: 15px; + } + + .explore-menu-list-item { + padding: 10px; + gap: 8px; + } + + .arrow { + width: 40px; + height: 40px; + font-size: 14px; + } +} + +/* Dark Theme Variables */ +[data-theme="dark"] { + --glass-bg: rgba(45, 55, 72, 0.3); + --glass-border: rgba(255, 255, 255, 0.1); + --menu-gradient: rgba(255, 99, 71, 0.05); + --item-bg: rgba(255, 255, 255, 0.02); + --item-border: rgba(255, 255, 255, 0.05); + --item-hover-bg: rgba(255, 255, 255, 0.05); + --arrow-bg: rgba(45, 55, 72, 0.9); + --arrow-border: rgba(255, 99, 71, 0.3); + --text-secondary: #a0aec0; +} + +/* Loading animation for items */ +.explore-menu-list-item { + animation: fadeInUp 0.6s ease-out forwards; + opacity: 0; + transform: translateY(20px); +} + +.explore-menu-list-item:nth-child(1) { animation-delay: 0.1s; } +.explore-menu-list-item:nth-child(2) { animation-delay: 0.2s; } +.explore-menu-list-item:nth-child(3) { animation-delay: 0.3s; } +.explore-menu-list-item:nth-child(4) { animation-delay: 0.4s; } +.explore-menu-list-item:nth-child(5) { animation-delay: 0.5s; } +.explore-menu-list-item:nth-child(6) { animation-delay: 0.6s; } +.explore-menu-list-item:nth-child(7) { animation-delay: 0.7s; } +.explore-menu-list-item:nth-child(8) { animation-delay: 0.8s; } + +@keyframes fadeInUp { + to { + opacity: 1; + transform: translateY(0); + } } -.explore-menu-list-item .active{ - border: 4px solid tomato; - padding: 2px; +/* Pulse effect for active items */ +.explore-menu-list-item img.active { + animation: pulse 2s infinite; } -@media (max-width:1050px){ - .explore-menu-text{ - max-width: 100%; - font-size: 14px; - } +@keyframes pulse { + 0% { + box-shadow: + 0 0 0 4px rgba(255, 99, 71, 0.2), + 0 8px 25px rgba(255, 99, 71, 0.3); + } + 50% { + box-shadow: + 0 0 0 8px rgba(255, 99, 71, 0.1), + 0 12px 35px rgba(255, 99, 71, 0.4); + } + 100% { + box-shadow: + 0 0 0 4px rgba(255, 99, 71, 0.2), + 0 8px 25px rgba(255, 99, 71, 0.3); + } } \ No newline at end of file diff --git a/frontend/src/components/ExploreMenu/ExploreMenu.jsx b/frontend/src/components/ExploreMenu/ExploreMenu.jsx index 5b4441de..f7feedc0 100644 --- a/frontend/src/components/ExploreMenu/ExploreMenu.jsx +++ b/frontend/src/components/ExploreMenu/ExploreMenu.jsx @@ -1,25 +1,246 @@ -import React from 'react' -import './ExploreMenu.css' -import { menu_list } from '../../assets/frontend_assets/assets' +import React, { useRef, useEffect, useState, useCallback } from "react"; +import "./ExploreMenu.css"; +import { menu_list } from "../../assets/frontend_assets/assets"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +const ExploreMenu = ({ category, setCategory }) => { + const scrollRef = useRef(null); + const animationRef = useRef(null); + const [isPaused, setIsPaused] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const pauseTimeoutRef = useRef(null); + + // Auto-scroll function using requestAnimationFrame for smooth animation + const autoScroll = () => { + if (isPaused || isHovered || !scrollRef.current) { + return; // Stop animation completely while hovered/paused + } + + const container = scrollRef.current; + const scrollSpeed = 0.5; // Pixels per frame + + // Move forward + container.scrollLeft += scrollSpeed; + + // Calculate when to reset with better precision + const scrollWidth = container.scrollWidth; + const halfWidth = scrollWidth / 2; + const resetBuffer = 20; // Small buffer to ensure clean reset + + // Reset when we've scrolled past the first set (with buffer) + if (container.scrollLeft >= halfWidth - resetBuffer) { + container.scrollLeft = 0; + console.log("Auto-scroll reset triggered"); // Debug log + } + + // Continue the animation + animationRef.current = requestAnimationFrame(autoScroll); + }; + + // Manual scroll with arrows + const handleManualScroll = (direction) => { + console.log(`Button clicked: ${direction}`); // Debug log + + if (!scrollRef.current) { + console.log("No scrollRef available"); // Debug log + return; + } + + const container = scrollRef.current; + const scrollAmount = 300; + + // Calculate safe boundaries to avoid reset issues + const scrollWidth = container.scrollWidth; + const halfWidth = scrollWidth / 2; + const safeZone = 100; // Buffer to stay away from reset boundary + + console.log(`Current scroll position: ${container.scrollLeft}`); // Debug log + console.log(`Half width (reset point): ${halfWidth}`); // Debug log + + // Stop auto-scroll first + setIsPaused(true); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + + // Calculate new position + let newPosition; + if (direction === "left") { + newPosition = container.scrollLeft - scrollAmount; + // If going too far left, wrap to safe position before reset point + if (newPosition < 0) { + newPosition = halfWidth - safeZone - scrollAmount; + } + } else { + newPosition = container.scrollLeft + scrollAmount; + // If getting too close to reset point, wrap to beginning + if (newPosition >= halfWidth - safeZone) { + newPosition = 0; + } + } + + // Apply the safe scroll position + container.scrollLeft = newPosition; + + console.log(`New scroll position: ${container.scrollLeft}`); // Debug log + + // Clear existing timeout + if (pauseTimeoutRef.current) { + clearTimeout(pauseTimeoutRef.current); + } + + // Resume after 1 second + pauseTimeoutRef.current = setTimeout(() => { + console.log("Resuming auto-scroll"); // Debug log + // Normalize position before resuming to ensure we're in a safe zone + if (scrollRef.current) { + const currentPos = scrollRef.current.scrollLeft; + const resetPoint = scrollRef.current.scrollWidth / 2; + if (currentPos >= resetPoint - 50) { + scrollRef.current.scrollLeft = 0; + console.log("Normalized position before resuming"); // Debug log + } + } + setIsPaused(false); + }, 900); + }; + + // Hover handlers + const handleMouseEnter = () => { + console.log("Mouse entered - pausing scroll"); + setIsHovered(true); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); // Stop animation on hover + } + }; + + const handleMouseLeave = () => { + console.log("Mouse left - resuming scroll"); + setIsHovered(false); + if (!isPaused) { + animationRef.current = requestAnimationFrame(autoScroll); // Resume animation after hover + } + }; + + // Start auto-scroll on mount + useEffect(() => { + animationRef.current = requestAnimationFrame(autoScroll); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + if (pauseTimeoutRef.current) { + clearTimeout(pauseTimeoutRef.current); + } + }; + }, []); + + // Restart animation when pause state changes + useEffect(() => { + if (!isPaused && !isHovered) { + animationRef.current = requestAnimationFrame(autoScroll); + } + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [isPaused, isHovered]); + -const ExploreMenu = ({category,setCategory}) => { return ( -
+

Explore Our Menu

-

Choose from a diverse menu featuring a delectable array of dishes. Our mission is to satisfy your cravings and elevate your dining experience, one delicious meal at a time

-
- {menu_list.map((item,index)=>{ - return ( -
setCategory(category===item.menu_name ? "All":item.menu_name)} key={index} className="explore-menu-list-item"> - -

{item.menu_name}

-
- ) - })} +

+ Choose from a diverse menu featuring a delectable array of dishes crafted with passion. + Our mission is to satisfy your cravings and elevate your dining experience, + one delicious meal at a time. +

+ +
+ + +
+ {/* Triple the items for seamless infinite scroll */} + {[...menu_list, ...menu_list, ...menu_list].map((item, index) => ( +
+ setCategory( + category === item.menu_name ? "All" : item.menu_name + ) + } + key={`${item.menu_name}-${index}`} + className="explore-menu-list-item" + > + {item.menu_name} +

{item.menu_name}

+
+ ))} +
+ +
+
- ) -} + ); +}; -export default ExploreMenu +export default ExploreMenu; \ No newline at end of file diff --git a/frontend/src/components/FAQ/FAQ.css b/frontend/src/components/FAQ/FAQ.css new file mode 100644 index 00000000..ba1f5686 --- /dev/null +++ b/frontend/src/components/FAQ/FAQ.css @@ -0,0 +1,288 @@ +.faq-section { + background: var(--bg-color); + padding: 60px 8vw; + /* margin-top: 60px; */ + color: var(--text-color); + border-top: 1px solid #e5e5e5; +} + +[data-theme='dark'] .faq-section { + border-top: 1px solid #333; +} + +.faq-container { + max-width: 800px; + margin: 0 auto; +} + +.faq-header { + text-align: center; + margin-bottom: 40px; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.faq-icon { + color: #ff6b6b; + margin-bottom: 10px; +} + +.faq-header h2 { + font-size: 2rem; + font-weight: 700; + margin: 0; + color: var(--title-color); +} + +.faq-header p { + font-size: 1rem; + opacity: 0.8; + color: var(--text-color); + margin: 0; +} + +.faq-search { + margin-bottom: 30px; +} + +.search-input-container { + position: relative; + max-width: 400px; + margin: 0 auto; +} + +.search-icon { + position: absolute; + left: 15px; + top: 50%; + transform: translateY(-50%); + color: #666; + z-index: 1; +} + +.faq-search-input { + width: 100%; + padding: 12px 15px 12px 45px; + border: 2px solid #e5e5e5; + border-radius: 25px; + font-size: 0.95rem; + background: var(--bg-color); + color: var(--text-color); + transition: all 0.3s ease; +} + +[data-theme='dark'] .faq-search-input { + border-color: #333; + background: #1a1a1a; +} + +.faq-search-input:focus { + outline: none; + border-color: #ff6b6b; + box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.1); +} + +.faq-search-input::placeholder { + color: #999; +} + +.faq-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.faq-item { + background: var(--bg-color); + border: 1px solid #e5e5e5; + border-radius: 8px; + overflow: hidden; + transition: all 0.3s ease; +} + +[data-theme='dark'] .faq-item { + border-color: #333; + background: #1a1a1a; +} + +.faq-item:hover { + border-color: #ff6b6b; + box-shadow: 0 2px 8px rgba(255, 107, 107, 0.1); +} + +.faq-question { + padding: 20px 25px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.3s ease; + background: var(--bg-color); +} + +.faq-question:hover { + background: rgba(255, 107, 107, 0.05); +} + +.faq-question h3 { + font-size: 1rem; + font-weight: 600; + color: var(--text-color); + margin: 0; + flex: 1; + padding-right: 20px; + line-height: 1.4; +} + +.faq-toggle { + color: #ff6b6b; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.faq-answer { + max-height: 0; + overflow: hidden; + transition: all 0.3s ease; + background: rgba(255, 107, 107, 0.02); +} + +[data-theme='dark'] .faq-answer { + background: rgba(255, 107, 107, 0.05); +} + +.faq-answer.expanded { + max-height: 150px; + padding: 0 25px 20px 25px; +} + +.faq-answer p { + margin: 0; + line-height: 1.5; + color: var(--text-color); + font-size: 0.95rem; + opacity: 0.9; +} + +.no-results { + text-align: center; + padding: 30px 20px; + background: rgba(255, 107, 107, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 107, 107, 0.2); +} + +.no-results p { + margin: 8px 0; + color: var(--text-color); + font-size: 0.95rem; + opacity: 0.8; +} + +.faq-footer { + text-align: center; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #e5e5e5; +} + +[data-theme='dark'] .faq-footer { + border-top-color: #333; +} + +.faq-footer p { + color: var(--text-color); + font-size: 0.9rem; + margin: 0; + opacity: 0.8; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .faq-section { + padding: 40px 5vw; + } + + .faq-header h2 { + font-size: 1.8rem; + } + + .faq-header p { + font-size: 0.9rem; + } + + .faq-question { + padding: 18px 20px; + } + + .faq-question h3 { + font-size: 0.95rem; + } + + .faq-answer.expanded { + padding: 0 20px 18px 20px; + } + + .faq-toggle { + width: 20px; + height: 20px; + } +} + +@media (max-width: 480px) { + .faq-section { + padding: 30px 4vw; + } + + .faq-header h2 { + font-size: 1.6rem; + } + + .faq-search-input { + padding: 10px 10px 10px 40px; + font-size: 0.9rem; + } + + .search-icon { + width: 18px; + height: 18px; + left: 12px; + } +} + +/* Animation for smooth transitions */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.faq-item { + animation: fadeIn 0.4s ease-out; +} + +/* Custom scrollbar for FAQ answers */ +.faq-answer::-webkit-scrollbar { + width: 4px; +} + +.faq-answer::-webkit-scrollbar-track { + background: rgba(255, 107, 107, 0.1); + border-radius: 2px; +} + +.faq-answer::-webkit-scrollbar-thumb { + background: rgba(255, 107, 107, 0.3); + border-radius: 2px; +} + +.faq-answer::-webkit-scrollbar-thumb:hover { + background: rgba(255, 107, 107, 0.5); +} \ No newline at end of file diff --git a/frontend/src/components/FAQ/FAQ.jsx b/frontend/src/components/FAQ/FAQ.jsx new file mode 100644 index 00000000..aa01538d --- /dev/null +++ b/frontend/src/components/FAQ/FAQ.jsx @@ -0,0 +1,121 @@ +import React, { useState } from 'react'; +import './FAQ.css'; +import { HelpCircle, Search, ChevronDown, ChevronUp } from 'lucide-react'; + +const FAQ = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [expandedId, setExpandedId] = useState(null); // Only one expanded at a time + + // Dummy FAQ data + const faqData = [ + { + id: 1, + question: "How do I place an order?", + answer: "Browse our menu, add items to cart, and proceed to checkout. We accept cards, digital wallets, and cash on delivery." + }, + { + id: 2, + question: "What are your delivery times?", + answer: "Standard delivery is 30-45 minutes. Track your order in real-time through our app." + }, + { + id: 3, + question: "Do you offer vegetarian options?", + answer: "Yes, we have extensive vegetarian and vegan options. Filter by dietary preferences in our menu." + }, + { + id: 4, + question: "Can I cancel my order?", + answer: "Orders can be cancelled within 5 minutes. Contact support for later cancellations." + }, + { + id: 5, + question: "What payment methods do you accept?", + answer: "We accept all major credit cards, debit cards, digital wallets (PayPal, Apple Pay, Google Pay), and cash on delivery." + }, + { + id: 6, + question: "Is there a minimum order amount?", + answer: "Minimum $10 for delivery orders. No minimum for pickup orders." + }, + { + id: 7, + question: "Do you offer loyalty rewards?", + answer: "Join our loyalty program to earn points on every order. Redeem for discounts on future orders." + }, + { + id: 8, + question: "How do I report an issue?", + answer: "Contact our customer support immediately through the app, website, or support line." + } + ]; + + // Filter FAQ items based on search term + const filteredFAQ = faqData.filter(item => + item.question.toLowerCase().includes(searchTerm.toLowerCase()) || + item.answer.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // Toggle expanded item + const toggleItem = (id) => { + setExpandedId(prevId => (prevId === id ? null : id)); + }; + + return ( +
+
+
+ +

Frequently Asked Questions

+

Quick answers to common questions

+
+ +
+
+ + setSearchTerm(e.target.value)} + className="faq-search-input" + /> +
+
+ +
+ {filteredFAQ.length > 0 ? ( + filteredFAQ.map((item) => ( +
+
toggleItem(item.id)} + > +

{item.question}

+ {expandedId === item.id ? + : + + } +
+
+

{item.answer}

+
+
+ )) + ) : ( +
+

No questions found matching your search.

+

Try different keywords or browse all questions.

+
+ )} +
+ +
+

Still have questions? Contact our support team for personalized assistance.

+
+
+
+ ); +}; + +export default FAQ; diff --git a/frontend/src/components/FoodDetail/FoodDetail.css b/frontend/src/components/FoodDetail/FoodDetail.css new file mode 100644 index 00000000..67225adc --- /dev/null +++ b/frontend/src/components/FoodDetail/FoodDetail.css @@ -0,0 +1,184 @@ +.food-detail-wrapper { + max-width: 1400px; + margin: 60px auto; + padding: 40px 20px; + font-family: "Segoe UI", Tahoma, sans-serif; + color: #1c1c1c; +} + +.food-detail-container { + display: flex; + gap: 60px; + align-items: center; + justify-content: center; + flex-wrap: wrap; +} + +.food-detail-image { + flex: 1; + max-width: 600px; +} + +.food-detail-image img { + width: 100%; + height: auto; + border-radius: 20px; + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12); +} + +.food-detail-info { + flex: 1; + max-width: 600px; +} + +.food-detail-info h1 { + font-size: 48px; + font-weight: 700; + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 24px; + +} + +.description { + font-size: 18px; + line-height: 1.8; + + margin-bottom: 32px; + font-weight: 400; +} + +.price { + color: #2e7d32; +} + +.rating [data-theme='dark']{ + + font-weight: 600; + +} + +.rating .star-icon { + color: #ffc107; +} + +.info-section { + display: flex; + flex-wrap: wrap; + gap: 36px; + font-size: 20px; + margin-bottom: 40px; +} + +.info-section>div { + display: flex; + align-items: center; + gap: 12px; + font-weight: 600; +} + +.add-to-cart { + display: inline-flex; + align-items: center; + gap: 12px; + background-color: red; + color: white; + padding: 18px 32px; + font-size: 18px; + font-weight: bold; + border: none; + border-radius: 10px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +.add-to-cart:hover { + background-color: rgb(239, 17, 17); + transform: scale(1.05); +} + +.food-item-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; +} + +.food-item-price { + font-size: 18px; + font-weight: bold; + color: #4caf50; +} + +.view-btn { + background-color: red; + color: white; + padding: 8px 16px; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.3s ease; +} + +.view-btn:hover { + background-color: #c70000; +} + + +@media (max-width: 768px) { + .food-detail-container { + flex-direction: column; + gap: 40px; + } + + .food-detail-info h1 { + font-size: 32px; + } + + .description { + font-size: 17px; + } + + .info-section { + flex-direction: column; + gap: 20px; + } + + .add-to-cart { + width: 100%; + justify-content: center; + } +} +:root { + --bg-color-light: #ffffff; + --bg-color-dark: #121212; + --text-color-light: #1c1c1c; + --text-color-dark: #f1f1f1; + --primary-red: #e53935; + --hover-red: #c62828; + --accent-green: #4caf50; +} + +[data-theme="dark"] .food-detail-wrapper { + color: var(--text-color-dark); +} + +[data-theme="dark"] .food-detail-info h1, +[data-theme="dark"] .description, +[data-theme="dark"] .info-section > div, +[data-theme="dark"] .food-item-price { + color: var(--text-color-dark); +} + +[data-theme="dark"] .add-to-cart, +[data-theme="dark"] .view-btn { + background-color: var(--primary-red); + color: white; +} + +[data-theme="dark"] .add-to-cart:hover, +[data-theme="dark"] .view-btn:hover { + background-color: var(--hover-red); +} diff --git a/frontend/src/components/FoodDetail/FoodDetail.jsx b/frontend/src/components/FoodDetail/FoodDetail.jsx new file mode 100644 index 00000000..63608e90 --- /dev/null +++ b/frontend/src/components/FoodDetail/FoodDetail.jsx @@ -0,0 +1,57 @@ +import React, { useEffect, useContext } from "react"; +import { useParams } from "react-router-dom"; +import { FaDollarSign, FaListUl, FaStar, FaShoppingCart } from "react-icons/fa"; +import { StoreContext } from "../context/StoreContext"; +import "./FoodDetail.css"; + + +const FoodDetail = () => { + const { addToCart, food_list } = useContext(StoreContext); + + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + const { id } = useParams(); + const foodItem = food_list.find(item => item._id === id); // Use food_list from context + console.log("URL ID:", id); +console.log("Food List IDs:", food_list.map(item => item._id)); + + + if (!foodItem) { + return
No food item found.
; + } + + return ( +
+
+
+ {foodItem.name} +
+ +
+

{foodItem.name}

+

{foodItem.description}

+ +
+
+ {foodItem.price} +
+
+ {foodItem.category} +
+
+ 4.5 (120+ reviews) +
+
+ + +
+
+
+ ); +}; + +export default FoodDetail; diff --git a/frontend/src/components/FoodDisplay/FoodDisplay.css b/frontend/src/components/FoodDisplay/FoodDisplay.css index 3a934fa0..d8204066 100644 --- a/frontend/src/components/FoodDisplay/FoodDisplay.css +++ b/frontend/src/components/FoodDisplay/FoodDisplay.css @@ -1,17 +1,106 @@ -.food-display{ - margin-top: 30px; +/* === Food Display Wrapper === */ +.food-display { + margin: 50px; + margin-top: 30px; } -.food-display h2{ - font-size: max(2vw 24px); - font-weight: 600; +/* Remove margin on smaller screens */ +@media (max-width: 768px) { + .food-display { + margin: 20px; + } +} + +/* === Heading === */ +.food-display h2 { + font-size: max(2vw, 24px); + font-weight: 600; + margin: 30px; + color: var(--title-color, #fff); /* Use theme variable for color */ +} + +/* === Grid Layout === */ +.food-display-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 30px; + row-gap: 50px; + margin-top: 30px; +} + +/* === Filter Buttons === */ +.filter-toggle { + margin: 15px 0; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.filter-toggle button { + padding: 8px 14px; + border: 1px solid #ccc; + background-color: white; + cursor: pointer; + border-radius: 4px; + font-weight: 500; + margin-left: 23px; + color: #333; + transition: all 0.3s ease; +} + +.filter-toggle button:hover { + background-color: #f0f0f0; +} + +.filter-toggle button.active { + background-color: #e63946; /* Tomato red with good contrast */ + color: white; + border-color: #e63946; +} + +/* === Food Item Card === */ +.food-item { + background-color: #ffffff; + color: #222222; + padding: 16px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + display: flex; + flex-direction: column; + align-items: center; + transition: transform 0.2s ease-in-out; +} + +.food-item:hover { + transform: translateY(-4px); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); +} + +.food-image { + width: 100%; + height: 180px; + object-fit: cover; + border-radius: 8px; +} + +/* === Text Inside Card === */ +.food-title { + font-size: 1.2rem; + font-weight: 600; + margin-top: 10px; + color: #1a1a1a; +} +.food-description { + font-size: 0.95rem; + margin: 8px 0; + color: #444444; + text-align: center; } -.food-display-list{ - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px,1fr)); - margin-top: 30px; - gap: 30px; - row-gap: 50px; +.food-price { + font-size: 1.1rem; + font-weight: bold; + color: #e63946; + margin-top: auto; } \ No newline at end of file diff --git a/frontend/src/components/FoodDisplay/FoodDisplay.jsx b/frontend/src/components/FoodDisplay/FoodDisplay.jsx index 30ab0a08..375b88c8 100644 --- a/frontend/src/components/FoodDisplay/FoodDisplay.jsx +++ b/frontend/src/components/FoodDisplay/FoodDisplay.jsx @@ -1,22 +1,65 @@ -import React, { useContext } from 'react' -import './FoodDisplay.css' -import { StoreContext } from '../context/StoreContext' -import FoodItem from '../FoodItem/FoodItem' -const FoodDisplay = ({category}) => { - const {food_list} = useContext(StoreContext) +import React, { useContext, useState } from 'react'; +import './FoodDisplay.css'; +import { StoreContext } from '../context/StoreContext'; +import FoodItem from '../FoodItem/FoodItem'; + +const FoodDisplay = ({ category }) => { + const { food_list } = useContext(StoreContext); + const [filterType, setFilterType] = useState('all'); // all | veg | non-veg + + const handleToggle = (type) => { + setFilterType(type); + }; + + const filteredFoodList = food_list.filter((item) => { + const matchCategory = category === item.category || category === 'All'; + const matchType = + filterType === 'all' || + (filterType === 'veg' && item.type === 'veg') || + (filterType === 'non-veg' && item.type === 'nonveg'); + return matchCategory && matchType; + }); + return (
-

Top Dishes Near You

-
- {food_list.map((item,index)=>{ - if(category === item.category || category === "All"){ - return - } - - })} -
+

Top Dishes Near You

+ + {/* Toggle Buttons */} +
+ + + +
+ +
+ {filteredFoodList.map((item, index) => ( + + ))} +
- ) -} + ); +}; -export default FoodDisplay +export default FoodDisplay \ No newline at end of file diff --git a/frontend/src/components/FoodItem/FoodItem.css b/frontend/src/components/FoodItem/FoodItem.css index 4cdd4b68..8e91a684 100644 --- a/frontend/src/components/FoodItem/FoodItem.css +++ b/frontend/src/components/FoodItem/FoodItem.css @@ -1,74 +1,157 @@ -.food-item{ - width: 100%; - margin: auto; - border-radius: 15px; - box-shadow: 0px 0px 10px #00000015; - transition: 0.7s; - animation: FadeIn 1s; +.food-item { + width: 90%; + max-width: 350px; + margin: auto; + border-radius: 15px; + box-shadow: 0px 0px 10px var(--box-shadow); + transition: 0.4s ease-in-out; + animation: FadeIn 0.8s ease; + background-color: var(--bg-color, #fff); + color: var(--text-color, #111); + border: 2px solid var(--border-color, #e2e8f0); } -.food-item-image{ - width: 100%; - border-radius: 15px 15px 0px 0px; +.food-item:hover { + box-shadow: 0 4px 16px var(--hover-box-shadow); + transform: translateY(-5px); } -.food-item-info{ - padding: 20px; +.food-item-img-container { + position: relative; + overflow: hidden; } -.food-item-name-rating{ - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; +.food-item-image { + width: 100%; + border-radius: 15px 15px 0px 0px; + object-fit: cover; + transition: transform 0.3s ease-in-out; } -.food-item-name-rating p{ - font-size: 20px; - font-weight: 500; +.food-item-image:hover { + transform: scale(1.05); } -.food-item-name-rating img{ - width: 70px; +.food-item-info { + padding: 20px; } -.food-item-desc{ - color: #676767; - font-size: 12px; +.food-item-name-rating { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; } -.food-item-price{ - color: tomato; - font-size: 22px; - font-weight: 500; - margin: 10px 0px; +.food-item-name-rating p { + font-size: 20px; + font-weight: 600; + color: var(--title-color, #111); } -.food-item-img-container{ - position: relative; +.food-item-desc { + color: var(--gray-text); + font-size: 13px; + margin-bottom: 10px; + line-height: 1.4; +} + +.food-item-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.food-item-price { + color: tomato; + font-size: 22px; + font-weight: 600; +} + +.view-btn { + display: flex; + align-items: center; + gap: 6px; + background-color: #ff6347; + border: none; + padding: 8px 12px; + border-radius: 50px; + color: white; + font-weight: 500; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.view-btn:hover { + background-color: #e5533d; +} + +.add { + background-color: tomato; + padding: 6px; + border-radius: 50%; + position: absolute; + bottom: 15px; + right: 15px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.food-item-counter { + position: absolute; + bottom: 15px; + right: 15px; + display: flex; + align-items: center; + gap: 10px; + border-radius: 50px; + background-color: white; + padding: 6px 12px; + box-shadow: 0px 0px 8px #00000030; +} + +.food-item-counter p { + font-weight: 500; + color: #111; +} +.wishlist-icon { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + z-index: 2; } -.food-item-img-container .add{ - width: 35px; - position: absolute; - bottom: 15px; - right: 15px; - border-radius: 50%; - cursor: pointer; +/* Shared wishlist item styles */ +.food-item.shared { + border: 2px solid #f093fb; + position: relative; } -.food-item-counter{ - position: absolute; - bottom: 15px; - right: 15px; - display: flex; - align-items: center; - gap: 10px; - border-radius: 50px; - background-color: white; +.food-item.shared::before { + content: "πŸ”— Shared"; + position: absolute; + top: -10px; + left: 15px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + padding: 4px 12px; + border-radius: 15px; + font-size: 12px; + font-weight: 500; + z-index: 3; } -.food-item-counter img{ - width: 30px; -} \ No newline at end of file +@keyframes FadeIn { + 0% { + opacity: 0; + transform: translateY(15px); + } + 100% { + opacity: 1; + transform: translateY(0px); + } +} diff --git a/frontend/src/components/FoodItem/FoodItem.jsx b/frontend/src/components/FoodItem/FoodItem.jsx index f6192279..755415de 100644 --- a/frontend/src/components/FoodItem/FoodItem.jsx +++ b/frontend/src/components/FoodItem/FoodItem.jsx @@ -1,34 +1,92 @@ -import React, { useContext } from 'react' -import './FoodItem.css' -import { assets } from '../../assets/frontend_assets/assets' -import { StoreContext } from '../context/StoreContext' -const FoodItem = ({id,name,price,description,image}) => { - const {cartItems,removeFromCart, addToCart} = useContext(StoreContext) +import React, { useContext, useEffect, useState } from "react"; +import "./FoodItem.css"; +import { StoreContext } from "../context/StoreContext"; +import { useNavigate } from "react-router-dom"; +import { Heart, Plus, Minus } from "lucide-react"; +import { assets } from "../../assets/frontend_assets/assets"; + +const FoodItem = ({ id, name, price, description, image, isShared = false }) => { + const { cartItems, removeFromCart, addToCart } = useContext(StoreContext); + const [isWishlisted, setIsWishlisted] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + const stored = JSON.parse(localStorage.getItem("wishlist")) || []; + setIsWishlisted(stored.includes(id)); + }, [id]); + + const toggleWishlist = () => { + const stored = JSON.parse(localStorage.getItem("wishlist")) || []; + let updated; + + if (stored.includes(id)) { + updated = stored.filter((itemId) => itemId !== id); + setIsWishlisted(false); + } else { + updated = [...stored, id]; + setIsWishlisted(true); + } + + localStorage.setItem("wishlist", JSON.stringify(updated)); + window.dispatchEvent(new Event("wishlistUpdated")); + }; + + const handleClick = () => { + navigate(`/food/${id}`); + }; + return ( -
-
-
- - {!cartItems[id] ? - addToCart(id)} src={assets.add_icon_white} alt=''/>: -
- removeFromCart(id)} src={assets.remove_icon_red} alt="" /> -

{cartItems[id]}

- addToCart(id)}src={assets.add_icon_green} alt="" /> -
- } +
+
+ {name} + {!cartItems[id] ? ( +
addToCart(id)}> + +
+ ) : ( +
+ removeFromCart(id)} + style={{ cursor: "pointer" }} + /> +

{cartItems[id]}

+ addToCart(id)} + style={{ cursor: "pointer" }} + /> +
+ )} + {!isShared && ( +
+ +
+ )} +
+ +
+
+

{name}

+ Rating
-
-
-

{name}

- -
-

{description}

-

${price}

+

{description}

+
+

${price}

+
- ) -} + ); +}; -export default FoodItem +export default FoodItem; diff --git a/frontend/src/components/Footer/Footer.css b/frontend/src/components/Footer/Footer.css index 6d9a95ed..000536a8 100644 --- a/frontend/src/components/Footer/Footer.css +++ b/frontend/src/components/Footer/Footer.css @@ -6,10 +6,10 @@ align-items: center; gap: 20px; padding: 20px 8vw; - padding-top: 80px; - margin-top: 100px; - border-top-left-radius: 30px; - border-top-right-radius: 30px; + padding-top: 50px; + /* margin-top: 100px; */ + /* border-top-left-radius: 30px; + border-top-right-radius: 30px; */ } .footer-content{ @@ -24,7 +24,7 @@ flex-direction: column; align-items: start; gap: 20px; - + color: rgb(233, 207, 207); } .footer-content-left li, .footer-content-right li, .footer-content-center li{ @@ -32,21 +32,78 @@ margin-bottom: 10px; cursor: pointer; margin-left: 3px; + color: rgb(233, 207, 207); +} + +.footer-content-left img { + width: 100px; /* or any size you want, e.g., 80px */ + height: auto; /* keeps aspect ratio */ + border-radius: 12px; /* optional: softer look */ + +} + +.footer-content-center ul li,.footer-content-right ul li{ + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.75rem; +} + +.footer-content-center ul li:hover .icon, +.footer-content-center ul li:hover span, +.footer-content-right ul li:hover .icon, +.footer-content-right ul li:hover span{ + color: rgb(243, 113, 38); + transform: scale(1.1); + transition: all 0.3s ease; +} + +.footer-content-center ul li span,.footer-content-right ul li span{ + color: White; } -.footer-content-right h2, .footer-content-center h2{ +.text{ + position: relative; + display: inline-block; color: white; } +.text::after{ + content: ""; + position: absolute; + left: 0; + bottom: -5px; + width: 100%; + height: 3px; + background-color: rgb(243, 113, 38); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.5s ease; +} + +.text:hover::after{ + transform: scaleX(1); +} + +.text:hover{ + -webkit-text-fill-color: rgb(243, 113, 38); +} + .footer-social-icons img{ width: 40px; margin-right: 15px; + border: 2px solid #fff; + border-radius: 50%; cursor: pointer; transition: 0.5s; + color: #555; + background-color: transparent; + transition: all 0.3s ease; } .footer-social-icons img:hover{ - opacity: 0.6; + background-color: rgb(243, 113, 38); + color: #fff; } .footer hr{ @@ -57,6 +114,10 @@ border: none; } +.footer p { + color: rgb(233, 207, 207); +} + @media (max-width:750px){ .footer-content{ display: flex; @@ -66,4 +127,13 @@ .footer-copyright{ text-align: center; } -} \ No newline at end of file +} + +.nav-link { + display: flex; + align-items: center; + gap: 8px; + text-decoration: none; + color: inherit; + } + \ No newline at end of file diff --git a/frontend/src/components/Footer/Footer.jsx b/frontend/src/components/Footer/Footer.jsx index 9e59d52b..b0bd8f11 100644 --- a/frontend/src/components/Footer/Footer.jsx +++ b/frontend/src/components/Footer/Footer.jsx @@ -1,38 +1,74 @@ import React from 'react' import './Footer.css' import { assets } from '../../assets/frontend_assets/assets' +import { IoHome, IoLogoGithub } from "react-icons/io5"; +import { SlSpeech } from "react-icons/sl"; +import { CiDeliveryTruck } from "react-icons/ci"; +import { MdPrivacyTip } from "react-icons/md"; +import { MdAddCall } from "react-icons/md"; +import { IoIosMail } from "react-icons/io"; +import { Link } from "react-router-dom"; + + const Footer = () => { return (
-

Copyright 2024   ©   foodie.com - All Right Reserved

+

+ Copyright {new Date().getFullYear()}   ©   foodie.com - All Right Reserved +

) } diff --git a/frontend/src/components/Header/Header.css b/frontend/src/components/Header/Header.css index bb2b97f7..c0a08d51 100644 --- a/frontend/src/components/Header/Header.css +++ b/frontend/src/components/Header/Header.css @@ -1,59 +1,381 @@ -.header { - height: 42vh; - width: 100%; - background: url('header_img.png') no-repeat center center; - background-size: cover; - position: relative; - border-radius: 20px; +:root { + --primary-orange: #ff6b35; + --primary-orange-light: #ff8c5a; + --secondary-yellow: #ffd23f; + --dark-bg: #1a1a1a; + --light-text: #ffffff; + --gray-text: #a0a0a0; + --card-bg: rgba(255, 255, 255, 0.1); + --glass-bg: rgba(255, 255, 255, 0.05); +} + +.hero-container { + position: relative; + min-height: 50vh; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + overflow: hidden; + background: linear-gradient(135deg, #1a1a1a 0%, #2d1810 50%, #1a1a1a 100%); + padding: 2.5rem 1.5rem; + /* border-radius: 32px; */ +} + +.hero-background { + position: absolute; + inset: 0; + background: url("/header_img.png") no-repeat center center / cover; + opacity: 0.3; + z-index: 1; + overflow: hidden; +} + +.hero-background::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 135deg, + rgba(255, 107, 53, 0.1) 0%, + rgba(45, 24, 16, 0.8) 50%, + rgba(26, 26, 26, 0.9) 100% + ); +} + +.hero-content { + position: relative; + z-index: 3; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + max-width: 700px; + width: 100%; + margin: 0 auto; + padding: 2rem 2rem; + background: rgba(0, 0, 0, 0.4); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); + border-radius: 32px; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 20px 40px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.2); + position: relative; +} + +.hero-content::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.03) 0%, + transparent 50%, + rgba(0, 0, 0, 0.1) 100% + ); + border-radius: 32px; + pointer-events: none; +} + +.hero-main { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.2rem; + width: 100%; + position: relative; + z-index: 1; +} + +.hero-title { + font-size: clamp(2rem, 4vw, 3.2rem); + font-weight: 800; + line-height: 1.1; + margin: 20px 1.5rem; + animation: slideInUp 1s ease-out 0.2s both; +} + +.title-line-1 { + display: block; + color: var(--light-text); + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +.title-line-2 { + display: block; +} + +.gradient-text { + background: linear-gradient( + 135deg, + var(--primary-orange) 0%, + var(--secondary-yellow) 100% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + position: relative; +} + +.gradient-text::after { + content: ""; + position: absolute; + bottom: -6px; + left: 50%; + transform: translateX(-50%); + width: 60px; + height: 3px; + background: linear-gradient( + 90deg, + var(--primary-orange), + var(--secondary-yellow) + ); + border-radius: 24px; + animation: expandWidth 1s ease-out 1s both; +} + +@keyframes expandWidth { + from { + width: 0; + } + to { + width: 60px; + } +} + +.hero-description { + font-size: 1rem; + line-height: 1.5; + color: var(--gray-text); + max-width: 550px; + animation: slideInUp 1s ease-out 0.4s both; +} + +.hero-stats { + display: flex; + align-items: center; + justify-content: center; + gap: 2rem; + padding: 1rem 0; + animation: slideInUp 1s ease-out 0.6s both; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 0.4rem; +} + +.stat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: rgba(255, 107, 53, 0.15); + border-radius: 16px; + color: var(--primary-orange); + border: 1px solid rgba(255, 107, 53, 0.2); +} + +.stat-number { + font-size: 1.4rem; + font-weight: 700; + color: var(--primary-orange); + display: block; +} + +.stat-label { + font-size: 0.8rem; + color: var(--gray-text); +} + +.stat-divider { + width: 1px; + height: 45px; + background: rgba(255, 255, 255, 0.1); + border-radius: 1px; +} + +.hero-actions { + display: flex; + gap: 0.8rem; + animation: slideInUp 1s ease-out 0.8s both; } -.header-contents{ - position: absolute; - display: flex; +.cta-primary, +.cta-secondary { + position: relative; + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.8rem 1.6rem; + border-radius: 28px; + font-weight: 600; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.3s ease; + border: none; + overflow: hidden; +} + +.cta-primary span { + color: white; +} + +.cta-primary, +.cta-secondary { + background: linear-gradient( + 135deg, + var(--primary-orange), + var(--primary-orange-light) + ); + color: white; + box-shadow: 0 8px 25px rgba(255, 107, 53, 0.3); +} + +.cta-primary:hover, +.cta-secondary:hover { + transform: translateY(-2px); + box-shadow: 0 12px 35px rgba(255, 107, 53, 0.4); +} + +.cta-primary .button-icon, +.cta-secondary .button-icon { + transition: transform 0.3s ease; +} + +.cta-primary:hover .button-icon, +.cta-secondary:hover .button-icon { + transform: translateX(4px); +} + +.button-shine { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + transition: left 0.6s ease; +} + +.cta-primary:hover .button-shine, +.cta-secondary:hover .button-shine { + left: 100%; +} + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .hero-container { + min-height: 45vh; + padding: 1rem; + border-radius: 24px; + } + + .hero-content { + padding: 1.5rem 1.5rem; + border-radius: 24px; + } + + .hero-background { + border-radius: 24px; + } + + .hero-title { + font-size: clamp(1.8rem, 5vw, 2.5rem); + margin-left: 1.2rem; + margin-right: 1.2rem; + } + + .hero-actions { flex-direction: column; - align-items: start; - gap: 1.5vw; - max-width: 50%; - bottom: 10%; - left: 6vw; - animation: FadeIn 3s; -} - -.header-contents h2{ - font-weight: 500; - color: white; - font-size: max(4vw,22px); -} - -.header-contents p{ - color: white; - font-size: 1vw; -} - -.header-contents button{ - border: none; - color: #747474; - font-weight: 500; - background-color: white; - font-size: max(1vw, 13px); - border-radius: 50px; - padding: 1vw 2.3vw; -} - -@media (max-width:1050px) { - .header-contents{ - max-width: 45%; - } -} - -@media (max-width:750px) { - .header-contents{ - max-width: 65%; - } - .header-contents p{ - display: none; - } - .header-contents button{ - padding: 2vw 4vw; - } -} \ No newline at end of file + align-items: center; + width: 100%; + gap: 0.6rem; + } + + .cta-primary, + .cta-secondary { + width: 100%; + max-width: 250px; + justify-content: center; + } + + .hero-stats { + gap: 1.5rem; + } + + .stat-divider { + height: 35px; + } +} + +@media (max-width: 480px) { + .hero-container { + min-height: 40vh; + border-radius: 20px; + } + + .hero-content { + padding: 1.2rem 1.2rem; + border-radius: 20px; + } + + .hero-background { + border-radius: 20px; + } + + .hero-stats { + gap: 1rem; + padding: 0.8rem 0; + } + + .stat-item { + gap: 0.2rem; + } + + .stat-icon { + width: 24px; + height: 24px; + border-radius: 12px; + } + + .stat-number { + font-size: 1rem; + font-weight: 700; + } + + .stat-label { + font-size: 0.65rem; + line-height: 1.2; + } + + .stat-divider { + width: 1px; + height: 25px; + } + + .hero-main { + gap: 1rem; + } +} diff --git a/frontend/src/components/Header/Header.jsx b/frontend/src/components/Header/Header.jsx index ed52935a..b6422508 100644 --- a/frontend/src/components/Header/Header.jsx +++ b/frontend/src/components/Header/Header.jsx @@ -1,15 +1,65 @@ -import React from 'react' -import './Header.css' +import SearchBar from "../SearchBar/SearchBar"; + +import "./Header.css"; +import { Star, Clock, MapPin, ArrowRight, Play } from "lucide-react"; const Header = () => { return ( -
-
-

Order Your Favourite Food Here

-

Choose from a diverse menu featuring a delectable array of dishes crafted with the finest ingredients and culinary expertise. Our mission is to satisfy your cravings and elevate your dining experience, one delicious meal at a time

- +
+ +
+ +
+
+

+ Craving Something + Delicious? +

+ +

+ Discover a world of flavors with dishes made fresh, fast, and + flawlessly. From comfort food to gourmet delights, we serve joy on + every plate. +

+ +
+
+
+ +
+ 500+ + Restaurants +
+
+
+
+ +
+ 30min + Avg Delivery +
+
+
+
+ +
+ 4.8β˜… + Rating +
+
+ + +
- ) -} + ); +}; -export default Header +export default Header; diff --git a/frontend/src/components/LoadingAnimation.css b/frontend/src/components/LoadingAnimation.css new file mode 100644 index 00000000..a81eb986 --- /dev/null +++ b/frontend/src/components/LoadingAnimation.css @@ -0,0 +1,383 @@ +/* Loading Animation Container */ +.loading-container { + height: 100vh; + width: 100%; + overflow: hidden; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #fff8e1, #ffc107); +} + +/* Animated City Skyline */ +.city-skyline { + position: absolute; + bottom: 0; + width: 100%; + height: 64px; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.buildings-container { + display: flex; + align-items: flex-end; + gap: 8px; + width: 100%; + max-width: 1200px; + padding: 0 16px; +} + +.building { + flex: 1; + background: rgba(255, 193, 7, 0.5); + opacity: 0.9; + animation: building-grow 0.8s ease-out forwards; + transform-origin: bottom; + border-radius: 2px 2px 0 0; +} + +.building-1 { height: 40px; animation-delay: 0ms; } +.building-2 { height: 64px; animation-delay: 200ms; } +.building-3 { height: 32px; animation-delay: 400ms; } +.building-4 { height: 56px; animation-delay: 600ms; } +.building-5 { height: 48px; animation-delay: 800ms; } +.building-6 { height: 72px; animation-delay: 1000ms; } +.building-7 { height: 24px; animation-delay: 1200ms; } +.building-8 { height: 64px; animation-delay: 1400ms; } +.building-9 { height: 48px; animation-delay: 1600ms; } +.building-10 { height: 40px; animation-delay: 1800ms; } +.building-11 { height: 56px; animation-delay: 2000ms; } +.building-12 { height: 32px; animation-delay: 2200ms; } + +/* Main Loading Section */ +.main-loader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 32px; + z-index: 10; +} + +/* Enhanced Loading Circle */ +.loader-wrapper { + position: relative; + animation: rotate-circle 4s linear infinite; +} + +.loader-circle { + position: relative; + width: 160px; + height: 160px; +} + +.glow-ring { + position: absolute; + inset: 0; + border-radius: 50%; + background: linear-gradient(to right, rgba(255, 152, 0, 0.2), rgba(255, 193, 7, 0.2)); + filter: blur(48px); +} + +.progress-svg { + width: 100%; + height: 100%; + transform: rotate(-90deg); +} + +.progress-circle { + animation: pulse-glow 2s ease-in-out infinite; +} + +.inner-circle { + animation: rotate-circle 4s linear infinite; +} + +.center-icon { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-circle { + width: 64px; + height: 64px; + background: linear-gradient(to bottom right, #ff9800, #ffc107); + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); +} + +.scooter-emoji { + font-size: 24px; +} + +/* Scooter Animation */ +.scooter-section { + position: relative; +} + +.scooter-animation { + animation: scooter-move 4s ease-in-out forwards; +} + +.scooter-container { + position: relative; + margin-top: 400px; +} + +.scooter-shadow { + position: absolute; + bottom: -12px; + left: 50%; + transform: translateX(-50%); + width: 80px; + height: 16px; + background: rgba(0, 0, 0, 0.3); + border-radius: 50%; + filter: blur(12px); +} + +.scooter-svg { + width: 80px; + height: 48px; +} + +.delivery-person { + position: absolute; + top: -24px; + left: 50%; + transform: translateX(-50%); +} + +.person-svg { + width: 24px; + height: 24px; +} + +/* Delivery Path */ +.delivery-path { + position: absolute; + bottom: 112px; + width: 100%; + height: 80px; +} + +.path-svg { + width: 100%; + height: 100%; +} + +/* Floating Food Icons */ +.floating-icons { + position: absolute; + inset: 0; + pointer-events: none; +} + +.food-icon { + position: absolute; + width: 64px; + height: 64px; +} + +.food-1 { + top: 96px; + left: 100px; + animation: float 3s ease-in-out infinite; +} + +.food-2 { + top: 160px; + right: 96px; + animation: float 3s ease-in-out infinite 1s; +} + +.food-3 { + bottom: 192px; + left: 112px; + animation: float 3s ease-in-out infinite 2s; +} + +.food-4 { + bottom: 160px; + right: 128px; + animation: float 3s ease-in-out infinite; +} + +.icon-container { + width: 100%; + height: 100%; + background: linear-gradient(to bottom right, #ff9800, #ffc107); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.icon-container span { + font-size: 24px; +} + +/* Loading Message */ +.loading-message { + position: absolute; + bottom: 48px; + text-align: center; + padding: 0 16px; +} + +.message-container { + font-size: 24px; + font-weight: bold; + color: #3e2723; + font-family: 'Courier New', Courier, monospace; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(8px); + padding: 12px 24px; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); +} + +.cursor { + display: inline-block; + width: 4px; + height: 32px; + background-color: #ff9800; + margin-left: 8px; + transition: opacity 100ms; + border-radius: 2px; +} + +.cursor.visible { + opacity: 1; +} + +.cursor.hidden { + opacity: 0; +} + +/* Keyframe Animations */ +@keyframes building-grow { + 0% { + transform: scaleY(0); + opacity: 0; + } + 100% { + transform: scaleY(1); + opacity: 1; + } +} + +@keyframes rotate-circle { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes pulse-glow { + 0%, 100% { + filter: drop-shadow(0 0 20px rgba(255, 193, 7, 0.4)); + } + 50% { + filter: drop-shadow(0 0 40px rgba(255, 193, 7, 0.8)); + } +} + +@keyframes scooter-move { + 0% { + transform: translateX(-100px) scale(1.6); + } + 100% { + transform: translateX(calc(100vw + 100px)) scale(1.6); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + } + 50% { + transform: translateY(-20px) rotate(5deg) scale(1.05); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .loader-circle { + width: 120px; + height: 120px; + } + + .icon-circle { + width: 48px; + height: 48px; + } + + .scooter-emoji { + font-size: 20px; + } + + .scooter-svg { + width: 60px; + height: 36px; + } + + .food-icon { + width: 48px; + height: 48px; + } + + .icon-container span { + font-size: 18px; + } + + .message-container { + font-size: 18px; + padding: 8px 16px; + } + + .cursor { + height: 24px; + } +} + +@media (max-width: 480px) { + .food-1 { + top: 80px; + left: 40px; + } + + .food-2 { + top: 120px; + right: 40px; + } + + .food-3 { + bottom: 160px; + left: 60px; + } + + .food-4 { + bottom: 120px; + right: 60px; + } +} + + + diff --git a/frontend/src/components/LoadingAnimation.jsx b/frontend/src/components/LoadingAnimation.jsx new file mode 100644 index 00000000..9af88024 --- /dev/null +++ b/frontend/src/components/LoadingAnimation.jsx @@ -0,0 +1,271 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import "./LoadingAnimation.css"; +import TextType from './Typetext/typetext'; + +const LoadingAnimation = () => { + const navigate = useNavigate(); + const [typedText, setTypedText] = useState(""); + const [showCursor, setShowCursor] = useState(true); + const fullText = "Delivering happiness, one bite at a time…"; + + // Fixed typewriter effect + useEffect(() => { + let currentIndex = 0; + const interval = setInterval(() => { + if (currentIndex <= fullText.length) { + setTypedText(fullText.slice(0, currentIndex)); + currentIndex++; + } else { + clearInterval(interval); + } + }, 80); + + return () => clearInterval(interval); + }, []); + + // Cursor blink effect + useEffect(() => { + const cursorInterval = setInterval(() => { + setShowCursor(prev => !prev); + }, 500); + + return () => clearInterval(cursorInterval); + }, []); + + // Navigate after animation completes + useEffect(() => { + const timer = setTimeout(() => { + navigate("/home"); + }, 5000); + return () => clearTimeout(timer); + }, [navigate]); + + return ( +
+ + {/* Animated City Skyline */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Main Loading Section - Centered */} +
+ + {/* Enhanced Loading Progress Circle */} +
+
+ {/* Outer glow ring */} +
+ + + + + + + + + + + + + + + + + + {/* Background circle */} + + + {/* Progress circle */} + + + {/* Inner accent circle */} + + + + {/* Center logo/icon */} +
+
+ πŸ›΅ +
+
+
+
+ + {/* Enhanced Scooter Animation */} +
+
+
+ {/* Enhanced Scooter Shadow */} +
+ + {/* Larger Scooter SVG */} + + {/* Wheels */} + + + + + + {/* Scooter Body */} + + + + {/* Headlight */} + + + + {/* Handle */} + + + + {/* Delivery Box */} + + + + + + {/* Delivery Person */} +
+ + {/* Head */} + + + + {/* Body */} + + +
+
+
+
+
+ + {/* Delivery Path */} +
+ + + + +
+ + {/* Enhanced Floating Food Icons */} +
+
+
+ Hamburger icon +
+
+ +
+
+ pizza icon +
+
+ + +
+
+ coke icon +
+
+ +
+
+ food icon +
+
+
+ + {/* Enhanced Typewriter Loading Message */} +
+ +
+
+ ); +}; + +export default LoadingAnimation; + + + diff --git a/frontend/src/components/LoginPopup/LoginPopup.css b/frontend/src/components/LoginPopup/LoginPopup.css index a752fd99..cdefcc3d 100644 --- a/frontend/src/components/LoginPopup/LoginPopup.css +++ b/frontend/src/components/LoginPopup/LoginPopup.css @@ -1,74 +1,123 @@ -.LoginPopup{ - position: absolute; - z-index: 1; - width: 100%; - height: 100%; - background-color: #00000090; - display: grid; +.LoginPopup { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 1000; + background-color: #00000090; + display: grid; } -.login-popup-container{ - place-self: center; - width: max(23vw,330px); - color: #808080; - background-color: white; - display: flex; - flex-direction: column; - gap: 25px; - padding: 25px 30px; - border-radius: 8px; - font-size: 14px; - animation: FadeIn 1.3s; + +.login-popup-container { + place-self: center; + width: max(23vw, 330px); + color: #808080; + background-color: white; + display: flex; + flex-direction: column; + gap: 25px; + padding: 25px 30px; + border-radius: 8px; + font-size: 14px; + animation: FadeIn 1.3s; +} + +.login-popup-title { + display: flex; + justify-content: space-between; + align-items: center; + color: black; +} + +.login-popup-title img { + width: 16px; + cursor: pointer; +} + +.login-popup-inputs { + display: flex; + flex-direction: column; + gap: 20px; } -.login-popup-title{ - display: flex; - justify-content: space-between; - align-items: center; - color: black; +.login-popup-inputs input { + outline: none; + border: 1px solid #c9c9c9; + padding: 10px; + border-radius: 4px; } -.login-popup-title img{ - width: 16px; - cursor: pointer; +.login-popup-container button { + border: none; + padding: 10px; + border-radius: 4px; + color: white; + background-color: tomato; + font-size: 15px; + cursor: pointer; } -.login-popup-inputs{ - display: flex; - flex-direction: column; - gap: 20px; +/* Forgot Password Buttons */ +.resend-otp-btn { + background-color: #f08080; + margin-top: -10px; + font-size: 13px; + padding: 8px; } -.login-popup-inputs input{ - outline: none; - border: 1px solid #c9c9c9; - padding: 10px; - border-radius: 4px; +.login-popup-condition { + display: flex; + align-items: start; + gap: 8px; + margin-top: -15px; } -.login-popup-container button{ - border: none; - padding: 10px; - border-radius: 4px; - color: white; - background-color: tomato; - font-size: 15px; - cursor: pointer; +.login-popup-condition input { + margin-top: 5px; } -.login-popup-condition{ - display: flex; - align-items: start; - gap: 8px; - margin-top: -15px; +.LoginPopup p span { + color: tomato; + font-weight: 500; + cursor: pointer; } -.login-popup-condition input{ - margin-top: 5px; +.forgot-password-link { + font-weight: 500; + margin-top: -15px; + text-align: right; + font-size: 14px; + color: tomato; + cursor: pointer; + text-decoration: none; } -.LoginPopup p span{ - color: tomato; - font-weight: 500; - cursor: pointer; -} \ No newline at end of file +.otp-container { + display: flex; + gap: 10px; + justify-content: center; +} + +.otp-container input { + width: 35px; + height: 40px; + text-align: center; + font-size: 18px; + border: 1px solid #c9c9c9; + border-radius: 4px; +} + +.resend-otp-btn { + background-color: #f08080; + font-size: 14px; + padding: 8px; + margin-top: 5px; + cursor: pointer; +} + +.resend-otp-btn.disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/frontend/src/components/LoginPopup/LoginPopup.jsx b/frontend/src/components/LoginPopup/LoginPopup.jsx index e27ee6be..b5372a85 100644 --- a/frontend/src/components/LoginPopup/LoginPopup.jsx +++ b/frontend/src/components/LoginPopup/LoginPopup.jsx @@ -1,32 +1,238 @@ -import React, { useState } from 'react' -import './LoginPopup.css' -import { assets } from '../../assets/frontend_assets/assets' +import React, { useState, useEffect, useRef } from 'react'; +import './LoginPopup.css'; +import { assets } from '../../assets/frontend_assets/assets'; +import toast, { Toaster } from 'react-hot-toast'; +import { useNavigate } from 'react-router-dom'; +import apiRequest from "../../lib/apiRequest"; + + +const LoginPopup = ({ setShowLogin }) => { + const [currState, setCurrState] = useState("Sign Up"); + const [forgotFlow, setForgotFlow] = useState(false); + const [stage, setStage] = useState(1); + const [email, setEmail] = useState(""); + const [otp, setOtp] = useState(Array(6).fill("")); + const [timer, setTimer] = useState(60); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const navigate=useNavigate(); + + const popupRef = useRef(); + const otpRefs = useRef([]); + + useEffect(() => { + const handleClickOutside = (event) => { + if (popupRef.current && !popupRef.current.contains(event.target)) { + setShowLogin(false); + } + }; + + const handleEscapeKey = (event) => { + if (event.key === 'Escape') { + setShowLogin(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscapeKey); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscapeKey); + }; + }, [setShowLogin]); + + useEffect(() => { + let interval; + if (stage === 2 && timer > 0) { + interval = setInterval(() => { + setTimer((prev) => prev - 1); + }, 1000); + } + return () => clearInterval(interval); + }, [stage, timer]); + + const handleOTPChange = (e, index) => { + const value = e.target.value; + if (!/^\d?$/.test(value)) return; + const newOtp = [...otp]; + newOtp[index] = value; + setOtp(newOtp); + + if (value && index < 5) { + otpRefs.current[index + 1].focus(); + } + }; + + const handleSendOTP = (e) => { + e.preventDefault(); + if (!email) return toast.error("Enter email"); + toast.success("OTP sent to your email"); + setStage(2); + setTimer(60); + }; + + const handleVerifyOTP = (e) => { + e.preventDefault(); + if (otp.join("").length !== 6) return toast.error("Enter 6-digit OTP"); + toast.success("OTP verified"); + setStage(3); + }; + + const handleResetPassword = (e) => { + e.preventDefault(); + if (newPassword !== confirmPassword) return toast.error("Passwords do not match"); + toast.success("Password reset successfully!"); + setForgotFlow(false); + setStage(1); + setOtp(Array(6).fill("")); + setCurrState("Login"); + }; + const handleSubmit = async (e) => { + e.preventDefault(); + + const formData = new FormData(e.target); + const name = formData.get("name"); + const email = formData.get("email"); + const password = formData.get("password"); + + if (!email || !password || (currState === "Sign Up" && !name)) { + return toast.error("Please fill all fields"); + } + + const endpoint = + currState === "Sign Up" ? "/api/auth/register" : "/api/auth/login"; + + try { + const { data } = await apiRequest.post(endpoint, { name, email, password }); + + toast.success(`${currState} successful!`); + + // Store user info locally (no token) + localStorage.setItem("user", JSON.stringify(data.user)); + + setShowLogin(false); + window.location.reload(); + } catch (err) { + const message = + err.response?.data?.message || `${currState} failed. Please try again.`; + toast.error(message); + console.error(err); + } + }; + + -const LoginPopup = ({setShowLogin}) => { - const [currState, setCurrState] = useState("Sign Up") return (
-
+ +
-

{currState}

- setShowLogin(false)} src={assets.cross_icon} alt="" /> +

{forgotFlow ? "Reset Password" : currState}

+ setShowLogin(false)} src={assets.cross_icon} alt="close" />
+
- {currState === "Login" ? <>:} - - + {!forgotFlow && currState !== "Login" && ( + + )} + + {!forgotFlow && ( + <> + + + + {currState === "Login" && ( +

{ + setForgotFlow(true); + setStage(1); + }}> + Forgot Password? +

+ )} + + )} + + {forgotFlow && stage === 1 && ( + <> + setEmail(e.target.value)} + required + /> + + + )} + + {forgotFlow && stage === 2 && ( + <> +
+ {otp.map((digit, i) => ( + otpRefs.current[i] = el} + onChange={(e) => handleOTPChange(e, i)} + /> + ))} +
+ + + + )} + + {forgotFlow && stage === 3 && ( + <> + setNewPassword(e.target.value)} + required + /> + setConfirmPassword(e.target.value)} + required + /> + + + )}
- -
- + + {!forgotFlow && ( +
+

By continuing, I agree to the terms of use & privacy policy.

-
- {currState === "Login"?

Create a new account? setCurrState("Sign Up")}>Click Here

: -

Already have an account? setCurrState("Login")}>Login Here

- } +
+ )} + + {!forgotFlow && ( + currState === "Login" ? ( +

Create a new account? setCurrState("Sign Up")}>Click Here

+ ) : ( +

Already have an account? setCurrState("Login")}>Login Here

+ ) + )}
- ) -} + ); +}; -export default LoginPopup +export default LoginPopup; diff --git a/frontend/src/components/Navbar/Navbar.css b/frontend/src/components/Navbar/Navbar.css index 8b4d0edf..1217347d 100644 --- a/frontend/src/components/Navbar/Navbar.css +++ b/frontend/src/components/Navbar/Navbar.css @@ -1,103 +1,480 @@ -.navbar{ - padding: 20px 0px; +:root { + --navbar-bg: #ffffff; + --bg-color: #ffffff; + --text-color: #2d3748; + --accent-color: #ff6347; + --accent-hover: #e55339; + --border-color: #e2e8f0; + --hover-bg: #f7fafc; + --active-bg: #fed7d7; + --theme-toggle-bg: #f7fafc; +} + +[data-theme="dark"] { + --navbar-bg: #11151d; + --bg-color: #1a202c; + --text-color: #e2e8f0; + --accent-color: #ff6347; + --accent-hover: #e55339; + --border-color: #2d3748; + --hover-bg: #2d3748; + --active-bg: #744210; + --theme-toggle-bg: #2d3748; +} + +body { + width: 100%; + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.3s, color 0.3s; + padding-top: 65px; + padding-bottom: 0; +} + +.navbar { + width: 100%; + position: fixed; + top: 0; left: 0; right: 0; + z-index: 1000; + background: var(--navbar-bg); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + height: 65px; + padding-left: 32px; + padding-right: 48px; +} + +.navbar-logo { + width: 10%; + max-width: 80px; + display: flex; + align-items: center; + /* margin-left: 0; + margin-right: 15%; */ + transition: transform 0.14s cubic-bezier(0.4,0.1,0.6,0.9); +} +.navbar-logo img{ + width: 60px; + height: 60px; +} +.navbar-logo img:hover{ + transform: scale(1.03); +} + +.app-icon { + height: 60px; + width: 60px; + border-radius: 10px; +} + +.navbar-menu, +.navbar-menu-desktop { + width: 60%; + max-width: 550px; + display: flex; + align-items: center; + padding-right: 2rem; + /* gap: 0.75rem; + flex: 1; + margin: 0 24px; */ +} +.navbar-menu-mobile { + display: none; +} + +.navbar-right { + width: 20%; + max-width: 200px; + display: flex; + align-items: center; + gap: 10px; + /* margin-right: 10px; */ +} + +/* ACTION BUTTONS */ +.theme-toggle { + background: var(--theme-toggle-bg); + border: none; + color: var(--text-color); + padding: 8px; + border-radius: 50%; + cursor: pointer; + transition: all 0.3s; + display: flex; + align-items: center; + justify-content: center; + width: 40px; height: 40px; +} +.theme-toggle:hover { background: var(--accent-color); color: #fff; transform: rotate(180deg); } + +.signin-button { + background: var(--accent-color); + color: #fff; + border: none; + padding: 10px 18px; + border-radius: 8px; + cursor: pointer; + display: flex; align-items: center; gap: 6px; + font-size: 14px; font-weight: 500; + transition: all 0.3s; +} +.signin-button:hover { + background: var(--accent-hover); + box-shadow: 0 4px 12px rgba(255,99,71,0.2); + transform: translateY(-1px); +} + +.navbar-cart { + position: relative; +} + +/* Add to Navbar.css */ +.cart-badge { + position: absolute; + top: 2px; + right: 2px; + background: #fc4b32; + color: #fff; + font-size: 12px; + font-weight: bold; + min-width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + box-shadow: 0 2px 8px rgba(252, 75, 50, 0.2); +} + + +@keyframes pulse { + 0% {transform: scale(1);} + 50% {transform: scale(1.25);} + 100% {transform: scale(1);} +} + +.icon-button { + background: transparent; + border: none; + color: var(--text-color); + padding: 10px; border-radius: 8px; + display: flex; align-items: center; justify-content: center; + cursor: pointer; + transition: all 0.3s; +} +.icon-button:hover { background: var(--hover-bg); color: var(--accent-color); } + +.nav-item { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-color); + text-decoration: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 15px; + font-weight: 500; + transition: all 0.3s; + position: relative; + cursor: pointer; + white-space: nowrap; +} +.nav-item:hover { + background: var(--hover-bg); + color: var(--accent-color); +} +.nav-item.active { + color: var(--accent-color); + background: var(--active-bg); +} +.nav-item.active::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 20px; height: 2px; + background: var(--accent-color); + border-radius: 1px; +} + +/* DESKTOP / TABLET RESPONSIVE ADJUSTMENTS */ + +@media screen and (min-width: 1200px) { + .navbar{ + width: 100%; display: flex; justify-content: space-between; - align-items: center; -} -.navbar-menu{ + } + .navbar-logo img{ + width: 70px; + height: 70px; + } + .navbar-logo{ + width: 100px; + } + .navbar-menu.navbar-menu-desktop{ + width: 1000px; + max-width: 1000px; + gap: 1rem; + padding-left: 100px; + } + .app-icon { + height: 60px; + width: 60px; + border-radius: 8px; + } + .nav-item { + gap: 8px; + padding: 0.5rem 0.6rem; + border-radius: 8px; + font-size: 16px; + } + .navbar-right{ + width: 200px; + max-width: 200px; + padding-right:0.5rem; + gap: 0.01rem; + } + .signin-button { + width: 200px; + padding: 0.5rem 0.3rem; + } +} + +@media screen and (min-width: 1100px) { + .navbar{ + width: 100%; display: flex; - list-style: none; - gap: 20px; - color: #49557e; - font-size: 18px; + justify-content: space-between; + } + .navbar-logo img{ + width: 70px; + height: 70px; + } + .navbar-logo{ + width: 100px; + } + .navbar-menu.navbar-menu-desktop{ + width: 800px; + max-width: 800px; + padding-left: 50px; + } + .app-icon { + height: 60px; + width: 60px; + border-radius: 8px; + } + .nav-item { + gap: 10px; + padding: 0.5rem 0.6rem; + border-radius: 8px; + font-size: 16px; + } + + .navbar-right{ + width: 200px; + max-width: 250px; + padding-right:0.5rem; + gap: 0.01rem; + } + + .theme-toggle { + padding:10px ; + width: 50px; height: 50px; + } + .navbar-cart { + width: 100%; + height: 100%; + } + .signin-button { + width: 180px; + padding: 0.4rem 0.2rem; + border-radius: 8px; + gap: 0.2rem; + font-size: 15px; + font-weight: 500; + } +} + +@media screen and (min-width:751px) and (max-width:1100px) { + .navbar-logo img{ + width: 50px; + height: 50px; + } + .navbar-logo{ + width: 80px; + left: 0; + margin-left: 0; + } + .navbar-menu.navbar-menu-desktop{ + width: 500px; + max-width: 500px; + display: flex; + padding-left: 1rem; + } + .nav-item { + display: flex; + align-items: center; + gap: 0.1rem; + padding: 0.2rem 0.4rem; + border-radius: 8px; + font-size: 12px; + } + + .app-icon { + height: 30px; + width: 30px; + border-radius: 6px; + } + .navbar-right{ + width: 150px; + max-width: 150px; + margin-left: 30px; + padding-right:10px; + gap: 0.1rem; + } + + .theme-toggle { + padding: 5px; + width: 30px; height: 30px; + } + .navbar-cart { + width: 50%; + height: 50%; + } + .signin-button { + width: 100px; + background: var(--accent-color); + color: #fff; + border: none; + padding: 0.3rem 0.4rem; + border-radius: 8px; + gap: 0.1rem; + font-size: 12px; + font-weight: 500; + } } -.navbar-right{ +@media (max-width: 750px) { + /* Hide inline menu, show mobile bottom nav */ + .navbar-menu, .navbar-menu-desktop { + display: none !important; + } + + body { + width: 100%; + /* padding-top: 44px; padding-bottom: 64px; */ + } + .navbar { + width: 100dvw; display: flex; + justify-content: space-between; + padding-inline: 1.5rem; + } + .app-icon { height: 32px; width: 32px; } + /* .navbar-logo { margin-right: 0; } */ + + .navbar-right{ + width: 170px; + } + .theme-toggle, .icon-button { width: 34px; height: 34px; padding: 8px;} + .signin-button { + font-size: 12px; + padding: 7px 9px; + } + + .navbar-menu-mobile { + display: flex !important; + position: fixed; + left: 0; right: 0; bottom: 0; + z-index: 1101; + background: var(--navbar-bg); + border-top: 1px solid var(--border-color); + box-shadow: 0 -2px 18px 0 rgba(0,0,0,0.04); + margin: 0; + padding: 0; + gap: 0.25rem; + justify-content: space-around; align-items: center; - gap: 40px; + width: 100vw; + min-width: 0; + max-width: 100vw; + overflow-x: auto; + height: 56px; + } + .navbar-menu-mobile .nav-item { + flex-direction: column; + gap: 2px; + padding: 2px 2px; + font-size: 11px; + min-width: 44px; + max-width: 64px; + width: 100%; + border-radius: 10px; + align-items: center; + justify-content: center; + word-break: break-word; + background: none; + font-weight: 500; + transition: all 0.3s; + } + .navbar-menu-mobile .nav-item svg { + width: 20px; height: 20px; + margin-bottom: 0; + display: block; + margin-left: auto; + margin-right: auto; + } + .navbar-menu-mobile .nav-item span { + font-size: 10px; + font-weight: 500; + text-align: center; + display: block; + white-space: normal; + line-height: 1.1; + word-break: break-word; + } + .navbar-menu-mobile .nav-item.active { + color: var(--accent-color); + background: none; + transform: translateY(-2px); + } + .navbar-menu-mobile .nav-item.active::after, + .navbar-menu-mobile .nav-item:hover::after { + display: none; + } + .navbar-menu-mobile .nav-item:hover { + background: none; + color: var(--accent-color); + transform: translateY(-2px); + } } -.navbar button{ - background: transparent; - font-size: 16px; - color: 49557e; - border: 1px solid tomato; - padding: 10px 30px; - border-radius: 50px; - cursor: pointer; - transition: 0.6s; -} - -.navbar button:hover{ - background-color: #f4c3ba; -} - -.navbar .active{ - padding-bottom: 2px; - border-bottom: 2px solid #49557e; -} - -.navbar li{ - cursor: pointer; -} - -.navbar-search-icon{ - position: relative; -} - -.navbar-search-icon .dot{ - position: absolute; - min-width: 10px; - min-height: 10px; - background-color: tomato; - border-radius: 5px; - top: -8px; - right: -8px; -} - -@media (max-width:1050px) { - .navbar .logo{ - width: 140px; - } - .navbar-menu{ - gap: 20px; - font-size: 17px; - } - .navbar-right{ - gap: 30px; - } - .navbar-right img{ - width: 22px; - } - .navbar-right button{ - padding: 8px 25px; - } -} - -@media (max-width:900px) { - .navbar .logo{ - width: 120px; - } - .navbar-menu{ - gap: 15px; - font-size: 16px; - } - .navbar-right{ - gap: 20px; - } - .navbar-right img{ - width: 20px; - } - .navbar-right button{ - padding: 7px 20px; - font-size: 15px; - } -} - -@media (max-width:750px) { - .navbar-menu{ - display: none; - } +.user-info { + display: flex; + align-items: center; + gap: 0.5rem; } +.user-avatar { + background-color: #333; + color: white; + border-radius: 50%; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; +} + +.logout-button { + background: transparent; + border: none; + color: red; + cursor: pointer; +} diff --git a/frontend/src/components/Navbar/Navbar.jsx b/frontend/src/components/Navbar/Navbar.jsx index 7b1c21b4..67b70aff 100644 --- a/frontend/src/components/Navbar/Navbar.jsx +++ b/frontend/src/components/Navbar/Navbar.jsx @@ -1,30 +1,177 @@ -import React, { useContext, useState } from 'react'; -import './Navbar.css'; -import {assets} from '../../assets/frontend_assets/assets'; -import { Link } from 'react-router-dom'; -import { StoreContext } from '../context/StoreContext'; +import React, { useContext, useState, useEffect } from "react"; +import "./Navbar.css"; +import { Link, useNavigate, useLocation } from "react-router-dom"; +import { StoreContext } from "../context/StoreContext"; +import { ThemeContext } from "../context/ThemeContext"; +import { assets } from "../../assets/frontend_assets/assets"; +import { + Home, + Menu, + Smartphone, + Heart, + Phone, + ShoppingCart, + User, + Sun, + Moon, + HelpCircle, + Utensils, +} from "lucide-react"; -const Navbar = ({setShowLogin}) => { +const Navbar = ({ setShowLogin }) => { const [menu, setMenu] = useState("home"); - const {getTotalCartAmount} = useContext(StoreContext) + const { getTotalCartAmount } = useContext(StoreContext); + const { theme, toggleTheme } = useContext(ThemeContext); + const [user, setUser] = useState(null); + + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + const storedUser = JSON.parse(localStorage.getItem("user")); + setUser(storedUser); + }, []); + + const handleNavMenuClick = (event, menuName, id) => { + event.preventDefault(); + setMenu(menuName); + if (id) { + if (location.pathname !== "/") { + localStorage.setItem("scrollToMenu", "true"); + navigate("/"); + } else { + const section = document.getElementById(id); + if (section) section.scrollIntoView({ behavior: "smooth" }); + } + } + }; + + const handleLogout = () => { + localStorage.removeItem("user"); + localStorage.removeItem("token"); + setUser(null); + window.location.reload(); + }; + + const navMenu = ( + <> + setMenu("home")} + className={`nav-item ${menu === "home" ? "active" : ""}`} + > + + Home + + setMenu("restaurants")} + className={`nav-item ${menu === "restaurants" ? "active" : ""}`} + > + + Restaurant + + handleNavMenuClick(e, "menu", "explore-menu")} + > + + Menu + + handleNavMenuClick(e, "mobile-app", "appdownload")} + > + + Mobile App + + setMenu("wishlist")} + className={`nav-item ${menu === "wishlist" ? "active" : ""}`} + > + + Wishlist + + setMenu("contact-us")} + className={`nav-item ${menu === "contact-us" ? "active" : ""}`} + > + + Contact + + handleNavMenuClick(e, "faq", "faq")} + > + + FAQ + + + ); + + const totalCartItems = Object.values(useContext(StoreContext).cartItems || {}).reduce( + (sum, qty) => sum + qty, + 0 + ); + return ( -
- - -
- -
- -
+ <> + {/* Top Navigation Bar */} +
+ {/* Logo */} + + app icon + + + {/* Desktop menu (center, hidden on mobile) */} + + + {/* Right action buttons */} +
+ + +
+ + + {totalCartItems > 0 && ( +
{totalCartItems}
+ )} + +
+ + {user ? ( +
+
+ {user.name?.charAt(0).toUpperCase()} +
+ {user.name} + +
+ ) : ( + + )}
-
-
+ + {/* Mobile bottom nav */} + + ); }; diff --git a/frontend/src/components/QRCode/QRCode.css b/frontend/src/components/QRCode/QRCode.css new file mode 100644 index 00000000..41d5338a --- /dev/null +++ b/frontend/src/components/QRCode/QRCode.css @@ -0,0 +1,136 @@ +.qr-code-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(5px); +} + +.qr-code-modal { + background: white; + border-radius: 20px; + padding: 0; + max-width: 400px; + width: 90%; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + animation: slideIn 0.3s ease-out; +} + +.qr-code-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 25px 15px; + border-bottom: 1px solid #eee; +} + +.qr-code-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; +} + +.close-btn { + background: none; + border: none; + cursor: pointer; + padding: 5px; + border-radius: 50%; + transition: background-color 0.2s ease; + color: #666; +} + +.close-btn:hover { + background-color: #f5f5f5; + color: #333; +} + +.qr-code-content { + padding: 25px; + text-align: center; +} + +.qr-code-image { + width: 200px; + height: 200px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; +} + +.qr-code-description { + color: #666; + font-size: 14px; + line-height: 1.5; + margin-bottom: 25px; +} + +.qr-code-actions { + display: flex; + justify-content: center; +} + +.download-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 25px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3); +} + +.download-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); +} + +.download-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@media (max-width: 480px) { + .qr-code-modal { + width: 95%; + margin: 10px; + } + + .qr-code-image { + width: 180px; + height: 180px; + } + + .qr-code-header { + padding: 15px 20px 10px; + } + + .qr-code-content { + padding: 20px; + } +} \ No newline at end of file diff --git a/frontend/src/components/QRCode/QRCode.jsx b/frontend/src/components/QRCode/QRCode.jsx new file mode 100644 index 00000000..f27947be --- /dev/null +++ b/frontend/src/components/QRCode/QRCode.jsx @@ -0,0 +1,67 @@ +import React, { useState } from 'react'; +import { X, Download } from 'lucide-react'; +import './QRCode.css'; + +const QRCode = ({ url, onClose }) => { + const [isDownloading, setIsDownloading] = useState(false); + + // Simple QR code generation using Google Charts API + const qrCodeUrl = `https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=${encodeURIComponent(url)}`; + + const downloadQRCode = async () => { + setIsDownloading(true); + try { + const response = await fetch(qrCodeUrl); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'wishlist-qr-code.png'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (error) { + console.error('Failed to download QR code:', error); + } finally { + setIsDownloading(false); + } + }; + + return ( +
+
e.stopPropagation()}> +
+

πŸ“± Scan to Share

+ +
+ +
+ QR Code +

+ Scan this QR code with your phone to open the shared wishlist +

+ +
+ +
+
+
+
+ ); +}; + +export default QRCode; \ No newline at end of file diff --git a/frontend/src/components/ScrollToBottomButton/ScrollToBottomButton.css b/frontend/src/components/ScrollToBottomButton/ScrollToBottomButton.css new file mode 100644 index 00000000..9d3a8d37 --- /dev/null +++ b/frontend/src/components/ScrollToBottomButton/ScrollToBottomButton.css @@ -0,0 +1,17 @@ +.scroll-to-bottom-btn { + position: fixed; + bottom: 80px; /* above the scroll-to-top button */ + left: 20px; + z-index: 999; + background-color: #fc4b32; + color: white; + padding: 10px; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 0 10px #00000040; + transition: transform 0.2s ease; +} + +.scroll-to-bottom-btn:hover { + transform: scale(1.1); +} diff --git a/frontend/src/components/ScrollToBottomButton/ScrollToBottomButton.jsx b/frontend/src/components/ScrollToBottomButton/ScrollToBottomButton.jsx new file mode 100644 index 00000000..418d7ae6 --- /dev/null +++ b/frontend/src/components/ScrollToBottomButton/ScrollToBottomButton.jsx @@ -0,0 +1,40 @@ +// src/components/ScrollToBottomButton/ScrollToBottomButton.jsx +import React, { useState, useEffect } from "react"; +import { FaArrowDown } from "react-icons/fa"; +import "./ScrollToBottomButton.css"; + +const ScrollToBottomButton = () => { + const [visible, setVisible] = useState(false); + + useEffect(() => { + const handleScroll = () => { + const atBottom = + window.innerHeight + window.scrollY >= document.body.scrollHeight - 50; + setVisible(!atBottom); + }; + + window.addEventListener("scroll", handleScroll); + window.addEventListener("resize", handleScroll); + handleScroll(); + + return () => { + window.removeEventListener("scroll", handleScroll); + window.removeEventListener("resize", handleScroll); + }; + }, []); + + const scrollToBottom = () => { + window.scrollTo({ + top: document.body.scrollHeight, + behavior: "smooth", + }); + }; + + return visible ? ( +
+ +
+ ) : null; +}; + +export default ScrollToBottomButton; diff --git a/frontend/src/components/ScrollToTopButton/ScrollToTopButton.css b/frontend/src/components/ScrollToTopButton/ScrollToTopButton.css new file mode 100644 index 00000000..d3d17162 --- /dev/null +++ b/frontend/src/components/ScrollToTopButton/ScrollToTopButton.css @@ -0,0 +1,17 @@ +.scroll-to-top-btn { + position: fixed; + bottom: 20px; /* ⬅️ bottom left corner */ + left: 20px; /* ⬅️ changed from right to left */ + z-index: 999; + background-color: #FC4B32; + color: white; + padding: 10px; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 0 10px #00000040; + transition: transform 0.2s ease; +} + +.scroll-to-top-btn:hover { + transform: scale(1.1); +} diff --git a/frontend/src/components/ScrollToTopButton/ScrollToTopButton.jsx b/frontend/src/components/ScrollToTopButton/ScrollToTopButton.jsx new file mode 100644 index 00000000..e54c916f --- /dev/null +++ b/frontend/src/components/ScrollToTopButton/ScrollToTopButton.jsx @@ -0,0 +1,29 @@ +// components/ScrollToTop/ScrollToTopButton.jsx +import React, { useEffect, useState } from 'react'; +import { FaArrowUp } from 'react-icons/fa'; +import './ScrollToTopButton.css'; + +const ScrollToTop = () => { + const [visible, setVisible] = useState(false); + + useEffect(() => { + const toggleVisible = () => { + setVisible(window.scrollY > 200); + }; + + window.addEventListener('scroll', toggleVisible); + return () => window.removeEventListener('scroll', toggleVisible); + }, []); + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return visible ? ( +
+ +
+ ) : null; +}; + +export default ScrollToTop; diff --git a/frontend/src/components/SearchBar/SearchBar.css b/frontend/src/components/SearchBar/SearchBar.css new file mode 100644 index 00000000..8d880e77 --- /dev/null +++ b/frontend/src/components/SearchBar/SearchBar.css @@ -0,0 +1,328 @@ +.search-bar-container { + position: relative; + width: 100%; + max-width: 700px; + margin: 0 auto; + /* padding-top: 20px; */ + margin-bottom: 20px; + z-index: 10; +} + +.search-bar { + display: flex; + align-items: center; + background: var(--bg-color, #ffffff); + border: 2px solid var(--border-color, #e2e8f0); + + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); + border-radius: 12px; + padding: 4px; + transition: all 0.3s ease; + box-shadow: 0 2px 8px var(--shadow-color); + margin: 0; +} + +.search-bar:focus-within { + border-color: var(--accent-color, #ff6347); + box-shadow: 0 0 0 3px rgba(255, 99, 71, 0.1); +} + +.search-input-wrapper { + display: flex; + align-items: center; + flex: 1; + position: relative; + padding: 0; +} + +.search-icon-sb { + color: var(--text-secondary, #64748b); + pointer-events: none; + margin-right: 20px; + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); +} + +.search-input { + border: none; + outline: none; + background: transparent; + font-size: 16px; + color: var(--text-color, #2d3748); + width: 80%; + padding: 12px 0px 12px 36px; + margin-left: 9px; + font-family: inherit; +} + +.search-input::placeholder { + color: var(--text-secondary, #64748b); +} + +.clear-button { + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 50%; + color: var(--text-secondary, #64748b); + transition: all 0.2s ease; + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; +} + +.clear-button:hover { + background-color: var(--hover-bg, #f1f5f9); + color: var(--text-color, #2d3748); +} + +.search-button { + background: var(--accent-color, #ff6347); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.search-button:hover { + background: var(--accent-hover, #e55339); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(255, 99, 71, 0.3); +} + +.search-button:active { + transform: translateY(0); +} + +/* Dropdown Styles */ +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-color, #ffffff); + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 12px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); + z-index: 1000; + margin-top: 8px; + + max-height: 400px; + overflow-y: auto; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); +} + +.suggestions-section { + padding: 12px 0; +} + +.suggestions-section:not(:last-child) { + border-bottom: 1px solid var(--border-light, #f1f5f9); +} + +.section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary, #64748b); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.suggestion-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 0; +} + +.suggestion-item:hover { + background-color: var(--hover-bg, #f8fafc); +} + +.suggestion-icon { + color: var(--text-secondary, #64748b); + flex-shrink: 0; +} + +.suggestion-text { + color: var(--text-color, #2d3748); + font-size: 14px; + flex: 1; +} + +.suggestion-text mark { + background-color: var(--accent-light, #fed7d7); + color: var(--accent-color, #ff6347); + padding: 0 2px; + border-radius: 2px; +} + +.recent-item .suggestion-icon { + color: var(--blue-500, #3b82f6); +} + +.popular-item .suggestion-icon { + color: var(--green-500, #10b981); +} + +.no-results { + padding: 20px 16px; + text-align: center; + color: var(--text-secondary, #64748b); + font-size: 14px; +} + +.default-suggestions { + max-height: 350px; + overflow-y: auto; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .search-bar-container { + max-width: 100%; + } + + .search-bar { + border-radius: 10px; + padding: 2px; + margin: 0; + } + + .search-input { + font-size: 16px; /* Prevents zoom on iOS */ + padding: 10px 0; + } + + .search-button { + padding: 10px 16px; + font-size: 13px; + } + + .search-dropdown { + border-radius: 10px; + margin-top: 6px; + } + + .suggestion-item { + padding: 14px 16px; + } +} + +@media (max-width: 480px) { + .search-bar-container { + padding-top: 40px; + max-width: 100vw; + margin: 0 0 10px 0; + } + .search-bar { + margin: 8px 2vw; + border-radius: 8px; + padding: 1px; + min-width: 0; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); + } + .search-input-wrapper { + padding: 0 4px; + } + .search-input { + font-size: 15px; + padding: 8px 0 8px 32px; + } + .search-icon-sb { + left: 8px; + width: 18px; + height: 18px; + } + .search-button { + padding: 7px 10px; + font-size: 11px; + border-radius: 6px; + } + .clear-button { + padding: 2px; + } + .search-dropdown { + border-radius: 8px; + margin-top: 4px; + font-size: 13px; + left: 0; + right: 0; + min-width: 0; + max-width: 100vw; + } + .suggestion-item { + padding: 10px 10px; + font-size: 13px; + } +} + +/* Dark Theme Support */ +[data-theme="dark"] .search-bar-container { + --bg-color: #2d3748; + --text-color: #e2e8f0; + --text-secondary: #a0aec0; + --border-color: #4a5568; + --border-light: #4a5568; + --hover-bg: #4a5568; + --shadow-color: #ffffff12; + + --accent-light: #744210; + --blue-500: #63b3ed; + --green-500: #68d391; +} + +/* Custom scrollbar for dropdown */ +.search-dropdown::-webkit-scrollbar { + width: 6px; +} + +.search-dropdown::-webkit-scrollbar-track { + background: transparent; +} + +.search-dropdown::-webkit-scrollbar-thumb { + background: var(--border-color, #e2e8f0); + border-radius: 3px; +} + +.search-dropdown::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary, #64748b); +} + +/* Animation for dropdown appearance */ +.search-dropdown { + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/frontend/src/components/SearchBar/SearchBar.jsx b/frontend/src/components/SearchBar/SearchBar.jsx new file mode 100644 index 00000000..15351d19 --- /dev/null +++ b/frontend/src/components/SearchBar/SearchBar.jsx @@ -0,0 +1,236 @@ +import React, { useState, useRef, useEffect, useMemo } from "react"; +import { Search, X, Clock, TrendingUp } from "lucide-react"; +import "./SearchBar.css"; + +const SearchBar = ({ + placeholder = "Search for food, restaurants, cuisines...", + onSearch, + suggestions = [], + recentSearches = [], + popularSearches = [], + showSuggestions = true, + className = "", +}) => { + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [filteredSuggestions, setFilteredSuggestions] = useState([]); + const searchRef = useRef(null); + const inputRef = useRef(null); + + // Sample data for demonstration + const defaultSuggestions = useMemo( + () => [ + "Pizza Margherita", + "Chicken Biryani", + "Pasta Carbonara", + "Sushi Roll", + "Burger Deluxe", + "Thai Green Curry", + "Caesar Salad", + "Fish and Chips", + "Tacos", + "Ramen Noodles", + ], + [] + ); + + const defaultRecentSearches = useMemo( + () => ["Pizza", "Chinese food", "Desserts"], + [] + ); + + const defaultPopularSearches = useMemo( + () => ["Pizza", "Burger", "Sushi", "Biryani", "Pasta"], + [] + ); + + // Use useMemo to prevent recreation on every render + const allSuggestions = useMemo( + () => (suggestions.length > 0 ? suggestions : defaultSuggestions), + [suggestions, defaultSuggestions] + ); + + const allRecentSearches = useMemo( + () => (recentSearches.length > 0 ? recentSearches : defaultRecentSearches), + [recentSearches, defaultRecentSearches] + ); + + const allPopularSearches = useMemo( + () => + popularSearches.length > 0 ? popularSearches : defaultPopularSearches, + [popularSearches, defaultPopularSearches] + ); + + useEffect(() => { + if (query.trim()) { + const filtered = allSuggestions.filter((item) => + item.toLowerCase().includes(query.toLowerCase()) + ); + setFilteredSuggestions(filtered.slice(0, 8)); + } else { + setFilteredSuggestions([]); + } + }, [query, allSuggestions]); + + useEffect(() => { + const handleClickOutside = (event) => { + if (searchRef.current && !searchRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleInputChange = (e) => { + setQuery(e.target.value); + setIsOpen(true); + }; + + const handleSearch = (searchQuery = query) => { + if (searchQuery.trim()) { + onSearch && onSearch(searchQuery.trim()); + setIsOpen(false); + inputRef.current?.blur(); + } + }; + + const handleSuggestionClick = (suggestion) => { + setQuery(suggestion); + handleSearch(suggestion); + }; + + const handleClear = () => { + setQuery(""); + setIsOpen(false); + inputRef.current?.focus(); + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + handleSearch(); + } else if (e.key === "Escape") { + setIsOpen(false); + inputRef.current?.blur(); + } + }; + + const handleFocus = () => { + setIsOpen(true); + }; + + return ( +
+
+
+
+ +
+ + + {query && ( + + )} +
+ +
+ + {isOpen && showSuggestions && ( +
+ {query.trim() ? ( +
+ {filteredSuggestions.length > 0 ? ( + <> +
+ + Suggestions +
+ {filteredSuggestions.map((suggestion, index) => ( +
handleSuggestionClick(suggestion)} + > + + + {suggestion + .split(new RegExp(`(${query})`, "gi")) + .map((part, i) => + part.toLowerCase() === query.toLowerCase() ? ( + {part} + ) : ( + part + ) + )} + +
+ ))} + + ) : ( +
+ No suggestions found for "{query}" +
+ )} +
+ ) : ( +
+ {allRecentSearches.length > 0 && ( +
+
+ + Recent Searches +
+ {allRecentSearches.map((search, index) => ( +
handleSuggestionClick(search)} + > + + {search} +
+ ))} +
+ )} + + {allPopularSearches.length > 0 && ( +
+
+ + Popular Searches +
+ {allPopularSearches.map((search, index) => ( +
handleSuggestionClick(search)} + > + + {search} +
+ ))} +
+ )} +
+ )} +
+ )} +
+ ); +}; + +export default SearchBar; diff --git a/frontend/src/components/Typetext/typetext.css b/frontend/src/components/Typetext/typetext.css new file mode 100644 index 00000000..a34799d9 --- /dev/null +++ b/frontend/src/components/Typetext/typetext.css @@ -0,0 +1,18 @@ +.text-type { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 60px; +} + + +.text-type__cursor { + margin-left: 0.25rem; + display: inline-block; + opacity: 1; +} + +.text-type__cursor--hidden { + display: none; +} \ No newline at end of file diff --git a/frontend/src/components/Typetext/typetext.jsx b/frontend/src/components/Typetext/typetext.jsx new file mode 100644 index 00000000..f5ee9a34 --- /dev/null +++ b/frontend/src/components/Typetext/typetext.jsx @@ -0,0 +1,180 @@ + +import { useEffect, useRef, useState, createElement } from "react"; +import { gsap } from "gsap"; +import "./typetext.css"; + +const TextType = ({ + text, + as: Component = "div", + typingSpeed = 50, + initialDelay = 0, + pauseDuration = 2000, + deletingSpeed = 30, + loop = true, + className = "", + showCursor = true, + hideCursorWhileTyping = false, + cursorCharacter = "|", + cursorClassName = "", + cursorBlinkDuration = 0.5, + textColors = [], + variableSpeed, + onSentenceComplete, + startOnVisible = false, + reverseMode = false, + ...props +}) => { + const [displayedText, setDisplayedText] = useState(""); + const [currentCharIndex, setCurrentCharIndex] = useState(0); + const [isDeleting, setIsDeleting] = useState(false); + const [currentTextIndex, setCurrentTextIndex] = useState(0); + const [isVisible, setIsVisible] = useState(!startOnVisible); + const cursorRef = useRef(null); + const containerRef = useRef(null); + + const textArray = Array.isArray(text) ? text : [text]; + + const getRandomSpeed = () => { + if (!variableSpeed) return typingSpeed; + const { min, max } = variableSpeed; + return Math.random() * (max - min) + min; + }; + + const getCurrentTextColor = () => { + if (textColors.length === 0) return "#d21212ff"; + return textColors[currentTextIndex % textColors.length]; + }; + + useEffect(() => { + if (!startOnVisible || !containerRef.current) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }); + }, + { threshold: 0.1 } + ); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, [startOnVisible]); + + useEffect(() => { + if (showCursor && cursorRef.current) { + gsap.set(cursorRef.current, { opacity: 1 }); + gsap.to(cursorRef.current, { + opacity: 0, + duration: cursorBlinkDuration, + repeat: -1, + yoyo: true, + ease: "power2.inOut", + }); + } + }, [showCursor, cursorBlinkDuration]); + + useEffect(() => { + if (!isVisible) return; + + let timeout; + const currentText = textArray[currentTextIndex]; + const processedText = reverseMode + ? currentText.split("").reverse().join("") + : currentText; + + const executeTypingAnimation = () => { + if (isDeleting) { + if (displayedText === "") { + setIsDeleting(false); + if (currentTextIndex === textArray.length - 1 && !loop) { + return; + } + + if (onSentenceComplete) { + onSentenceComplete(textArray[currentTextIndex], currentTextIndex); + } + + setCurrentTextIndex((prev) => (prev + 1) % textArray.length); + setCurrentCharIndex(0); + timeout = setTimeout(() => { }, pauseDuration); + } else { + timeout = setTimeout(() => { + setDisplayedText((prev) => prev.slice(0, -1)); + }, deletingSpeed); + } + } else { + if (currentCharIndex < processedText.length) { + timeout = setTimeout( + () => { + setDisplayedText( + (prev) => prev + processedText[currentCharIndex] + ); + setCurrentCharIndex((prev) => prev + 1); + }, + variableSpeed ? getRandomSpeed() : typingSpeed + ); + } else if (textArray.length > 1) { + timeout = setTimeout(() => { + setIsDeleting(true); + }, pauseDuration); + } + } + }; + + if (currentCharIndex === 0 && !isDeleting && displayedText === "") { + timeout = setTimeout(executeTypingAnimation, initialDelay); + } else { + executeTypingAnimation(); + } + + return () => clearTimeout(timeout); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentCharIndex, + displayedText, + isDeleting, + typingSpeed, + deletingSpeed, + pauseDuration, + textArray, + currentTextIndex, + loop, + initialDelay, + isVisible, + reverseMode, + variableSpeed, + onSentenceComplete, + ]); + + const shouldHideCursor = + hideCursorWhileTyping && + (currentCharIndex < textArray[currentTextIndex].length || isDeleting); + + return createElement( + Component, + { + ref: containerRef, + className: `text-type ${className}`, + ...props, + }, + + {displayedText} + , + showCursor && ( + + {cursorCharacter} + + ) + ); +}; + +export default TextType; diff --git a/frontend/src/components/context/StoreContext.jsx b/frontend/src/components/context/StoreContext.jsx index 13544000..56077250 100644 --- a/frontend/src/components/context/StoreContext.jsx +++ b/frontend/src/components/context/StoreContext.jsx @@ -1,47 +1,72 @@ import { createContext, useState } from "react"; +import { toast } from 'react-toastify'; // Assuming you are using react-toastify import { food_list } from "../../assets/frontend_assets/assets"; -export const StoreContext= createContext(null) +export const StoreContext = createContext(null); -const StoreContextProvider = (props) =>{ +const StoreContextProvider = (props) => { const [cartItems, setCartItems] = useState({}); + + /** + * Adds an item to the cart or increments its quantity if it already exists. + * @param {string} itemId - The ID of the food item to add. + */ const addToCart = (itemId) => { - if(!cartItems[itemId]) { - setCartItems((prev) => ({...prev,[itemId] : 1})); - }else{ - setCartItems((prev) => ({...prev,[itemId] : prev[itemId]+1})); + // Check if the item is not already in the cart + if (!cartItems[itemId]) { + // Add the item to the cart with a quantity of 1 + setCartItems((prev) => ({ ...prev, [itemId]: 1 })); + } else { + // If the item is already in the cart, increment its quantity + setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] + 1 })); } - } + // Optional: Add a success notification + toast.success("Item added to cart!"); + }; + /** + * Removes an item from the cart by decrementing its quantity. + * @param {string} itemId - The ID of the food item to remove. + */ const removeFromCart = (itemId) => { - setCartItems((prev) => ({...prev,[itemId] : prev[itemId]-1})); - } + if (cartItems[itemId] > 0) { + setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] - 1 })); + toast.error("Item removed from cart!"); + } + }; - const getTotalCartAmount = ()=>{ + /** + * Calculates the total amount for all items in the cart. + * @returns {number} - The total cart amount. + */ + const getTotalCartAmount = () => { let totalAmount = 0; - for(const item in cartItems){ - if(cartItems[item] > 0){ - let itemInfo = food_list.find((el)=>el._id === item); - totalAmount += itemInfo.price * cartItems[item]; + for (const item in cartItems) { + if (cartItems[item] > 0) { + // Find the item details from the master food list + let itemInfo = food_list.find((product) => product._id === item); + if (itemInfo) { + totalAmount += itemInfo.price * cartItems[item]; + } } } return totalAmount; - } - + }; const contextValue = { food_list, cartItems, setCartItems, - removeFromCart, addToCart, - getTotalCartAmount - } + removeFromCart, + getTotalCartAmount, + }; + return ( {props.children} - ) -} + ); +}; -export default StoreContextProvider \ No newline at end of file +export default StoreContextProvider; diff --git a/frontend/src/components/context/ThemeContext.jsx b/frontend/src/components/context/ThemeContext.jsx new file mode 100644 index 00000000..5d4e163b --- /dev/null +++ b/frontend/src/components/context/ThemeContext.jsx @@ -0,0 +1,26 @@ +import React, { createContext, useState, useEffect } from 'react'; + +export const ThemeContext = createContext(); + +const ThemeContextProvider = ({ children }) => { + const [theme, setTheme] = useState(() => + localStorage.getItem("theme") || "light" + ); + + const toggleTheme = () => { + setTheme(prev => (prev === "light" ? "dark" : "light")); + }; + + useEffect(() => { + localStorage.setItem("theme", theme); + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); + + return ( + + {children} + + ); +}; + +export default ThemeContextProvider; diff --git a/frontend/src/index.css b/frontend/src/index.css index bbb119d9..18722f30 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,32 +1,108 @@ -@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap"); -*{ +html, +body, +#root { + height: 100%; + margin: 0; + padding: 0; +} + +* { margin: 0; padding: 0; box-sizing: border-box; - font-family: 'Outfit'; + font-family: "Outfit", sans-serif; scroll-behavior: smooth; } -body{ - min-height: 100vh; +body { + background-color: var(--bg-color, #ffffff) !important; } -a{ +a { text-decoration: none; color: inherit; } -.app{ - width: 80%; - margin: auto; +.app { + width: 100%; + margin-top: 60px; + height: 100%; +} + +/* Typewriter caret */ +@keyframes blink { + 0%, + 100% { + border-color: transparent; + } + 50% { + border-color: black; + } +} + +.animate-pulse { + animation: blink 1s step-end infinite; +} + +/* Scooter animation */ +@keyframes scooterMove { + 0% { + transform: translateX(-100%); + } + 50% { + transform: translateX(50%); + } + 100% { + transform: translateX(110%); + } +} + +.animate-scooter { + animation: scooterMove 5s linear infinite; } +/* Optional FadeIn (if needed) */ @keyframes FadeIn { - 0%{ - opacity:0; + from { + opacity: 0; } - 100%{ + to { opacity: 1; } -} \ No newline at end of file +} + +/* Theme support */ +:root { + --bg-color: #ffffff; + --text-color: #111111; + --title-color: #111111; + --border-color: #d1d5db; + --box-shadow: #00000015; + --hover-box-shadow: rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] { + --bg-color: #1a1a1a; + --text-color: #eeeeee; + --title-color: #ffffff; + --border-color: #374151; + --box-shadow: #ffffff12; + --hover-box-shadow: hsla(0, 0%, 90%, 0.2); +} + + +h1, +h2, +h3, +h4, +h5, +h6, +p, +a, +span, +li { + color: var(--text-color); +} + diff --git a/frontend/src/lib/apiRequest.js b/frontend/src/lib/apiRequest.js new file mode 100644 index 00000000..808ce7aa --- /dev/null +++ b/frontend/src/lib/apiRequest.js @@ -0,0 +1,8 @@ +import axios from "axios"; + +const apiRequest = axios.create({ + baseURL: "http://localhost:4000", + withCredentials: true, +}); + +export default apiRequest; \ No newline at end of file diff --git a/frontend/src/pages/AboutPage/AboutPage.css b/frontend/src/pages/AboutPage/AboutPage.css new file mode 100644 index 00000000..0c205dd4 --- /dev/null +++ b/frontend/src/pages/AboutPage/AboutPage.css @@ -0,0 +1,232 @@ +.about-container { + font-family: "Poppins", sans-serif; + color: #222; + background: #fff8f0; +} + +.section, +.hero-section { + animation: fadeIn 0.7s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Section Wrapper */ +.section { + padding: 70px 20px; + max-width: 1100px; + margin: auto; + margin-bottom: 60px; +} + +/* Hero Section */ +.hero-section { + text-align: center; + padding: 100px 20px; + background: linear-gradient(135deg, #ff7e5f, #ffb347); + color: white; + border-radius: 16px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); +} + +.hero-section h1 { + font-size: 3.2rem; + font-weight: 700; + margin-bottom: 15px; + letter-spacing: 1.2px; +} + +.hero-section p { + font-size: 1.2rem; + max-width: 650px; + margin: auto; + opacity: 0.95; +} + +/* Story Section */ +.story-section { + background: transparent; + padding: 60px 20px 40px; + text-align: center; + box-shadow: none; +} + +.story-section .story-content { + display: flex; + flex-direction: column; + gap: 30px; + align-items: center; +} + +.story-text { + max-width: 750px; +} + +.story-text h2 { + font-size: 2.4rem; + margin-bottom: 15px; + color: #ff7e5f; + position: relative; +} + +.story-text h2::after { + content: ""; + display: block; + width: 60px; + height: 3px; + background: #ffb347; + margin: 10px auto 0; + border-radius: 2px; +} + +.story-text p { + line-height: 1.7; + margin-bottom: 12px; + color: #444; + font-size: 1.05rem; +} + +.story-image img { + max-width: 40%; + border-radius: 16px; + transition: transform 0.4s ease, box-shadow 0.4s ease; + display: block; + margin: 0 auto; +} + +/* Mission Section */ +.mission-section { + background: #fff4e0; + text-align: center; + border-radius: 16px; + padding: 60px 20px; +} + +.mission-section h2 { + font-size: 2.2rem; + font-weight: 600; + margin-bottom: 20px; + color: #ff7e5f; +} + +.mission-quote { + font-style: italic; + margin-bottom: 40px; + font-size: 1.15rem; + color: #666; +} + +.mission-cards { + display: flex; + flex-wrap: wrap; + gap: 25px; + justify-content: center; +} + +.mission-card { + background: white; + border-radius: 16px; + padding: 30px; + width: 280px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.mission-card:hover { + transform: translateY(-8px); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15); +} + +.mission-card .icon { + font-size: 2.5rem; + margin-bottom: 12px; + color: #ff7e5f; +} + +/* Achievements Section */ +.achievements-section { + background: #222; + color: white; + text-align: center; + border-radius: 16px; + padding: 60px 20px; + margin-top: 40px; /* Extra gap above Achievements */ +} + +.achievements-section h2 { + color: #ffb347; + margin-bottom: 25px; +} + +.achievements-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 25px; +} + +.achievement-number { + font-size: 2.2rem; + font-weight: bold; + color: #ffb347; +} + +/* CTA Section */ +.cta-section { + text-align: center; + background: linear-gradient(135deg, #ffb347, #ffcc33); + color: #222; + padding: 60px 20px; + border-radius: 16px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.cta-section h2 { + font-size: 2.2rem; + margin-bottom: 15px; + font-weight: 600; +} + +.cta-section p { + max-width: 600px; + margin: auto; + margin-bottom: 25px; + font-size: 1.05rem; +} + +.cta-button { + background: white; + border: none; + padding: 14px 32px; + font-size: 1.05rem; + border-radius: 10px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s ease, transform 0.25s ease; +} + +.cta-button:hover { + background: #f9f9f9; + transform: scale(1.07); +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .story-image img { + max-width: 80%; /* Larger on smaller screens for better visibility */ + } +} + +.icon { + font-size: 2.5rem; + color: #ff7f32; + margin-bottom: 10px; +} diff --git a/frontend/src/pages/AboutPage/AboutPage.jsx b/frontend/src/pages/AboutPage/AboutPage.jsx new file mode 100644 index 00000000..0010af2f --- /dev/null +++ b/frontend/src/pages/AboutPage/AboutPage.jsx @@ -0,0 +1,112 @@ +import React from "react"; +import "./AboutPage.css"; +import { IoFastFoodOutline } from "react-icons/io5"; +import { PiChefHatFill } from "react-icons/pi"; +import { BiSolidCategory } from "react-icons/bi"; +import illustration from "../../assets/frontend_assets/foodie_illustration.png"; + +export default function AboutPage() { + return ( +
+ {/* Hero Section */} +
+

About Foodie

+

+ Where taste meets technology β€” explore, list, and manage a world of + flavors at your fingertips. +

+
+ + {/* Our Story */} +
+
+
+

Our Story

+

+ Foodie was founded with a simple mission β€” to bring the world’s + culinary delights into one place. We wanted to make it easier for + food lovers to discover new dishes, chefs to share their + creations, and restaurants to reach passionate eaters. +

+

+ Today, Foodie has grown into a vibrant community where thousands + of food items are browsed, listed, and managed daily. +

+

+ Our platform combines intuitive design with powerful tools to make + food exploration both fun and efficient. +

+
+ + {/* Image after text */} +
+ Delicious food variety +
+
+
+ + {/* Our Mission */} +
+

Our Mission

+

+ "To connect people through the universal language of food, empowering + everyone to discover, share, and enjoy culinary experiences." +

+
+
+ +

Food Discovery

+

+ Explore an ever-growing library of dishes from around the world. +

+
+
+ +

For Creators

+

+ Give chefs and home cooks a platform to showcase their recipes. +

+
+
+ +

Smart Management

+

Effortlessly list, categorize, and manage your food items.

+
+
+
+ + {/* Achievements */} +
+

Our Impact

+
+
+
10K+
+
Food Items Listed
+
+
+
5K+
+
Active Foodies
+
+
+
1M+
+
Monthly Searches
+
+
+
95%
+
Positive Feedback
+
+
+
+ + {/* Call to Action */} +
+

Join the Foodie Community

+

+ Discover new tastes, share your creations, and be part of a growing + food network. +

+ +
+
+ ); +} diff --git a/frontend/src/pages/Cart/Cart.css b/frontend/src/pages/Cart/Cart.css index fcbbcb01..9ea60271 100644 --- a/frontend/src/pages/Cart/Cart.css +++ b/frontend/src/pages/Cart/Cart.css @@ -1,6 +1,13 @@ .cart{ margin-top: 100px; + margin: 100px; } +@media (max-width: 768px) { + .cart { + margin: 20px; + } +} + .cart-items-title{ display: grid; @@ -98,6 +105,35 @@ cursor: pointer; } +.cart-quantity-controls { + display: flex; + align-items: center; + gap: 10px; +} + +.cart-quantity-controls button { + padding: 4px 10px; + background-color: tomato; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + font-weight: bold; + font-size: 16px; + transition: background 0.2s; +} + +.cart-quantity-controls button:hover { + background-color: darkorange; +} + +.cart-quantity-controls span { + min-width: 20px; + text-align: center; + font-weight: 500; +} + + @media (max-width:750px ){ .cart-bottom{ flex-direction: column-reverse; @@ -105,4 +141,5 @@ .cart-promo-code{ justify-content: start; } -} \ No newline at end of file +} + diff --git a/frontend/src/pages/Cart/Cart.jsx b/frontend/src/pages/Cart/Cart.jsx index a41b01ce..1a929594 100644 --- a/frontend/src/pages/Cart/Cart.jsx +++ b/frontend/src/pages/Cart/Cart.jsx @@ -1,10 +1,98 @@ -import React, { useContext } from "react"; import "./Cart.css"; +import React, { useContext } from "react"; import { StoreContext } from "../../components/context/StoreContext"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, Link } from "react-router-dom"; + const Cart = () => { - const { cartItems, food_list, removeFromCart, getTotalCartAmount } = useContext(StoreContext); + const { cartItems, food_list, removeFromCart, getTotalCartAmount, addToCart } = useContext(StoreContext); const navigate = useNavigate(); + + // Check if cart is empty + const isCartEmpty = getTotalCartAmount() === 0; + + + if (isCartEmpty) { + return ( +
+
+
+
🍽️
+
0
+
+

+ Your Cart is Empty +

+

+ Start your food journey by adding some delicious items! +

+ +
+
+ ); + } + return (
@@ -14,28 +102,29 @@ const Cart = () => {

Price

Quantity

Total

-

Remove



{food_list.map((item, index) => { if (cartItems[item._id] > 0) { return ( - <> +
-

{item.name}

+ {item.name}

${item.price}

-

{cartItems[item._id]}

+
+ + {cartItems[item._id]} + +

${item.price * cartItems[item._id]}

-

removeFromCart(item._id)} className="cross"> - x -


- +
); } + return null; })}
diff --git a/frontend/src/pages/Contactpage.jsx b/frontend/src/pages/Contactpage.jsx new file mode 100644 index 00000000..5c013298 --- /dev/null +++ b/frontend/src/pages/Contactpage.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import ContactForm from "../components/ContactForm/ContactForm"; + +const ContactPage = () => { + return ( +
+ +
+ ); +}; + +export default ContactPage; diff --git a/frontend/src/pages/Home/Home.css b/frontend/src/pages/Home/Home.css index e69de29b..d3c9afc3 100644 --- a/frontend/src/pages/Home/Home.css +++ b/frontend/src/pages/Home/Home.css @@ -0,0 +1,28 @@ +/* Home.css */ + +.home-page { + position: relative; + padding-bottom: 60px; /* room for floating elements */ + background-color: var(--bg-color, #ffffff); +} + +/* Floating back-to-top button */ +.back-to-top { + position: fixed; + bottom: 30px; + right: 25px; + background-color: #ff4f4f; + color: white; + border: none; + padding: 12px 16px; + font-size: 20px; + border-radius: 50%; + cursor: pointer; + z-index: 1000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + transition: background-color 0.3s ease; +} + +.back-to-top:hover { + background-color: #d43f3f; +} diff --git a/frontend/src/pages/Home/Home.jsx b/frontend/src/pages/Home/Home.jsx index 222109c4..800b764a 100644 --- a/frontend/src/pages/Home/Home.jsx +++ b/frontend/src/pages/Home/Home.jsx @@ -1,17 +1,41 @@ -import React,{useState} from 'react' -import './Home.css' -import Header from '../../components/Header/Header' -import ExploreMenu from '../../components/ExploreMenu/ExploreMenu' -import FoodDisplay from '../../components/FoodDisplay/FoodDisplay' +import React, { useState, useEffect } from "react"; +import "./Home.css"; +import Header from "../../components/Header/Header"; +import ExploreMenu from "../../components/ExploreMenu/ExploreMenu"; +import FoodDisplay from "../../components/FoodDisplay/FoodDisplay"; + const Home = () => { - const [category, setCategory] = useState('All'); + + const [category, setCategory] = useState('All'); + const [showButton, setShowButton] = useState(false); + + + useEffect(() => { + const handleScroll = () => { + setShowButton(window.scrollY > 100); + }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + useEffect(() => { + const shouldScroll = localStorage.getItem("scrollToMenu"); + if (shouldScroll === "true") { + const section = document.getElementById("explore-menu"); + if (section) { + section.scrollIntoView({ behavior: "smooth" }); + } + localStorage.removeItem("scrollToMenu"); + } + }, []); + return ( -
-
- - +
+
+ +
- ) -} + ); +}; -export default Home +export default Home; diff --git a/frontend/src/pages/Notfound.jsx b/frontend/src/pages/Notfound.jsx new file mode 100644 index 00000000..a0ec742c --- /dev/null +++ b/frontend/src/pages/Notfound.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import notFoundImage from "../assets/frontend_assets/404.png"; + +const NotFound = () => { + return ( +
+
+ 404 Not Found + Go Back Home +
+
+ ); +}; + +const styles = { + container: { + display: "flex", + justifyContent: "center", + alignItems: "center", + minHeight: "100vh", + padding: "20px", + backgroundColor: "#f8f9fa", + }, + imageContainer: { + position: "relative", + display: "inline-block", + }, + image: { + width: "100%", + maxWidth: "1200px", + minWidth: "500px", + height: "auto", + borderRadius: "15px", + boxShadow: "0 10px 30px rgba(0, 0, 0, 0.1)", + }, + button: { + position: "absolute", + top: "10px", + right: "10px", + padding: "10px 20px", + backgroundColor: "#ff6b35", + color: "white", + textDecoration: "none", + borderRadius: "20px", + fontSize: "14px", + fontWeight: "600", + boxShadow: "0 4px 12px rgba(255, 107, 53, 0.4)", + transition: "all 0.3s ease", + border: "2px solid white", + zIndex: 10, + }, +}; + +export default NotFound; diff --git a/frontend/src/pages/PlaceOrder/Location.css b/frontend/src/pages/PlaceOrder/Location.css new file mode 100644 index 00000000..73c02e28 --- /dev/null +++ b/frontend/src/pages/PlaceOrder/Location.css @@ -0,0 +1,258 @@ +/* Location Popup Overlay */ +.location-popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + animation: fadeIn 0.3s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Modal Container */ +.location-modal { + background: white; + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 80vh; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + overflow: hidden; + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + transform: translateY(50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Header */ +.location-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #eee; + background-color: #d2d0cd; +} + +.location-header h3 { + margin: 0; + color: #333; + font-size: 1.2rem; + font-weight: 600; +} + +.close-btn { + background: none; + border: none; + font-size: 30px; + color: #121111; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background-color 0.2s, color 0.2s; +} + +.close-btn:hover { + background-color: #e9ecef; + color: #333; +} + +/* Content */ +.location-content { + padding: 20px; + overflow-y: auto; /* enables vertical scroll */ + max-height: calc(80vh - 60px); +} + +/* Loading State */ +.loading { + text-align: center; + padding: 40px 20px; + color: #3c3a3a; +} + +.loading p { + margin: 0; + font-size: 1rem; +} + +/* Map Container */ +.map-container { + margin-bottom: 20px; + border-radius: 8px; + overflow: hidden; + border: 1px solid #ddd; +} + +.map { + height: 300px; + width: 100%; +} + +/* Location Details */ +.location-details { + background-color: #f8f9fa; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + border: 1px solid #e9ecef; +} + +.location-details p { + margin: 0 0 8px 0; + color: #333; +} + +.location-details p:last-child { + margin-bottom: 0; +} + +.address-text { + color: #4c4b4b; + font-size: 0.9rem; + line-height: 1.4; +} + +/* Action Buttons */ +.location-actions { + display: flex; + gap: 16px; + justify-content: flex-end; +} + +.confirm-btn, +.cancel-btn { + padding: 12px 24px; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + min-width: 100px; +} + +.confirm-btn { + background-color: tomato; + color: white; +} + +.confirm-btn:hover:not(:disabled) { + background-color: #218838; + transform: translateY(-1px); +} + +.confirm-btn:disabled { + background-color: #6c757d; + cursor: not-allowed; + transform: none; +} + +.cancel-btn { + background-color: #6c757d; + color: white; +} + +.cancel-btn:hover { + background-color: #5a6268; + transform: translateY(-1px); +} + +/* Error Message */ +.error-message { + text-align: center; + padding: 40px 20px; +} + +.error-message p { + color: #dc3545; + margin-bottom: 20px; + font-size: 1rem; +} + +.error-message button { + background-color: #dc3545; + color: white; + border: none; + padding: 12px 24px; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; +} + +.error-message button:hover { + background-color: #c82333; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .location-modal { + width: 95%; + margin: 20px; + max-height: 90vh; + } + + .location-header { + padding: 15px; + } + + .location-content { + padding: 15px; + } + + .map { + height: 250px; + } + + .location-actions { + flex-direction: column; + } + + .confirm-btn, + .cancel-btn { + width: 100%; + } +} + +@media (max-width: 480px) { + .location-modal { + width: 100%; + height: 100%; + max-height: 100vh; + border-radius: 0; + } + + +} +.map { + width: 100%; + height: 300px; + background-color: #f0f0f0; +} + diff --git a/frontend/src/pages/PlaceOrder/Location.jsx b/frontend/src/pages/PlaceOrder/Location.jsx new file mode 100644 index 00000000..e080225d --- /dev/null +++ b/frontend/src/pages/PlaceOrder/Location.jsx @@ -0,0 +1,380 @@ +import React, { useState, useEffect, useRef } from 'react'; +import './Location.css'; + +const Location = ({ onLocationSelect, onClose }) => { + const [map, setMap] = useState(null); + const [marker, setMarker] = useState(null); + const [selectedLocation, setSelectedLocation] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const mapRef = useRef(null); + + useEffect(() => { + // Wait for component to mount and DOM to be ready + const initMap = async () => { + // Wait for the ref to be available + let attempts = 0; + const maxAttempts = 20; + + while (!mapRef.current && attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 100)); + attempts++; + } + + if (mapRef.current) { + initializeMap(); + } else { + setError('Map container not found after waiting'); + setIsLoading(false); + } + }; + + initMap(); + + return () => { + if (map) { + try { + map.remove(); + } catch (err) { + console.error('Error removing map:', err); + } + } + }; + }, []); + + const initializeMap = async () => { + try { + console.log('Initializing map...'); + console.log('mapRef.current:', mapRef.current); + + // Double-check that the container exists + if (!mapRef.current) { + console.error('Map container still not found'); + setError('Map container not found'); + setIsLoading(false); + return; + } + + // Check if Leaflet is available + if (!window.L) { + console.log('Loading Leaflet...'); + await loadLeaflet(); + // Wait a bit more for Leaflet to be fully ready + await new Promise(resolve => setTimeout(resolve, 200)); + } + + if (!window.L) { + throw new Error('Leaflet failed to load'); + } + + console.log('Leaflet loaded, creating map...'); + + // Default to Vijayawada coordinates + let lat = 16.5062; + let lng = 80.6480; + + // Try to get user's location + try { + if (navigator.geolocation) { + console.log('Getting user location...'); + const position = await getCurrentPosition(); + lat = position.coords.latitude; + lng = position.coords.longitude; + console.log('Got user location:', lat, lng); + } + } catch (geoError) { + console.log('Geolocation failed, using default location'); + } + + createMap(lat, lng); + } catch (err) { + console.error('Error initializing map:', err); + setError(`Failed to load map: ${err.message}`); + setIsLoading(false); + } + }; + + const getCurrentPosition = () => { + return new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition( + resolve, + reject, + { timeout: 5000, enableHighAccuracy: false } + ); + }); + }; + + const loadLeaflet = () => { + return new Promise((resolve, reject) => { + // Load CSS + const cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; + document.head.appendChild(cssLink); + + // Load JS + const script = document.createElement('script'); + script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load Leaflet')); + document.head.appendChild(script); + }); + }; + + const createMap = (lat, lng) => { + try { + console.log('Creating map with coordinates:', lat, lng); + console.log('Map container element:', mapRef.current); + + // Ensure container exists and has dimensions + if (!mapRef.current) { + throw new Error('Map container is null'); + } + + // Clear any existing map + if (map) { + console.log('Removing existing map'); + map.remove(); + } + + // Ensure the container has dimensions + const containerRect = mapRef.current.getBoundingClientRect(); + console.log('Container dimensions:', containerRect); + + if (containerRect.width === 0 || containerRect.height === 0) { + console.warn('Container has no dimensions, forcing size'); + mapRef.current.style.width = '100%'; + mapRef.current.style.height = '300px'; + } + + // Create map with explicit options + console.log('Creating Leaflet map...'); + const mapInstance = window.L.map(mapRef.current, { + center: [lat, lng], + zoom: 13, + zoomControl: true, + scrollWheelZoom: true, + doubleClickZoom: true, + dragging: true + }); + + console.log('Map instance created:', mapInstance); + + // Add tile layer + console.log('Adding tile layer...'); + const tileLayer = window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: 'Β© OpenStreetMap contributors', + maxZoom: 19 + }); + + tileLayer.addTo(mapInstance); + + // Add marker + console.log('Adding marker...'); + const markerInstance = window.L.marker([lat, lng], { + draggable: true + }); + + markerInstance.addTo(mapInstance); + + // Event handlers + markerInstance.on('dragend', function(e) { + const position = e.target.getLatLng(); + console.log('Marker dragged to:', position); + reverseGeocode(position.lat, position.lng); + }); + + mapInstance.on('click', function(e) { + const { lat, lng } = e.latlng; + console.log('Map clicked at:', lat, lng); + markerInstance.setLatLng([lat, lng]); + reverseGeocode(lat, lng); + }); + + // Force map to resize/refresh + setTimeout(() => { + mapInstance.invalidateSize(); + }, 100); + + setMap(mapInstance); + setMarker(markerInstance); + + console.log('Map created successfully'); + + // Get address for initial location + reverseGeocode(lat, lng); + setIsLoading(false); + + } catch (err) { + console.error('Error creating map:', err); + setError(`Failed to create map: ${err.message}`); + setIsLoading(false); + } + }; + + // const reverseGeocode = async (lat, lng) => { + // try { + // const response = await fetch( + // `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1` + // ); + + // if (!response.ok) { + // throw new Error('Geocoding failed'); + // } + + // const data = await response.json(); + + // if (data && data.address) { + // const address = data.address; + // const locationData = { + // lat, + // lng, + // street: `${address.house_number || ''} ${address.road || address.neighbourhood || ''}`.trim(), + // city: address.city || address.town || address.village || address.suburb || '', + // state: address.state || address.region || '', + // zipCode: address.postcode || '', + // country: address.country || '', + // fullAddress: data.display_name || `${lat}, ${lng}` + // }; + // setSelectedLocation(locationData); + // } + // } catch (error) { + // console.error('Reverse geocoding failed:', error); + // // Set basic location data even if geocoding fails + // setSelectedLocation({ + // lat, + // lng, + // street: '', + // city: '', + // state: '', + // zipCode: '', + // country: '', + // fullAddress: `Coordinates: ${lat.toFixed(4)}, ${lng.toFixed(4)}` + // }); + // } + // }; + + const reverseGeocode = async (lat, lng) => { + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1` + ); + + if (!response.ok) { + throw new Error('Geocoding failed'); + } + + const data = await response.json(); + const address = data.address || {}; + + const locationData = { + lat, + lng, + street: `${address.house_number || ''} ${address.road || address.neighbourhood || ''}`.trim(), + city: address.city || address.town || address.village || address.suburb || '', + state: address.state || address.region || '', + zipCode: address.postcode || '', // May be empty β€” fallback next + country: address.country || '', + fullAddress: data.display_name || `${lat}, ${lng}` + }; + + // Optional: fallback to zip from fullAddress if missing + if (!locationData.zipCode && locationData.fullAddress) { + const zipMatch = locationData.fullAddress.match(/\b\d{5,6}\b/); + if (zipMatch) { + locationData.zipCode = zipMatch[0]; + } + } + + setSelectedLocation(locationData); + } catch (error) { + console.error('Reverse geocoding failed:', error); + setSelectedLocation({ + lat, + lng, + street: '', + city: '', + state: '', + zipCode: '', + country: '', + fullAddress: `Coordinates: ${lat.toFixed(4)}, ${lng.toFixed(4)}` + }); + } +}; + + + const handleConfirm = () => { + if (selectedLocation && onLocationSelect) { + onLocationSelect(selectedLocation); + } + if (onClose) { + onClose(); + } + }; + + if (error) { + return ( +
+
+
+

Select Location

+ +
+
+

{error}

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

Select Your Location

+ +
+ + {/* Always render the map container so it's available in the DOM */} +
+
+
+
+ + {isLoading ? ( +
+
+

Loading map...

+ This may take a few seconds +
+ ) : ( + <> + {selectedLocation && ( +
+

Selected Address:

+

{selectedLocation.fullAddress}

+
+ )} + +
+ + +
+ + )} +
+
+
+); +} +export default Location; \ No newline at end of file diff --git a/frontend/src/pages/PlaceOrder/PlaceOrder.css b/frontend/src/pages/PlaceOrder/PlaceOrder.css index a448d1b8..84914635 100644 --- a/frontend/src/pages/PlaceOrder/PlaceOrder.css +++ b/frontend/src/pages/PlaceOrder/PlaceOrder.css @@ -4,6 +4,12 @@ justify-content: space-between; gap: 50px; margin-top: 100px; + margin: 100px; +} +@media (max-width: 768px) { + .place-order { + margin: 20px; + } } .place-order-left{ @@ -38,4 +44,39 @@ .place-order .cart-total button{ margin-top: 30px; +} + +/* for location purpose */ + +/* Add this to your existing PlaceOrder.css file */ + + +.select-location-btn { + width: 100%; + padding: 12px 16px; + margin-bottom: 15px; + background: linear-gradient(135deg, tomato, #d26634); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2); +} + +.select-location-btn:hover { + background: linear-gradient(135deg, #f19162, #cf832b); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3); +} + +.select-location-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2); } \ No newline at end of file diff --git a/frontend/src/pages/PlaceOrder/PlaceOrder.jsx b/frontend/src/pages/PlaceOrder/PlaceOrder.jsx index d3e1c76b..f96861c0 100644 --- a/frontend/src/pages/PlaceOrder/PlaceOrder.jsx +++ b/frontend/src/pages/PlaceOrder/PlaceOrder.jsx @@ -1,56 +1,274 @@ -import React, { useContext } from "react"; +// import React, { useContext } from "react"; +// import "./PlaceOrder.css"; +// import { StoreContext } from "../../components/context/StoreContext"; +// const PlaceOrder = () => { +// const {getTotalCartAmount} = useContext(StoreContext) +// return ( +//
+//
+//

Delivery Information

+//
+// +// +//
+// +// +//
+// +// +//
+//
+// +// +//
+// +//
+//
+//
+//

Cart Totals

+//
+//

Subtotal

+//

${getTotalCartAmount()}

+//
+//
+//
+//

Delivery Fee

+//

${getTotalCartAmount() === 0? 0 : 2}

+//
+//
+//
+// +//

Total

+//
+// +//

${getTotalCartAmount() === 0 ? 0 : getTotalCartAmount() + 2}

+//
+//
+// +//
+//
+//
+// ); +// }; + +// export default PlaceOrder; + + + +import React, { useContext, useState } from "react"; import "./PlaceOrder.css"; import { StoreContext } from "../../components/context/StoreContext"; +import Location from "./Location"; // Import the Location component + const PlaceOrder = () => { - const {getTotalCartAmount} = useContext(StoreContext) - return ( -
-
-

Delivery Information

-
- - -
- - -
- - -
-
- - -
- -
-
-
-

Cart Totals

-
-

Subtotal

-

${getTotalCartAmount()}

-
-
-
-

Delivery Fee

-

${getTotalCartAmount() === 0? 0 : 2}

-
-
-
- -

Total

-
- -

${getTotalCartAmount() === 0 ? 0 : getTotalCartAmount() + 2}

-
-
- -
-
-
- ); + const { getTotalCartAmount } = useContext(StoreContext); + const [showLocationPopup, setShowLocationPopup] = useState(false); + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + street: '', + city: '', + state: '', + zipCode: '', + country: '', + phone: '' + }); + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleLocationSelect = (locationData) => { + setFormData(prev => ({ + ...prev, + street: locationData.street, + city: locationData.city, + state: locationData.state, + zipCode: locationData.zipCode, + country: locationData.country + })); + setShowLocationPopup(false); + }; + + const handleCloseLocationPopup = () => { + setShowLocationPopup(false); + }; + const handleSubmit = async (e) => { + e.preventDefault(); + + const token = localStorage.getItem("authToken"); + if (!token) { + alert("Please log in to place your order."); + return; + } + + const orderData = { + ...formData, + cartTotal: getTotalCartAmount(), + deliveryFee: getTotalCartAmount() === 0 ? 0 : 2, + }; + + try { + const response = await fetch("/api/orders/place", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(orderData), + }); + + if (!response.ok) throw new Error("Order failed"); + + const data = await response.json(); + alert("Order placed successfully!"); + console.log("Server response:", data); + + // Optional: navigate("/thankyou"); + } catch (error) { + console.error("Order error:", error); + alert("Failed to place order."); + } + }; + + return ( + <> +
+
+

Delivery Information

+ + + +
+ + +
+ + +
+ + +
+
+ + +
+ + + + {/* Location Button */} + +
+ + + + + + +
+
+

Cart Totals

+
+

Subtotal

+

${getTotalCartAmount()}

+
+
+
+

Delivery Fee

+

${getTotalCartAmount() === 0 ? 0 : 2}

+
+
+
+ +

Total

+
+ +

${getTotalCartAmount() === 0 ? 0 : getTotalCartAmount() + 2}

+
+
+ +
+
+
+ + {/* Location Popup */} + {showLocationPopup && ( + + )} + + ); }; -export default PlaceOrder; +export default PlaceOrder; \ No newline at end of file diff --git a/frontend/src/pages/Restaurants/RestaurantDetail.jsx b/frontend/src/pages/Restaurants/RestaurantDetail.jsx new file mode 100644 index 00000000..f0c37c30 --- /dev/null +++ b/frontend/src/pages/Restaurants/RestaurantDetail.jsx @@ -0,0 +1,184 @@ +import React, { useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Star } from "lucide-react"; + +const RestaurantDetail = () => { + const { id } = useParams(); + const navigate = useNavigate(); + + const restaurants = [ + { + id: 1, + name: "The Golden Plate", + image: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&h=400&fit=crop", + rating: 4.5, + discount: "20% OFF", + cuisine: "Italian", + deliveryTime: "25-35 min", + location: "Downtown", + description: "Authentic Italian cuisine with fresh ingredients and traditional recipes.", + }, + { + id: 2, + name: "Spice Garden", + image: "https://images.unsplash.com/photo-1552566626-52f8b828add9?w=800&h=400&fit=crop", + rating: 4.2, + discount: "15% OFF", + cuisine: "Indian", + deliveryTime: "30-45 min", + location: "Midtown", + description: "Exotic Indian flavors with aromatic spices and rich curries.", + }, + { + id: 3, + name: "Ocean's Catch", + image: "https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=800&h=400&fit=crop", + rating: 4.7, + discount: "25% OFF", + cuisine: "Seafood", + deliveryTime: "20-30 min", + location: "Harbor District", + description: "Fresh seafood and coastal cuisine with stunning ocean views.", + }, + { + id: 4, + name: "Burger Haven", + image: "https://images.unsplash.com/photo-1571091718767-18b5b1457add?w=800&h=400&fit=crop", + rating: 4.3, + discount: "10% OFF", + cuisine: "American", + deliveryTime: "15-25 min", + location: "Westside", + description: "Gourmet burgers and comfort food with premium ingredients.", + }, + ]; + + const restaurant = restaurants.find((rest) => rest.id === parseInt(id, 10)); + + if (!restaurant) { + return ( +
+

Restaurant not found

+ +
+ ); + } + + const sampleReviews = [ + { id: 1, username: "JaneDoe", rating: 5, comment: "Amazing food and cozy atmosphere!" }, + { id: 2, username: "FoodLover99", rating: 4, comment: "Great taste but a bit slow service." }, + { id: 3, username: "ChefMaster", rating: 3, comment: "Decent flavors, but could be better." }, + ]; + + const [showReviews, setShowReviews] = useState(false); + + const renderStars = (rating) => { + const stars = []; + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 !== 0; + + for (let i = 0; i < fullStars; i++) { + stars.push(); + } + + if (hasHalfStar) { + stars.push(); + } + + const emptyStars = 5 - Math.ceil(rating); + for (let i = 0; i < emptyStars; i++) { + stars.push(); + } + + return stars; + }; + + return ( +
+ + + {restaurant.name} +

{restaurant.name}

+ +
+ {renderStars(restaurant.rating)} + {restaurant.rating.toFixed(1)} + + {restaurant.discount} + +
+ +

+ {restaurant.cuisine} Β· {restaurant.deliveryTime} Β· {restaurant.location} +

+ +

{restaurant.description}

+ + + + {showReviews && ( +
+ {sampleReviews.map((review) => ( +
+ {review.username} +
+ {renderStars(review.rating)} + {review.rating} +
+

{review.comment}

+
+ ))} +
+ )} +
+ ); +}; + +export default RestaurantDetail; diff --git a/frontend/src/pages/Restaurants/Restaurants.css b/frontend/src/pages/Restaurants/Restaurants.css new file mode 100644 index 00000000..136037c8 --- /dev/null +++ b/frontend/src/pages/Restaurants/Restaurants.css @@ -0,0 +1,211 @@ +.restaurants-page { + padding: 20px; + max-width: 1200px; + margin: 0 auto; + min-height: 100vh; + background-color: var(--bg-color, #fff); + color: var(--text-color, #111); +} + +.restaurants-header { + text-align: center; + margin-bottom: 40px; + padding: 20px 0; +} + +.restaurants-header h1 { + font-size: 2.5rem; + font-weight: 700; + color: var(--title-color, #111); + margin-bottom: 10px; +} + +.restaurants-header p { + font-size: 1.1rem; + color: #676767; + max-width: 600px; + margin: 0 auto; +} + +.restaurants-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 30px; + padding: 20px 0; +} + +.restaurant-card { + width: 100%; + max-width: 400px; + margin: auto; + border-radius: 15px; + box-shadow: 0px 0px 10px #00000015; + transition: 0.4s ease-in-out; + animation: FadeIn 0.8s ease; + background-color: var(--bg-color, #fff); + color: var(--text-color, #111); + overflow: hidden; +} + +.restaurant-card:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + transform: translateY(-5px); +} + +.restaurant-image-container { + position: relative; + overflow: hidden; +} + +.restaurant-image { + width: 100%; + height: 200px; + object-fit: cover; + transition: transform 0.3s ease-in-out; +} + +.restaurant-image:hover { + transform: scale(1.05); +} + +.discount-badge { + position: absolute; + top: 15px; + left: 15px; + background-color: #ff6347; + color: white; + padding: 6px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + z-index: 2; +} + +.restaurant-info { + padding: 20px; +} + +.restaurant-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; +} + +.restaurant-name { + font-size: 20px; + font-weight: 600; + color: var(--title-color, #111); + margin: 0; + flex: 1; + margin-right: 10px; +} + +.rating-container { + display: flex; + align-items: center; + gap: 5px; + flex-shrink: 0; +} + +.rating-text { + font-size: 14px; + font-weight: 600; + color: var(--title-color, #333); /* Use theme variable for color */ +} +.restaurant-details { + display: flex; + gap: 20px; + margin-bottom: 15px; +} + +.detail-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 13px; + color: #676767; +} + +.restaurant-cuisine { + font-size: 14px; + font-weight: 600; + color: #ff6347; + margin: 0 0 10px 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.restaurant-description { + color: #676767; + font-size: 13px; + margin-bottom: 15px; + line-height: 1.4; +} + +.order-now-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + background-color: #ff6347; + border: none; + padding: 10px 20px; + border-radius: 50px; + color: white; + font-weight: 500; + cursor: pointer; + transition: background-color 0.3s ease; + width: 100%; + font-size: 14px; +} + + +.order-now-btn:hover { + background-color: #e5533d; +} + +@keyframes FadeIn { + 0% { + opacity: 0; + transform: translateY(15px); + } + 100% { + opacity: 1; + transform: translateY(0px); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .restaurants-page { + padding: 15px; + } + + .restaurants-header h1 { + font-size: 2rem; + } + + .restaurants-grid { + grid-template-columns: 1fr; + gap: 20px; + } + + .restaurant-card { + max-width: 100%; + } + + .restaurant-header { + flex-direction: column; + gap: 10px; + } + + .restaurant-name { + margin-right: 0; + } + + .restaurant-details { + flex-direction: column; + gap: 8px; + } +} \ No newline at end of file diff --git a/frontend/src/pages/Restaurants/Restaurants.jsx b/frontend/src/pages/Restaurants/Restaurants.jsx new file mode 100644 index 00000000..a940ceea --- /dev/null +++ b/frontend/src/pages/Restaurants/Restaurants.jsx @@ -0,0 +1,129 @@ +import React from "react"; +import "./Restaurants.css"; +import { Star, MapPin, Clock } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +const Restaurants = () => { + const navigate = useNavigate(); + + const restaurants = [ + { + id: 1, + name: "The Golden Plate", + image: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500&h=300&fit=crop", + rating: 4.5, + discount: "20% OFF", + cuisine: "Italian", + deliveryTime: "25-35 min", + location: "Downtown", + description: "Authentic Italian cuisine with fresh ingredients and traditional recipes.", + }, + { + id: 2, + name: "Spice Garden", + image: "https://images.unsplash.com/photo-1552566626-52f8b828add9?w=500&h=300&fit=crop", + rating: 4.2, + discount: "15% OFF", + cuisine: "Indian", + deliveryTime: "30-45 min", + location: "Midtown", + description: "Exotic Indian flavors with aromatic spices and rich curries.", + }, + { + id: 3, + name: "Ocean's Catch", + image: "https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=500&h=300&fit=crop", + rating: 4.7, + discount: "25% OFF", + cuisine: "Seafood", + deliveryTime: "20-30 min", + location: "Harbor District", + description: "Fresh seafood and coastal cuisine with stunning ocean views.", + }, + { + id: 4, + name: "Burger Haven", + image: "https://images.unsplash.com/photo-1571091718767-18b5b1457add?w=500&h=300&fit=crop", + rating: 4.3, + discount: "10% OFF", + cuisine: "American", + deliveryTime: "15-25 min", + location: "Westside", + description: "Gourmet burgers and comfort food with premium ingredients.", + }, + ]; + + const renderStars = (rating) => { + const stars = []; + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 !== 0; + + for (let i = 0; i < fullStars; i++) { + stars.push(); + } + + if (hasHalfStar) { + stars.push(); + } + + const emptyStars = 5 - Math.ceil(rating); + for (let i = 0; i < emptyStars; i++) { + stars.push(); + } + + return stars; + }; + + return ( +
+
+

Discover Amazing Restaurants

+

Explore the best restaurants in your area with exclusive offers

+
+ +
+ {restaurants.map((restaurant) => ( +
navigate(`/restaurant/${restaurant.id}`)} + style={{ cursor: "pointer" }} + > +
+ {restaurant.name} +
{restaurant.discount}
+
+ +
+
+

{restaurant.name}

+
+ {renderStars(restaurant.rating)} + {restaurant.rating} +
+
+ +
+
+ + {restaurant.location} +
+
+ + {restaurant.deliveryTime} +
+
+ +

{restaurant.cuisine}

+

{restaurant.description}

+ + +
+
+ ))} +
+
+ ); +}; + +export default Restaurants; diff --git a/frontend/src/pages/wishlist/SharedWishlist.jsx b/frontend/src/pages/wishlist/SharedWishlist.jsx new file mode 100644 index 00000000..4f889726 --- /dev/null +++ b/frontend/src/pages/wishlist/SharedWishlist.jsx @@ -0,0 +1,118 @@ +import { useEffect, useState, useContext } from 'react'; +import { useParams } from 'react-router-dom'; +import { StoreContext } from '../../components/context/StoreContext'; +import FoodItem from '../../components/FoodItem/FoodItem'; +import "./wishlist.css"; + +const SharedWishlist = () => { + const { food_list } = useContext(StoreContext); + const { userId } = useParams(); + const [wishlistedItems, setWishlistedItems] = useState([]); + const [userName, setUserName] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [viewCount, setViewCount] = useState(0); + + useEffect(() => { + const loadSharedWishlist = () => { + try { + // Get the shared wishlist data from localStorage + const sharedWishlistData = localStorage.getItem(`shared_wishlist_${userId}`); + + if (!sharedWishlistData) { + setError('Wishlist not found or has expired'); + setLoading(false); + return; + } + + const { wishlistIds, userName: ownerName, createdAt, viewCount = 0 } = JSON.parse(sharedWishlistData); + + // Check if the link has expired (24 hours) + const now = new Date().getTime(); + const linkAge = now - createdAt; + const twentyFourHours = 24 * 60 * 60 * 1000; + + if (linkAge > twentyFourHours) { + setError('This shared wishlist link has expired'); + setLoading(false); + return; + } + + // Update view count + const updatedData = { + wishlistIds, + userName: ownerName, + createdAt, + viewCount: viewCount + 1 + }; + localStorage.setItem(`shared_wishlist_${userId}`, JSON.stringify(updatedData)); + + setUserName(ownerName); + setViewCount(viewCount + 1); + + // Filter food items based on the shared wishlist IDs + const filtered = food_list.filter(food => wishlistIds.includes(food._id)); + setWishlistedItems(filtered); + setLoading(false); + } catch (err) { + setError('Failed to load wishlist'); + setLoading(false); + } + }; + + loadSharedWishlist(); + }, [userId, food_list]); + + if (loading) { + return ( +
+
Loading shared wishlist...
+
+ ); + } + + if (error) { + return ( +
+
+

❌ {error}

+

The link may be invalid or the wishlist may have been removed.

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

🎁 You're viewing {userName}'s wishlist

+

+ This is a shared wishlist. You can view the items but cannot modify them. +

+
+ πŸ‘οΈ Viewed {viewCount} time{viewCount !== 1 ? 's' : ''} +
+
+ + {wishlistedItems.length === 0 ? ( +

No items in this wishlist!

+ ) : ( +
+ {wishlistedItems.map(item => ( + + ))} +
+ )} +
+ ); +}; + +export default SharedWishlist; \ No newline at end of file diff --git a/frontend/src/pages/wishlist/wishlist.css b/frontend/src/pages/wishlist/wishlist.css new file mode 100644 index 00000000..4b091575 --- /dev/null +++ b/frontend/src/pages/wishlist/wishlist.css @@ -0,0 +1,218 @@ +.wishlist-page{ + margin:0px 100px; +} + +.wishlist-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +.share-wishlist-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 25px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3); +} + +.share-wishlist-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); +} + +.share-wishlist-btn:disabled { + background: #4CAF50; + cursor: default; + transform: none; + box-shadow: 0 2px 10px rgba(76, 175, 80, 0.3); +} + +.share-wishlist-btn svg { + width: 16px; + height: 16px; +} + +.share-buttons { + display: flex; + gap: 10px; + align-items: center; +} + +.qr-code-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + border: none; + border-radius: 25px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + box-shadow: 0 2px 10px rgba(240, 147, 251, 0.3); +} + +.qr-code-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4); +} + +.qr-code-btn svg { + width: 16px; + height: 16px; +} + +.wishlist-controls { + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-end; +} + +.privacy-toggle-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + transition: all 0.3s ease; +} + +.privacy-toggle-btn.private { + background: #f1f5f9; + color: #64748b; + border: 1px solid #e2e8f0; +} + +.privacy-toggle-btn.public { + background: #dcfce7; + color: #166534; + border: 1px solid #bbf7d0; +} + +.privacy-toggle-btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.privacy-toggle-btn svg { + width: 14px; + height: 14px; +} + +/* Shared wishlist styles */ +.shared-wishlist { + position: relative; +} + +.shared-wishlist-header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + border-radius: 15px; + color: white; + box-shadow: 0 4px 15px rgba(240, 147, 251, 0.3); +} + +.shared-wishlist-header h2 { + margin: 0 0 10px 0; + font-size: 24px; + font-weight: 600; +} + +.shared-wishlist-subtitle { + margin: 0; + opacity: 0.9; + font-size: 14px; +} + +.view-count { + margin-top: 10px; + font-size: 12px; + opacity: 0.8; + background: rgba(255, 255, 255, 0.2); + padding: 4px 12px; + border-radius: 15px; + display: inline-block; +} + +.loading { + text-align: center; + padding: 40px; + font-size: 18px; + color: #666; +} + +.error-message { + text-align: center; + padding: 40px; + background: #fff3f3; + border-radius: 10px; + border: 1px solid #ffcdd2; +} + +.error-message h2 { + color: #d32f2f; + margin-bottom: 10px; +} + +.error-message p { + color: #666; + margin: 0; +} + +@media (max-width: 768px) { + .wishlist-page { + margin: 10px; + } + + .wishlist-header { + flex-direction: column; + align-items: stretch; + text-align: center; + } + + .share-buttons { + flex-direction: column; + align-items: center; + gap: 8px; + } + + .wishlist-controls { + align-items: center; + } + + .share-wishlist-btn, + .qr-code-btn { + align-self: center; + max-width: 200px; + } + + .privacy-toggle-btn { + align-self: center; + max-width: 150px; + } + + .shared-wishlist-header h2 { + font-size: 20px; + } +} diff --git a/frontend/src/pages/wishlist/wishlist.jsx b/frontend/src/pages/wishlist/wishlist.jsx new file mode 100644 index 00000000..d0543f0a --- /dev/null +++ b/frontend/src/pages/wishlist/wishlist.jsx @@ -0,0 +1,211 @@ +import { useEffect, useState, useContext } from 'react'; +import { StoreContext } from '../../components/context/StoreContext'; +import FoodItem from '../../components/FoodItem/FoodItem'; +import { toast } from 'react-hot-toast'; +import { Share2, Copy, Check, QrCode, Lock, Unlock } from 'lucide-react'; +import QRCode from '../../components/QRCode/QRCode'; +import "./wishlist.css"; + +const Wishlist = () => { + const { food_list } = useContext(StoreContext); + const [wishlistedItems, setWishlistedItems] = useState([]); + const [copied, setCopied] = useState(false); + const [user, setUser] = useState(null); + const [showQRCode, setShowQRCode] = useState(false); + const [shareUrl, setShareUrl] = useState(''); + const [isPublic, setIsPublic] = useState(false); + + useEffect(() => { + // Get user data + const storedUser = JSON.parse(localStorage.getItem("user")); + setUser(storedUser); + + // Get privacy setting + const privacySetting = localStorage.getItem(`wishlist_privacy_${storedUser?.email || 'anonymous'}`); + setIsPublic(privacySetting === 'public'); + + const updateWishlist = () => { + const wishlistIds = JSON.parse(localStorage.getItem('wishlist')) || []; + const filtered = food_list.filter(food => wishlistIds.includes(food._id)); + setWishlistedItems(filtered); + }; + updateWishlist(); // Load initially + + // Listen for custom wishlist update event + window.addEventListener("wishlistUpdated", updateWishlist); + + // Cleanup when component unmounts + return () => { + window.removeEventListener("wishlistUpdated", updateWishlist); + }; + }, [food_list]); + + const generateShareLink = () => { + if (!user) { + // For demo purposes, use a default user + const demoUser = { name: 'Demo User', email: 'demo@example.com' }; + setUser(demoUser); + } + + const wishlistIds = JSON.parse(localStorage.getItem('wishlist')) || []; + if (wishlistIds.length === 0) { + toast.error('Your wishlist is empty!'); + return; + } + + // Create a unique user ID for sharing (using user's email or name) + const userId = user.email || user.name || 'user'; + const shareId = btoa(userId).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10); + + // Store the shared wishlist data + const sharedWishlistData = { + wishlistIds, + userName: user.name || 'Anonymous', + createdAt: new Date().getTime(), + viewCount: 0 + }; + + localStorage.setItem(`shared_wishlist_${shareId}`, JSON.stringify(sharedWishlistData)); + + // Generate the share URL + const shareUrl = `${window.location.origin}/wishlist/${shareId}`; + return shareUrl; + }; + + const handleShareWishlist = async () => { + const url = generateShareLink(); + if (!url) return; + + setShareUrl(url); + + try { + await navigator.clipboard.writeText(url); + setCopied(true); + toast.success('Wishlist link copied to clipboard!'); + + // Reset copied state after 2 seconds + setTimeout(() => setCopied(false), 2000); + } catch (err) { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = url; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + setCopied(true); + toast.success('Wishlist link copied to clipboard!'); + setTimeout(() => setCopied(false), 2000); + } + }; + + const handleShowQRCode = () => { + const url = generateShareLink(); + if (!url) return; + + setShareUrl(url); + setShowQRCode(true); + }; + + const handlePrivacyToggle = () => { + if (!user) { + // For demo purposes, use a default user + const demoUser = { name: 'Demo User', email: 'demo@example.com' }; + setUser(demoUser); + } + + const newPrivacy = !isPublic; + setIsPublic(newPrivacy); + + const privacyKey = `wishlist_privacy_${user.email || 'anonymous'}`; + localStorage.setItem(privacyKey, newPrivacy ? 'public' : 'private'); + + toast.success(`Wishlist is now ${newPrivacy ? 'public' : 'private'}`); + }; + + return ( +
+
+

πŸ’– Your Wishlist

+ {/* Debug info */} +
+ Debug: User signed in: {user ? 'Yes' : 'No'} | Items in wishlist: {wishlistedItems.length} +
+ {wishlistedItems.length > 0 && ( +
+
+ + +
+ +
+ )} +
+ {wishlistedItems.length === 0 ? ( +

No items wishlisted yet!

+ ) : ( +
+ {wishlistedItems.map(item => ( + + + ))} +
+ )} + + {showQRCode && ( + setShowQRCode(false)} + /> + )} +
+ ); +}; + +export default Wishlist; diff --git a/frontend/utility/ScrollToTop.jsx b/frontend/utility/ScrollToTop.jsx new file mode 100644 index 00000000..7ace5e06 --- /dev/null +++ b/frontend/utility/ScrollToTop.jsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; + +const ScrollToTop = () => { + const { pathname } = useLocation(); + + useEffect(() => { + window.scrollTo({ top: 0, behavior: "instant" }); // or "smooth" + }, [pathname]); + + return null; +}; + +export default ScrollToTop; diff --git a/images/foodie-home-light.png b/images/foodie-home-light.png new file mode 100644 index 00000000..652f6c2b Binary files /dev/null and b/images/foodie-home-light.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..93c3c9a8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Foodie", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..408821b1 --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/" + } + ] +}