diff --git a/.agent/knowledge.md b/.agent/knowledge.md new file mode 100644 index 000000000..d67066822 --- /dev/null +++ b/.agent/knowledge.md @@ -0,0 +1,256 @@ +# QuickCart - E-Commerce Project Knowledge Base + +## Overview + +QuickCart is a full-stack e-commerce application built with Next.js, featuring a customer shopping experience, a seller dashboard for managing products and orders, and an admin dashboard for site management. + +## Technology Stack + +| Technology | Purpose | +|------------|---------| +| Next.js 15 (App Router) | React framework with server-side rendering | +| React 19 | Frontend UI library | +| Tailwind CSS | Styling | +| MongoDB (Local) | Database (MongoDB Community Server) | +| Mongoose | MongoDB ODM | +| JWT (jsonwebtoken) | Authentication tokens | +| jose | JWT verification for Edge Runtime (Middleware) | +| bcryptjs | Password hashing | +| react-hot-toast | Toast notifications | + +## Project Structure + +``` +QuickCart/ +├── app/ +│ ├── api/ +│ │ ├── admin/ +│ │ │ ├── messages/route.js # Message management +│ │ │ └── users/route.js # User/Seller management +│ │ ├── auth/ +│ │ │ ├── login/route.js # Login endpoint +│ │ │ ├── register/route.js # Registration endpoint +│ │ │ ├── logout/route.js # Logout endpoint +│ │ │ └── me/route.js # Get current user +│ │ ├── user/ +│ │ │ └── profile/route.js # Profile management +│ │ ├── product/ +│ │ │ ├── route.js # GET all, POST new +│ │ │ └── [id]/route.js # GET, PUT, DELETE single +│ │ ├── order/ +│ │ │ ├── route.js # GET user orders, POST new +│ │ │ └── seller/route.js # Seller order management +│ │ ├── feature/route.js # Feature/Hero content management +│ │ ├── contact/route.js # Customer inquiry submission +│ │ ├── address/route.js # Address CRUD +│ │ ├── cart/route.js # Cart sync +│ │ └── seed/route.js # Database seeding (Dummy data) +│ ├── admin/ # Admin Dashboard +│ │ ├── features/ # Manage homepage features +│ │ ├── messages/ # Customer inquiries +│ │ ├── users/ # User management +│ │ └── page.jsx # Admin stats/entry +│ ├── seller/ # Seller Dashboard +│ │ ├── product-list/page.jsx # Product management +│ │ └── orders/page.jsx # Order management +│ ├── product/[id]/page.jsx # Product details +│ ├── all-products/page.jsx # Shop all page +│ ├── cart/page.jsx # Shopping cart +│ ├── my-orders/page.jsx # User order history +│ ├── my-profile/page.jsx # User profile +│ ├── contact/page.jsx # Contact us page +│ ├── order-placed/page.jsx # Success page +│ ├── layout.js # Root layout +│ └── page.jsx # Homepage +├── components/ +│ ├── Navbar.jsx # Navigation with auth +│ ├── ProductCard.jsx +│ ├── OrderSummary.jsx +│ └── seller/ +│ ├── Sidebar.jsx +│ └── Footer.jsx +├── context/ +│ └── AppContext.jsx # Global state (user, cart, products) +├── models/ +│ ├── user.js # User schema (isAdmin flag) +│ ├── Product.js # Product schema +│ ├── Order.js # Order schema +│ ├── Address.js # Address schema +│ ├── Message.js # Contact message schema +│ └── feature.js # Homepage feature/banner schema +├── lib/ +│ ├── auth.js # Auth utilities (JWT, password) +│ ├── authSeller.js # Seller authorization +│ └── authAdmin.js # Admin authorization +├── config/ +│ └── db.js # MongoDB connection +├── middleware.js # Route protection (Edge compatible) +└── assets/ # Static assets +``` + +## Authentication System + +### Roles & Permissions +- **Customer**: Default role. Can browse, cart, and order. +- **Seller**: Can manage their own products and orders. Accesses `/seller`. +- **Admin**: Can manage all users, update site features, and respond to messages. Accesses `/admin`. + +### Local JWT Authentication +- Users register with email/password (passwords hashed with bcrypt). +- JWT tokens stored in httpOnly cookies (7-day expiry). +- Middleware protects routes based on authentication, seller role, and admin role. + +### Auth Endpoints +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/auth/register` | POST | Register new user (optional seller flag) | +| `/api/auth/login` | POST | Login with email/password | +| `/api/auth/logout` | POST | Clear auth cookie | +| `/api/auth/me` | GET | Get current user data | +| `/api/user/profile` | PUT | Update user profile (name, phone, etc) | + +## API Routes + +### Admin API +| Route | Method | Description | +|-------|--------|-------------| +| `/api/admin/users` | GET | List all users (excluding passwords) | +| `/api/admin/users` | PUT | Toggle user's seller status | +| `/api/admin/users` | DELETE | Remove a user | +| `/api/admin/messages` | GET | List all contact messages | +| `/api/admin/messages` | PUT | Reply to or update message status | + +### Features & Site Content +| Route | Method | Auth | Description | +|-------|--------|------|-------------| +| `/api/feature` | GET | No | Get homepage hero/banners | +| `/api/feature` | POST | Admin | Add new feature content | +| `/api/feature` | DELETE | Admin | Delete feature content | +| `/api/contact` | GET/POST | Yes | Get/Submit contact messages | + +### Products & Orders +| Route | Method | Auth | Description | +|-------|--------|------|-------------| +| `/api/product` | GET | No | Get all products | +| `/api/product` | POST | Seller | Add new product | +| `/api/order` | GET | Yes | Get user's orders | +| `/api/order/seller` | GET | Seller | Get seller's orders | +| `/api/seed` | POST | No | Seed DB with dummy data | + +## Database Models + +### User +```javascript +{ + name: String, + email: String (unique), + password: String (hashed), + isSeller: Boolean (default: false), + isAdmin: Boolean (default: false), + phone: String, + description: String, + imageUrl: String, + cartItems: Object (default: {}), + createdAt: Date +} +``` + +### Feature +```javascript +{ + type: String ('hero', 'featured', 'banner'), + title: String, + description: String, + image: String, + secondaryImage: String, + product: ObjectId (optional link), + buttonText: String, + order: Number +} +``` + +### Message +```javascript +{ + name: String, + email: String, + message: String, + status: String ('Unread', 'Read', 'Replied'), + reply: String, + createdAt: Date +} +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `MONGODB_URI` | Yes | MongoDB connection string | +| `JWT_SECRET` | Yes | Secret key for JWT signing | +| `NEXT_PUBLIC_CURRENCY` | No | Currency symbol (default: $) | + +## Features + +### Admin Features +- Manage users (promote to seller, delete users) +- Manage homepage content (Hero sections, Banners) +- View and reply to customer messages + +### Customer & Seller Features +- Order tracking and history +- Address management +- Profile management (name, phone, bio) +- Product listings with category filtering +- Seller dashboard for inventory and sales + +## Getting Started + +### Prerequisites +1. Node.js 18+ +2. MongoDB Community Server (local installation) + +### Setup Steps +1. Install dependencies: `npm install` +2. Start MongoDB service (Windows: `net start MongoDB` or MongoDB Compass) +3. Create `.env` file with: + ``` + MONGODB_URI=mongodb://localhost:27017 + JWT_SECRET=your-secret-key-here + NEXT_PUBLIC_CURRENCY=$ + ``` +4. Run development server: `npm run dev` +5. Open http://localhost:3000 + +## Known Issues / TODOs + +1. Images stored as URLs/Base64 - should migrate to S3/Cloudinary for production. +2. Payment integration (Stripe/PayPal) is missing. +3. Product search is visual only (not functional). +4. No automated email notifications for orders. + +## Real-time Signaling & Communication + +### Real-time Signaling System +- **Signaling API**: `/api/signal` + - Handles `GET` (poll for new signals) and `POST` (send a signal). + - Used for WebRTC coordination (call invites, status updates). +- **Signal Model**: `models/Signal.js` + - Stores signals with an `expireAfterSeconds` TTL of 60 seconds. + - Fields: `from`, `to`, `type`, `data`, `read`. + +### Video & Voice Calls +- **Technology**: Built using **PeerJS** for peer-to-peer WebRTC connections. +- **Components**: + - `ChatWidget.jsx`: User-side chat and call interface (Messenger style). + - `AdminMessages.jsx`: Admin-side conversation management and call handling. + - `VideoCallInterface.jsx`: Premium, glassmorphic UI for active calls with full media controls. +- **Standard Flow**: + 1. User/Admin initiates call -> `CALL_INVITE` signal sent via `/api/signal`. + 2. Receiver polls signal -> Shows incoming call UI. + 3. Receiver accepts -> Connects via PeerJS and shares media streams. + 4. Call termination -> `CALL_ENDED` signal sent; tracks stopped. + +### Admin Dashboard (Updated) +- **Inbox**: Advanced messaging system with real-time signal polling. +- **Live Support**: Admin can initiate voice or video calls directly with customers. +- **Premium UI**: Uses glassmorphism, smooth animations, and a modern layout. diff --git a/.env b/.env deleted file mode 100644 index 195967ea4..000000000 --- a/.env +++ /dev/null @@ -1,13 +0,0 @@ -# Public Environment Variables -NEXT_PUBLIC_CURRENCY=$ -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY='' - -# Private Environment Variables -CLERK_SECRET_KEY='' -MONGODB_URI='' -INNGEST_SIGNING_KEY='' -INNGEST_EVENT_KEY='' -# Cloudinary -CLOUDINARY_CLOUD_NAME ='' -CLOUDINARY_API_KEY ='' -CLOUDINARY_API_SECRET ='' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1b7a73ab0..9e6d6bcd7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ !.yarn/plugins !.yarn/releases !.yarn/versions - +. # testing /coverage @@ -23,7 +23,7 @@ # misc .DS_Store *.pem - +.env # debug npm-debug.log* yarn-debug.log* @@ -39,3 +39,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# clerk configuration (can include secrets) +/.clerk/ + +certificates \ No newline at end of file diff --git a/app/add-address/page.jsx b/app/add-address/page.jsx index 4fdf10ac3..09165f26a 100644 --- a/app/add-address/page.jsx +++ b/app/add-address/page.jsx @@ -4,9 +4,12 @@ import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; import Image from "next/image"; import { useState } from "react"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; const AddAddress = () => { - + const router = useRouter(); + const [loading, setLoading] = useState(false); const [address, setAddress] = useState({ fullName: '', phoneNumber: '', @@ -19,6 +22,37 @@ const AddAddress = () => { const onSubmitHandler = async (e) => { e.preventDefault(); + // Validate all fields + if (!address.fullName || !address.phoneNumber || !address.pincode || !address.area || !address.city || !address.state) { + toast.error('Please fill in all fields'); + return; + } + + setLoading(true); + + try { + const response = await fetch('/api/address', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(address) + }); + + const data = await response.json(); + + if (data.success) { + toast.success('Address added successfully!'); + router.push('/cart'); + } else { + toast.error(data.message || 'Failed to add address'); + } + } catch (error) { + console.error('Error adding address:', error); + toast.error('Error adding address. Please try again.'); + } finally { + setLoading(false); + } } return ( @@ -34,13 +68,15 @@ const AddAddress = () => { className="px-2 py-2.5 focus:border-orange-500 transition border border-gray-500/30 rounded outline-none w-full text-gray-500" type="text" placeholder="Full name" + required onChange={(e) => setAddress({ ...address, fullName: e.target.value })} value={address.fullName} /> setAddress({ ...address, phoneNumber: e.target.value })} value={address.phoneNumber} /> @@ -48,14 +84,15 @@ const AddAddress = () => { className="px-2 py-2.5 focus:border-orange-500 transition border border-gray-500/30 rounded outline-none w-full text-gray-500" type="text" placeholder="Pin code" + required onChange={(e) => setAddress({ ...address, pincode: e.target.value })} value={address.pincode} /> @@ -64,6 +101,7 @@ const AddAddress = () => { className="px-2 py-2.5 focus:border-orange-500 transition border border-gray-500/30 rounded outline-none w-full text-gray-500" type="text" placeholder="City/District/Town" + required onChange={(e) => setAddress({ ...address, city: e.target.value })} value={address.city} /> @@ -71,13 +109,18 @@ const AddAddress = () => { className="px-2 py-2.5 focus:border-orange-500 transition border border-gray-500/30 rounded outline-none w-full text-gray-500" type="text" placeholder="State" + required onChange={(e) => setAddress({ ...address, state: e.target.value })} value={address.state} /> - { + const { products: allProducts } = useAppContext(); + const [activeTab, setActiveTab] = useState('hero'); + const [features, setFeatures] = useState([]); + const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(true); + + // Form State + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [selectedProduct, setSelectedProduct] = useState(''); + const [buttonText, setButtonText] = useState('Buy Now'); + + // Fetch features + const fetchFeatures = async () => { + setFetching(true); + try { + const response = await fetch('/api/feature'); + const data = await response.json(); + if (data.success) { + setFeatures(data.features || []); + } + } catch (error) { + console.error('Error fetching features:', error); + } finally { + setFetching(false); + } + }; + + useEffect(() => { + fetchFeatures(); + }, []); + + const handleImageUpload = (file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; + + // Image Source State + const [imageSourceValues, setImageSourceValues] = useState({ + main: { type: 'upload', value: null, preview: null }, + secondary: { type: 'upload', value: null, preview: null } + }); + + // Template images (filtered from assets) + const templateImages = Object.keys(assets).filter(key => key.includes('image')); + + const handleImageSourceChange = (imageType, sourceType) => { + setImageSourceValues(prev => ({ + ...prev, + [imageType]: { ...prev[imageType], type: sourceType, value: null, preview: null } + })); + + // If switching to 'product' and a product is already selected, auto-set it + if (sourceType === 'product' && selectedProduct) { + const prod = allProducts.find(p => p._id === selectedProduct); + if (prod) { + updateImageState(imageType, sourceType, prod.image[0], prod.image[0]); + } + } + }; + + const updateImageState = (imageType, sourceType, value, preview) => { + let finalValue = value; + // If value is a Next.js StaticImageData object, extract src string + if (value && typeof value === 'object' && value.src) { + finalValue = value.src; + } + + setImageSourceValues(prev => ({ + ...prev, + [imageType]: { ...prev[imageType], type: sourceType, value: finalValue, preview } + })); + }; + + const handleFileChange = async (imageType, file) => { + if (file) { + const base64 = await handleImageUpload(file); + updateImageState(imageType, 'upload', base64, base64); + } + }; + + // Update product image if product selection changes and source is 'product' + useEffect(() => { + if (selectedProduct) { + const prod = allProducts.find(p => p._id === selectedProduct); + if (prod && prod.image && prod.image.length > 0) { + // If main image is set to product source, update it + if (imageSourceValues.main.type === 'product') { + updateImageState('main', 'product', prod.image[0], prod.image[0]); + } + // If secondary image is set to product source, update it + if (imageSourceValues.secondary.type === 'product') { + updateImageState('secondary', 'product', prod.image[0], prod.image[0]); + } + } + } + }, [selectedProduct, allProducts]); + + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + + try { + if (!imageSourceValues.main.value) { + toast.error('Main image is required'); + setLoading(false); + return; + } + + const response = await fetch('/api/feature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: activeTab, + title, + description, + image: imageSourceValues.main.value, + secondaryImage: imageSourceValues.secondary.value || '', + product: selectedProduct || null, + buttonText + }) + }); + + const data = await response.json(); + + if (data.success) { + toast.success('Feature added successfully'); + // Reset form + setTitle(''); + setDescription(''); + setImageSourceValues({ + main: { type: 'upload', value: null, preview: null }, + secondary: { type: 'upload', value: null, preview: null } + }); + setSelectedProduct(''); + setButtonText('Buy Now'); + fetchFeatures(); + } else { + toast.error(data.message || 'Failed to add feature'); + } + } catch (error) { + toast.error('Error: ' + error.message); + } finally { + setLoading(false); + } + }; + + const renderImageSelector = (label, imageType) => { + const state = imageSourceValues[imageType]; + + return ( +
+ + + {/* Source Tabs */} +
+ {['upload', 'product', 'template'].map(type => ( + + ))} +
+ + {/* Content based on source */} + {state.type === 'upload' && ( + + )} + + {state.type === 'product' && ( +
+ {selectedProduct ? ( + state.preview ? ( + Product + ) : ( +

Selected product has no image

+ ) + ) : ( +

Select a product below first

+ )} +
+ )} + + {state.type === 'template' && ( +
+ {templateImages.map(key => ( +
updateImageState(imageType, 'template', assets[key], assets[key])} + className={`cursor-pointer border rounded overflow-hidden h-16 relative hover:opacity-80 ${state.value === assets[key] ? 'ring-2 ring-orange-500' : ''}`} + title={key} + > + {key} +
+ ))} +
+ )} +
+ ); + }; + + const handleDelete = async (id) => { + if (!confirm('Are you sure you want to delete this content?')) return; + + try { + const response = await fetch(`/api/feature?id=${id}`, { + method: 'DELETE' + }); + const data = await response.json(); + if (data.success) { + toast.success('Deleted successfully'); + fetchFeatures(); + } else { + toast.error(data.message); + } + } catch (error) { + toast.error('Delete failed'); + } + }; + + const filteredFeatures = features.filter(f => f.type === activeTab); + + return ( +
+

Manage Site Content

+ + {/* Tabs */} +
+ {['hero', 'featured', 'banner'].map((tab) => ( + + ))} +
+ +
+ {/* Add Form */} +
+

Add New {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Item

+
+ + {renderImageSelector('Main Image', 'main')} + + {activeTab === 'banner' && renderImageSelector('Secondary Image (Optional)', 'secondary')} + +
+ + setTitle(e.target.value)} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-orange-500 focus:outline-none" + placeholder="Enter title" + required + /> +
+ +
+ + + + +
+
+
+