diff --git a/frontend/src/pages/admin/AddProduct.jsx b/frontend/src/pages/admin/AddProduct.jsx
new file mode 100644
index 0000000..38c6a1e
--- /dev/null
+++ b/frontend/src/pages/admin/AddProduct.jsx
@@ -0,0 +1,176 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import toast from 'react-hot-toast'
+import { productsAPI } from '../../services/api'
+
+export default function AddProduct() {
+ const navigate = useNavigate()
+
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ price: '',
+ discount_price: '',
+ stock: '',
+ sku: '',
+ brand: '',
+ category: '',
+ is_featured: false,
+ is_active: true,
+ })
+
+ const [loading, setLoading] = useState(false)
+
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target
+
+ setFormData((prev) => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value,
+ }))
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ try {
+ setLoading(true)
+
+ await productsAPI.create({
+ ...formData,
+ price: Number(formData.price),
+ discount_price: formData.discount_price
+ ? Number(formData.discount_price)
+ : null,
+ stock: Number(formData.stock),
+ category: Number(formData.category),
+ })
+
+ toast.success('Product created successfully')
+
+ navigate('/admin/products')
+ } catch (error) {
+ console.error(error)
+ toast.error('Failed to create product')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Add Product
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/pages/admin/AdminProducts.jsx b/frontend/src/pages/admin/AdminProducts.jsx
new file mode 100644
index 0000000..f61ac34
--- /dev/null
+++ b/frontend/src/pages/admin/AdminProducts.jsx
@@ -0,0 +1,156 @@
+import { useEffect, useState } from 'react'
+import { Link } from 'react-router-dom'
+import toast from 'react-hot-toast'
+import { productsAPI } from '../../services/api'
+
+export default function AdminProducts() {
+ const [products, setProducts] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [search, setSearch] = useState('')
+
+ const loadProducts = async () => {
+ try {
+ const { data } = await productsAPI.list({
+ search,
+ ordering: '-created_at'
+ })
+
+ setProducts(data.results || [])
+ } catch {
+ toast.error('Failed to load products')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ loadProducts()
+ }, [search])
+
+ const handleDelete = async (slug) => {
+ if (!window.confirm('Delete this product?')) return
+
+ try {
+ await productsAPI.delete(slug)
+
+ toast.success('Product deleted')
+
+ loadProducts()
+ } catch {
+ toast.error('Failed to delete product')
+ }
+ }
+
+ if (loading) {
+ return (
+
+ Loading...
+
+ )
+ }
+
+ return (
+
+
+
+
+ Product Management
+
+
+
+ Add Product
+
+
+
+
setSearch(e.target.value)}
+ className="input-field mb-6"
+ />
+
+
+
+
+
+
+ | Image |
+ Name |
+ Price |
+ Stock |
+ Status |
+ Actions |
+
+
+
+
+ {products.map((product) => (
+
+
+
+ {product.primary_image ? (
+
+ ) : (
+ 'No Image'
+ )}
+ |
+
+
+ {product.name}
+ |
+
+
+ ₹{product.effective_price}
+ |
+
+
+ {product.stock}
+ |
+
+
+ {product.is_active ? 'Active' : 'Inactive'}
+ |
+
+
+
+
+
+ Edit
+
+
+
+
+
+ |
+
+
+ ))}
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index c55373d..4c049c5 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -73,6 +73,9 @@ export const productsAPI = {
bestSellers: () => api.get('/products/best_sellers/'),
related: (slug) => api.get(`/products/${slug}/related/`),
categories: (tree = false) => api.get('/products/categories/', { params: tree ? { tree: true } : {} }),
+ create: (data) => api.post('/products/', data),
+ update: (slug, data) => api.patch(`/products/${slug}/`, data),
+ delete: (slug) => api.delete(`/products/${slug}/`),
}
// Cart
From 2fbf63bfe7e804c6fa69fbe0ba1d8edf622e2382 Mon Sep 17 00:00:00 2001
From: vishnu R <157986076+vishnu-R2005@users.noreply.github.com>
Date: Sat, 13 Jun 2026 21:25:36 +0530
Subject: [PATCH 4/9] feat(admin): implement complete product management CRUD
---
frontend/src/App.jsx | 3 +
frontend/src/pages/admin/AddProduct.jsx | 52 ++++--
frontend/src/pages/admin/EditProduct.jsx | 206 +++++++++++++++++++++++
frontend/src/services/api.js | 1 +
4 files changed, 251 insertions(+), 11 deletions(-)
create mode 100644 frontend/src/pages/admin/EditProduct.jsx
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 7c315db..b45ef64 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -22,6 +22,8 @@ const Orders = lazy(() => import('./pages/Orders'))
const OrderDetail = lazy(() => import('./pages/OrderDetail'))
const Profile = lazy(() => import('./pages/Profile'))
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'))
+const AddProducts = lazy(() => import('./pages/admin/AddProduct'))
+const EditProduct = lazy(() => import('./pages/admin/EditProduct'))
function PageLoader() {
return (
@@ -67,6 +69,7 @@ export default function App() {
} />
}/>
}/>
+ }/>
diff --git a/frontend/src/pages/admin/AddProduct.jsx b/frontend/src/pages/admin/AddProduct.jsx
index 38c6a1e..aff740a 100644
--- a/frontend/src/pages/admin/AddProduct.jsx
+++ b/frontend/src/pages/admin/AddProduct.jsx
@@ -1,9 +1,9 @@
-import { useState } from 'react'
+import { useEffect,useState } from 'react'
import { useNavigate } from 'react-router-dom'
import toast from 'react-hot-toast'
import { productsAPI } from '../../services/api'
-export default function AddProduct() {
+export default function AddProducts() {
const navigate = useNavigate()
const [formData, setFormData] = useState({
@@ -57,6 +57,24 @@ export default function AddProduct() {
}
}
+ const [categories, setCategories] = useState([])
+
+ useEffect(() => {
+ const loadCategories = async () => {
+ try {
+ const { data } = await productsAPI.categories()
+
+ setCategories(data.results || data)
+ } catch (error) {
+ console.error(error)
+ toast.error('Failed to load categories')
+ }
+ }
+
+ loadCategories()
+}, [])
+
+
return (
@@ -128,14 +146,26 @@ export default function AddProduct() {
onChange={handleChange}
/>
-
+
@@ -173,4 +203,4 @@ export default function AddProduct() {
)
-}
\ No newline at end of file
+}
diff --git a/frontend/src/pages/admin/EditProduct.jsx b/frontend/src/pages/admin/EditProduct.jsx
new file mode 100644
index 0000000..67ebfbf
--- /dev/null
+++ b/frontend/src/pages/admin/EditProduct.jsx
@@ -0,0 +1,206 @@
+import { useEffect, useState } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
+import toast from 'react-hot-toast'
+import { productsAPI } from '../../services/api'
+
+export default function EditProduct() {
+ const { slug } = useParams()
+ const navigate = useNavigate()
+
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [categories, setCategories] = useState([])
+
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ price: '',
+ discount_price: '',
+ stock: '',
+ sku: '',
+ brand: '',
+ category: '',
+ is_featured: false,
+ })
+
+ useEffect(() => {
+ loadProduct()
+ loadCategories()
+ }, [slug])
+
+ const loadProduct = async () => {
+ try {
+ const { data } = await productsAPI.get(slug)
+
+ setFormData({
+ name: data.name || '',
+ description: data.description || '',
+ price: data.price || '',
+ discount_price: data.discount_price || '',
+ stock: data.stock || '',
+ sku: data.sku || '',
+ brand: data.brand || '',
+ category: data.category?.id || '',
+ is_featured: data.is_featured || false,
+ })
+ } catch {
+ toast.error('Failed to load product')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const loadCategories = async () => {
+ try {
+ const { data } = await productsAPI.categories()
+ setCategories(data.results || data)
+ } catch {
+ toast.error('Failed to load categories')
+ }
+ }
+
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target
+
+ setFormData((prev) => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value,
+ }))
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ try {
+ setSaving(true)
+
+ await productsAPI.update(slug, {
+ ...formData,
+ category: Number(formData.category),
+ })
+
+ toast.success('Product updated successfully')
+
+ navigate('/admin/products')
+ } catch {
+ toast.error('Failed to update product')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ if (loading) return
Loading...
+
+ return (
+
+)
+}
\ No newline at end of file
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 4c049c5..fa1532e 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -76,6 +76,7 @@ export const productsAPI = {
create: (data) => api.post('/products/', data),
update: (slug, data) => api.patch(`/products/${slug}/`, data),
delete: (slug) => api.delete(`/products/${slug}/`),
+
}
// Cart
From de7323d6bf3037b2271c4821c806976340f2af16 Mon Sep 17 00:00:00 2001
From: vishnu R <157986076+vishnu-R2005@users.noreply.github.com>
Date: Sat, 13 Jun 2026 21:59:23 +0530
Subject: [PATCH 5/9] feat(products): add product image upload API
---
backend/apps/products/serializers.py | 10 ++++++++--
backend/apps/products/urls.py | 10 +++++++++-
backend/apps/products/views.py | 19 ++++++++++++++++++-
3 files changed, 35 insertions(+), 4 deletions(-)
diff --git a/backend/apps/products/serializers.py b/backend/apps/products/serializers.py
index 72aa0e8..9005ff7 100644
--- a/backend/apps/products/serializers.py
+++ b/backend/apps/products/serializers.py
@@ -24,8 +24,14 @@ class Meta:
class ProductImageSerializer(serializers.ModelSerializer):
class Meta:
model = ProductImage
- fields = ['id', 'image', 'alt_text', 'is_primary', 'order']
-
+ fields = [
+ 'id',
+ 'product',
+ 'image',
+ 'alt_text',
+ 'is_primary',
+ 'order',
+ ]
class ProductListSerializer(serializers.ModelSerializer):
effective_price = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
diff --git a/backend/apps/products/urls.py b/backend/apps/products/urls.py
index 0b8b224..1e4016f 100644
--- a/backend/apps/products/urls.py
+++ b/backend/apps/products/urls.py
@@ -2,13 +2,21 @@
from rest_framework.routers import DefaultRouter
from .views import CategoryViewSet, ProductViewSet, ReviewViewSet, WishlistViewSet
-
+from .views import (
+ CategoryViewSet,
+ ProductViewSet,
+ ReviewViewSet,
+ WishlistViewSet,
+ ProductImageViewSet,
+)
router = DefaultRouter()
router.register('categories', CategoryViewSet, basename='category')
router.register('reviews', ReviewViewSet, basename='review')
router.register('wishlist', WishlistViewSet, basename='wishlist')
+router.register('images',ProductImageViewSet,basename='product-image')
router.register('', ProductViewSet, basename='product')
urlpatterns = [
path('', include(router.urls)),
+
]
diff --git a/backend/apps/products/views.py b/backend/apps/products/views.py
index 2fb9f97..63048e2 100644
--- a/backend/apps/products/views.py
+++ b/backend/apps/products/views.py
@@ -3,7 +3,7 @@
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
-
+from rest_framework.parsers import MultiPartParser,FormParser
from apps.common.permissions import IsAdmin, IsAdminOrReadOnly
from .filters import ProductFilter
@@ -21,6 +21,8 @@
)
from .services import CategoryService, ProductService, ReviewService, WishlistService
+from .models import ProductImage
+from .serializers import ProductImageSerializer
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.filter(is_active=True)
@@ -172,3 +174,18 @@ def add(self, request, product_id=None):
def remove(self, request, product_id=None):
WishlistService.remove_from_wishlist(request.user, product_id)
return Response({'success': True, 'message': 'Removed from wishlist.'})
+
+class ProductImageViewSet(viewsets.ModelViewSet):
+ queryset = ProductImage.objects.all()
+ serializer_class = ProductImageSerializer
+ permission_classes = [IsAdmin]
+
+ parser_classes = [MultiPartParser, FormParser]
+
+ @extend_schema(tags=['Product Images'])
+ def create(self, request, *args, **kwargs):
+ return super().create(request, *args, **kwargs)
+
+ @extend_schema(tags=['Product Images'])
+ def destroy(self, request, *args, **kwargs):
+ return super().destroy(request, *args, **kwargs)
\ No newline at end of file
From a3812fe1d64ed536107371889ee862bd70240d64 Mon Sep 17 00:00:00 2001
From: vishnu R <157986076+vishnu-R2005@users.noreply.github.com>
Date: Sat, 13 Jun 2026 22:16:28 +0530
Subject: [PATCH 6/9] feat(admin): implement product CRUD and image upload
support
---
frontend/src/services/api.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index fa1532e..0dbe528 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -49,6 +49,8 @@ api.interceptors.request.use((config) => {
return config
})
+
+
export default api
// Auth
@@ -88,6 +90,18 @@ export const cartAPI = {
clear: () => api.delete('/cart/clear/'),
}
+export const productImagesAPI = {
+ upload: (formData) =>
+ api.post('/products/images/', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ }),
+
+ remove: (imageId) =>
+ api.delete(`/products/images/${imageId}/`),
+}
+
// Wishlist
export const wishlistAPI = {
list: () => api.get('/products/wishlist/'),
From 0f6ab005801256d794bd9ccd05dd8910923f27c1 Mon Sep 17 00:00:00 2001
From: vishnu R <157986076+vishnu-R2005@users.noreply.github.com>
Date: Sun, 14 Jun 2026 20:26:52 +0530
Subject: [PATCH 7/9] feat(products): complete product CRUD with image upload
support
---
backend/apps/products/serializers.py | 2 +-
frontend/src/pages/admin/AddProduct.jsx | 52 ++++++++++++++++------
frontend/src/pages/admin/AdminProducts.jsx | 8 ++--
frontend/src/pages/admin/EditProduct.jsx | 42 +++++++++++++----
4 files changed, 76 insertions(+), 28 deletions(-)
diff --git a/backend/apps/products/serializers.py b/backend/apps/products/serializers.py
index 9005ff7..835a97b 100644
--- a/backend/apps/products/serializers.py
+++ b/backend/apps/products/serializers.py
@@ -68,7 +68,7 @@ class ProductCreateUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = [
- 'name', 'description', 'price', 'discount_price', 'stock',
+ 'id','name', 'description', 'price', 'discount_price', 'stock',
'sku', 'brand', 'category', 'is_featured', 'is_active',
]
diff --git a/frontend/src/pages/admin/AddProduct.jsx b/frontend/src/pages/admin/AddProduct.jsx
index aff740a..127ef7f 100644
--- a/frontend/src/pages/admin/AddProduct.jsx
+++ b/frontend/src/pages/admin/AddProduct.jsx
@@ -1,10 +1,10 @@
import { useEffect,useState } from 'react'
import { useNavigate } from 'react-router-dom'
import toast from 'react-hot-toast'
-import { productsAPI } from '../../services/api'
-
+import { productsAPI, productImagesAPI } from '../../services/api'
export default function AddProducts() {
const navigate = useNavigate()
+
const [formData, setFormData] = useState({
name: '',
@@ -20,6 +20,7 @@ export default function AddProducts() {
})
const [loading, setLoading] = useState(false)
+ const [selectedImage, setSelectedImage] = useState(null)
const handleChange = (e) => {
const { name, value, type, checked } = e.target
@@ -36,23 +37,35 @@ export default function AddProducts() {
try {
setLoading(true)
- await productsAPI.create({
- ...formData,
- price: Number(formData.price),
- discount_price: formData.discount_price
- ? Number(formData.discount_price)
- : null,
- stock: Number(formData.stock),
- category: Number(formData.category),
- })
+ const { data: product } = await productsAPI.create({
+ ...formData,
+ price: Number(formData.price),
+ discount_price: formData.discount_price
+ ? Number(formData.discount_price)
+ : null,
+ stock: Number(formData.stock),
+ category: Number(formData.category),
+})
+console.log("PRODUCT ID:", product.id)
+console.log("PRODUCT RESPONSE:", product)
+if (selectedImage) {
+ const imageData = new FormData()
+
+ imageData.append('product', product.id)
+ imageData.append('image', selectedImage)
+ imageData.append('alt_text', formData.name)
+ imageData.append('is_primary', true)
+ imageData.append('order', 1)
+
+ await productImagesAPI.upload(imageData)
+}
toast.success('Product created successfully')
navigate('/admin/products')
} catch (error) {
- console.error(error)
- toast.error('Failed to create product')
- } finally {
+ console.log(JSON.stringify(error.response?.data, null, 2))
+}finally {
setLoading(false)
}
}
@@ -191,6 +204,17 @@ export default function AddProducts() {
+