Fix image not load correcttlly
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
f9419c2f4a
commit
9176e32e6f
386
IMAGE_UPLOAD_FIX.md
Normal file
386
IMAGE_UPLOAD_FIX.md
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
# Image Upload & Loading Fix - Brand Master
|
||||||
|
|
||||||
|
## Problem Summary
|
||||||
|
|
||||||
|
Images were not loading correctly in the Brand Master application due to:
|
||||||
|
|
||||||
|
1. **URL Resolution Issue**: Images stored with relative paths (`/uploads/products/...`) were resolving to the frontend domain instead of the backend
|
||||||
|
2. **Missing Category Upload Endpoint**: No dedicated endpoint for category image uploads
|
||||||
|
3. **Ingress Routing**: No route from frontend domain to backend for serving static uploads
|
||||||
|
4. **No Fallback Handling**: Broken images displayed when URLs were invalid
|
||||||
|
|
||||||
|
## Solutions Implemented
|
||||||
|
|
||||||
|
### 1. Backend Changes
|
||||||
|
|
||||||
|
#### a. Updated Settings Configuration
|
||||||
|
**File**: `backend/app/config.py`
|
||||||
|
- Added `backend_url` setting to store the backend URL
|
||||||
|
- This is set via `BACKEND_URL` environment variable in Kubernetes
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend_url: str = "http://localhost:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### b. Enhanced Upload Utility
|
||||||
|
**File**: `backend/app/utils.py`
|
||||||
|
- Modified `save_upload_file()` to accept `backend_url` parameter
|
||||||
|
- Now returns full URLs (e.g., `https://api-brand-master.dvirlabs.com/uploads/products/...`) instead of relative paths
|
||||||
|
- Maintains backward compatibility with relative paths if `backend_url` not provided
|
||||||
|
|
||||||
|
```python
|
||||||
|
def save_upload_file(upload_file, folder: str = "products", backend_url: str = None) -> str:
|
||||||
|
# ... saves file ...
|
||||||
|
relative_path = f"/uploads/{folder}/{unique_filename}"
|
||||||
|
if backend_url:
|
||||||
|
backend_url = backend_url.rstrip('/')
|
||||||
|
return f"{backend_url}{relative_path}"
|
||||||
|
return relative_path
|
||||||
|
```
|
||||||
|
|
||||||
|
#### c. Updated Product Upload Endpoints
|
||||||
|
**File**: `backend/app/routers/products.py`
|
||||||
|
- Updated `/api/products/upload-image` to use `backend_url` from settings
|
||||||
|
- Updated `/api/products/upload-images` to use `backend_url` from settings
|
||||||
|
- Images now return full URLs immediately
|
||||||
|
|
||||||
|
#### d. Added Category Upload Endpoint
|
||||||
|
**File**: `backend/app/routers/categories.py`
|
||||||
|
- Added new endpoint: `POST /api/categories/upload-image`
|
||||||
|
- Validates image file types (jpeg, png, jpg, webp, gif)
|
||||||
|
- Returns full URL with backend domain
|
||||||
|
- Saves to `uploads/categories/` folder
|
||||||
|
|
||||||
|
### 2. Frontend Changes
|
||||||
|
|
||||||
|
#### a. Updated Admin Panel Image Upload
|
||||||
|
**File**: `frontend/src/pages/Admin.jsx`
|
||||||
|
|
||||||
|
**Product Images**:
|
||||||
|
- Removed URL building logic (backend returns full URLs)
|
||||||
|
- Simplified `handleImageUpload()` function
|
||||||
|
- Backend now handles URL generation
|
||||||
|
|
||||||
|
**Category Images**:
|
||||||
|
- Changed from `/products/upload-image` to `/categories/upload-image`
|
||||||
|
- Removed URL building logic
|
||||||
|
- Directly uses backend-returned full URLs
|
||||||
|
|
||||||
|
#### b. Added Fallback Image Handling
|
||||||
|
**File**: `frontend/src/components/ProductCard.jsx`
|
||||||
|
- Added error state tracking with `useState`
|
||||||
|
- Handles image load failures with `onError` handler
|
||||||
|
- Falls back to placeholder image if product image fails to load
|
||||||
|
- Fallback: `https://via.placeholder.com/400x400?text={product-name}`
|
||||||
|
|
||||||
|
**File**: `frontend/src/components/CategoryCard.jsx`
|
||||||
|
- Added error state tracking with `useState`
|
||||||
|
- Handles image load failures with `onError` handler
|
||||||
|
- Falls back to placeholder image if category image fails to load
|
||||||
|
- Fallback: `https://via.placeholder.com/300x300?text={category-name}`
|
||||||
|
|
||||||
|
### 3. Kubernetes/Helm Changes
|
||||||
|
|
||||||
|
#### a. Updated Frontend Ingress
|
||||||
|
**File**: `charts/brand-master-chart/templates/frontend-ingress.yaml`
|
||||||
|
- Added `/uploads` path routing to backend service
|
||||||
|
- This provides backward compatibility for any existing relative URLs
|
||||||
|
- Routes `/uploads/*` requests from frontend domain to backend service
|
||||||
|
- Ensures images work even if old relative URLs exist in database
|
||||||
|
|
||||||
|
**Path Priority**:
|
||||||
|
1. `/uploads` → Backend Service (for images)
|
||||||
|
2. `/` → Frontend Service (for everything else)
|
||||||
|
|
||||||
|
#### b. Persistence Configuration
|
||||||
|
**Already Configured** in `manifests/brand-master/values.yaml`:
|
||||||
|
- PersistentVolumeClaim enabled for backend
|
||||||
|
- Storage: 15Gi on NFS storage class
|
||||||
|
- Mount path: `/app/uploads`
|
||||||
|
- Ensures images persist across pod restarts
|
||||||
|
|
||||||
|
## Deployment Instructions
|
||||||
|
|
||||||
|
### 1. Backend Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/OneDrive/Desktop/gitea/brand-master
|
||||||
|
|
||||||
|
# Build new backend image
|
||||||
|
docker build -t harbor.dvirlabs.com/my-apps/brand-master-backend:latest -f backend/Dockerfile backend/
|
||||||
|
|
||||||
|
# Push to Harbor
|
||||||
|
docker push harbor.dvirlabs.com/my-apps/brand-master-backend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Frontend Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/OneDrive/Desktop/gitea/brand-master
|
||||||
|
|
||||||
|
# Build new frontend image
|
||||||
|
docker build -t harbor.dvirlabs.com/my-apps/brand-master-frontend:latest -f frontend/Dockerfile frontend/
|
||||||
|
|
||||||
|
# Push to Harbor
|
||||||
|
docker push harbor.dvirlabs.com/my-apps/brand-master-frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Update Helm Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/OneDrive/Desktop/gitea/my-apps
|
||||||
|
|
||||||
|
# Update image tags in values.yaml if using specific tags
|
||||||
|
# Or use :latest for auto-pull
|
||||||
|
|
||||||
|
# Upgrade Helm release
|
||||||
|
helm upgrade brand-master charts/brand-master-chart \
|
||||||
|
-f manifests/brand-master/values.yaml \
|
||||||
|
-n my-apps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check pods are running
|
||||||
|
kubectl get pods -n my-apps -l app.kubernetes.io/name=brand-master
|
||||||
|
|
||||||
|
# Check backend logs
|
||||||
|
kubectl logs -n my-apps -l app.kubernetes.io/component=backend --tail=50
|
||||||
|
|
||||||
|
# Check frontend logs
|
||||||
|
kubectl logs -n my-apps -l app.kubernetes.io/component=frontend --tail=50
|
||||||
|
|
||||||
|
# Verify PVC is mounted
|
||||||
|
kubectl describe pod -n my-apps -l app.kubernetes.io/component=backend | grep uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Instructions
|
||||||
|
|
||||||
|
### 1. Test Category Image Upload
|
||||||
|
|
||||||
|
1. Login as admin: https://brand-master.dvirlabs.com/admin
|
||||||
|
2. Go to "Categories" tab
|
||||||
|
3. Create or edit a category
|
||||||
|
4. Upload an image
|
||||||
|
5. **Expected**: Image preview shows immediately
|
||||||
|
6. Save category
|
||||||
|
7. **Expected**: Image displays on home page category cards
|
||||||
|
8. Refresh page
|
||||||
|
9. **Expected**: Image still loads correctly
|
||||||
|
|
||||||
|
### 2. Test Product Image Upload
|
||||||
|
|
||||||
|
1. Login as admin
|
||||||
|
2. Go to "Products" tab
|
||||||
|
3. Create or edit a product
|
||||||
|
4. Upload one or more images
|
||||||
|
5. **Expected**: Image previews show immediately in upload section
|
||||||
|
6. Save product
|
||||||
|
7. **Expected**: Product displays correctly in products list
|
||||||
|
8. View product detail page
|
||||||
|
9. **Expected**: All images load and gallery works
|
||||||
|
10. Refresh page
|
||||||
|
11. **Expected**: All images still load correctly
|
||||||
|
|
||||||
|
### 3. Test Image Persistence
|
||||||
|
|
||||||
|
1. Upload some product/category images
|
||||||
|
2. Restart backend pod:
|
||||||
|
```bash
|
||||||
|
kubectl rollout restart deployment -n my-apps brand-master-backend
|
||||||
|
```
|
||||||
|
3. Wait for pod to be ready
|
||||||
|
4. **Expected**: All previously uploaded images still load correctly
|
||||||
|
|
||||||
|
### 4. Test Fallback Handling
|
||||||
|
|
||||||
|
1. Manually break an image URL in the database (or edit in admin with invalid URL)
|
||||||
|
2. View product/category with broken image
|
||||||
|
3. **Expected**: Placeholder image displays instead of broken image icon
|
||||||
|
4. No console errors about failed image loads
|
||||||
|
|
||||||
|
### 5. Verify URLs in Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to database pod
|
||||||
|
kubectl exec -it -n my-apps brand-master-db-0 -- psql -U brand_master_user -d brand_master_db
|
||||||
|
|
||||||
|
# Check product image URLs
|
||||||
|
SELECT id, name, images FROM product LIMIT 5;
|
||||||
|
|
||||||
|
# Check category image URLs
|
||||||
|
SELECT id, name, image FROM category;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected URL Format**:
|
||||||
|
- New images: `https://api-brand-master.dvirlabs.com/uploads/products/...`
|
||||||
|
- Old images (if any): `/uploads/products/...` (will still work via ingress routing)
|
||||||
|
|
||||||
|
### 6. Test Direct Image Access
|
||||||
|
|
||||||
|
1. Upload an image and note the URL returned
|
||||||
|
2. Copy the full URL
|
||||||
|
3. Open in new browser tab
|
||||||
|
4. **Expected**: Image loads directly
|
||||||
|
5. Test both domains:
|
||||||
|
- `https://api-brand-master.dvirlabs.com/uploads/...` (backend)
|
||||||
|
- `https://brand-master.dvirlabs.com/uploads/...` (frontend → routed to backend)
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Ingress (Traefik) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ api-brand-master.dvirlabs.com │ brand-master.dvirlabs.com
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Backend Svc │ │ Frontend Svc │
|
||||||
|
│ (Port 8000) │ │ (Port 80) │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
│ │
|
||||||
|
│ ├── /uploads → Backend Svc
|
||||||
|
│ └── /* → Frontend Pod
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Backend Pod │
|
||||||
|
│ - FastAPI │
|
||||||
|
│ - Static Files │
|
||||||
|
│ at /uploads │
|
||||||
|
└─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ PVC (15Gi) │
|
||||||
|
│ /app/uploads │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Flow
|
||||||
|
|
||||||
|
### Upload Flow:
|
||||||
|
1. Admin uploads image via frontend form
|
||||||
|
2. Frontend sends file to `/api/products/upload-image` or `/api/categories/upload-image`
|
||||||
|
3. Backend saves file to `uploads/{folder}/{uuid}{ext}` on PVC
|
||||||
|
4. Backend returns full URL: `https://api-brand-master.dvirlabs.com/uploads/{folder}/{filename}`
|
||||||
|
5. Frontend stores full URL in database
|
||||||
|
|
||||||
|
### Display Flow (New Images):
|
||||||
|
1. Frontend renders `<img src="https://api-brand-master.dvirlabs.com/uploads/..."/>`
|
||||||
|
2. Browser requests image from backend domain
|
||||||
|
3. Ingress routes to Backend Service
|
||||||
|
4. FastAPI serves file via StaticFiles mount
|
||||||
|
5. Image displays
|
||||||
|
|
||||||
|
### Display Flow (Old Relative URLs - Backup):
|
||||||
|
1. Frontend renders `<img src="/uploads/..."/>`
|
||||||
|
2. Browser requests from frontend domain
|
||||||
|
3. Ingress routes `/uploads/*` to Backend Service
|
||||||
|
4. FastAPI serves file
|
||||||
|
5. Image displays
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: Images still not loading
|
||||||
|
|
||||||
|
**Check 1**: Verify backend URL is set correctly
|
||||||
|
```bash
|
||||||
|
kubectl get pod -n my-apps -l app.kubernetes.io/component=backend -o jsonpath='{.items[0].spec.containers[0].env[?(@.name=="BACKEND_URL")].value}'
|
||||||
|
```
|
||||||
|
Should return: `https://api-brand-master.dvirlabs.com`
|
||||||
|
|
||||||
|
**Check 2**: Verify StaticFiles is mounted
|
||||||
|
```bash
|
||||||
|
kubectl exec -n my-apps -l app.kubernetes.io/component=backend -- ls -la /app/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check 3**: Verify ingress routes
|
||||||
|
```bash
|
||||||
|
kubectl get ingress -n my-apps brand-master-frontend -o yaml
|
||||||
|
```
|
||||||
|
Should show `/uploads` path routing to backend service.
|
||||||
|
|
||||||
|
**Check 4**: Test direct backend access
|
||||||
|
```bash
|
||||||
|
curl -I https://api-brand-master.dvirlabs.com/uploads/products/some-file.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Images disappear after pod restart
|
||||||
|
|
||||||
|
**Check**: PVC is properly mounted
|
||||||
|
```bash
|
||||||
|
kubectl get pvc -n my-apps
|
||||||
|
kubectl describe pvc -n my-apps brand-master-uploads-pvc
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
- STATUS: Bound
|
||||||
|
- STORAGECLASS: nfs-client
|
||||||
|
- CAPACITY: 15Gi
|
||||||
|
|
||||||
|
### Issue: Upload fails with 500 error
|
||||||
|
|
||||||
|
**Check backend logs**:
|
||||||
|
```bash
|
||||||
|
kubectl logs -n my-apps -l app.kubernetes.io/component=backend --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Common causes:
|
||||||
|
- Permissions issue on /app/uploads
|
||||||
|
- PVC not mounted
|
||||||
|
- Disk space full
|
||||||
|
|
||||||
|
## Summary of Files Changed
|
||||||
|
|
||||||
|
### Backend (Code Repo)
|
||||||
|
- ✅ `backend/app/config.py` - Added backend_url setting
|
||||||
|
- ✅ `backend/app/utils.py` - Updated save_upload_file to return full URLs
|
||||||
|
- ✅ `backend/app/routers/products.py` - Updated to use backend_url
|
||||||
|
- ✅ `backend/app/routers/categories.py` - Added upload endpoint
|
||||||
|
|
||||||
|
### Frontend (Code Repo)
|
||||||
|
- ✅ `frontend/src/pages/Admin.jsx` - Removed URL building, use backend URLs
|
||||||
|
- ✅ `frontend/src/components/ProductCard.jsx` - Added fallback handling
|
||||||
|
- ✅ `frontend/src/components/CategoryCard.jsx` - Added fallback handling
|
||||||
|
|
||||||
|
### Kubernetes (GitOps Repo)
|
||||||
|
- ✅ `charts/brand-master-chart/templates/frontend-ingress.yaml` - Added /uploads routing
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Deploy backend changes
|
||||||
|
2. Deploy frontend changes
|
||||||
|
3. Update Helm deployment with new ingress config
|
||||||
|
4. Test all image upload scenarios
|
||||||
|
5. Verify images persist across restarts
|
||||||
|
6. Monitor logs for any errors
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues occur after deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rollback Helm release
|
||||||
|
helm rollback brand-master -n my-apps
|
||||||
|
|
||||||
|
# Or rollback individual components
|
||||||
|
kubectl rollout undo deployment/brand-master-backend -n my-apps
|
||||||
|
kubectl rollout undo deployment/brand-master-frontend -n my-apps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables Reference
|
||||||
|
|
||||||
|
Required in backend deployment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
BACKEND_URL: "https://api-brand-master.dvirlabs.com" # Must be set!
|
||||||
|
FRONTEND_URL: "https://brand-master.dvirlabs.com"
|
||||||
|
PYTHONUNBUFFERED: "1"
|
||||||
|
# ... other env vars ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Already configured in `manifests/brand-master/values.yaml`.
|
||||||
29
apply-migration.bat
Normal file
29
apply-migration.bat
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@echo off
|
||||||
|
REM Apply database migration 005 - Add password reset fields
|
||||||
|
|
||||||
|
echo Getting database pod name...
|
||||||
|
for /f "delims=" %%i in ('kubectl get pod -n my-apps -l app.kubernetes.io/component=db -o jsonpath^="{.items[0].metadata.name}"') do set DB_POD=%%i
|
||||||
|
|
||||||
|
if "%DB_POD%"=="" (
|
||||||
|
echo ❌ Database pod not found
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 📦 Database pod: %DB_POD%
|
||||||
|
echo 📝 Applying migration 005_add_password_reset_fields.sql...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Copy migration file to pod
|
||||||
|
kubectl cp backend/migrations/005_add_password_reset_fields.sql my-apps/%DB_POD%:/tmp/migration.sql
|
||||||
|
|
||||||
|
REM Execute migration
|
||||||
|
kubectl exec -n my-apps %DB_POD% -- psql -U brand_master_user -d brand_master_db -f /tmp/migration.sql
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ✅ Migration applied successfully!
|
||||||
|
echo.
|
||||||
|
echo 🔄 Restarting backend pod to pick up changes...
|
||||||
|
kubectl delete pod -n my-apps -l app.kubernetes.io/component=backend
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ✅ Done! Backend will restart with updated schema.
|
||||||
29
apply-migration.sh
Normal file
29
apply-migration.sh
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Apply database migration 005 - Add password reset fields
|
||||||
|
|
||||||
|
# Get the database pod name
|
||||||
|
DB_POD=$(kubectl get pod -n my-apps -l app.kubernetes.io/component=db -o jsonpath='{.items[0].metadata.name}')
|
||||||
|
|
||||||
|
if [ -z "$DB_POD" ]; then
|
||||||
|
echo "❌ Database pod not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📦 Database pod: $DB_POD"
|
||||||
|
echo "📝 Applying migration 005_add_password_reset_fields.sql..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Copy migration file to pod
|
||||||
|
kubectl cp backend/migrations/005_add_password_reset_fields.sql my-apps/$DB_POD:/tmp/migration.sql
|
||||||
|
|
||||||
|
# Execute migration
|
||||||
|
kubectl exec -n my-apps $DB_POD -- psql -U brand_master_user -d brand_master_db -f /tmp/migration.sql
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Migration applied successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Restarting backend pod to pick up changes..."
|
||||||
|
kubectl delete pod -n my-apps -l app.kubernetes.io/component=backend
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Done! Backend will restart with updated schema."
|
||||||
@ -28,4 +28,3 @@ ENV PYTHONUNBUFFERED=1
|
|||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -8,6 +8,7 @@ class Settings(BaseSettings):
|
|||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
access_token_expire_minutes: int = 30
|
access_token_expire_minutes: int = 30
|
||||||
frontend_url: str = "http://localhost:5173"
|
frontend_url: str = "http://localhost:5173"
|
||||||
|
backend_url: str = "http://localhost:8000"
|
||||||
|
|
||||||
# Admin user credentials (created on first startup)
|
# Admin user credentials (created on first startup)
|
||||||
admin_email: str = "admin@brandmaster.com"
|
admin_email: str = "admin@brandmaster.com"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,10 +1,12 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
from typing import List
|
||||||
from app.database.database import get_db
|
from app.database.database import get_db
|
||||||
from app.models import Category, User
|
from app.models import Category, User
|
||||||
from app.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate
|
from app.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate
|
||||||
from app.services.auth import get_current_admin_user
|
from app.services.auth import get_current_admin_user
|
||||||
|
from app.utils import save_upload_file
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/categories", tags=["categories"])
|
router = APIRouter(prefix="/api/categories", tags=["categories"])
|
||||||
|
|
||||||
@ -69,3 +71,25 @@ def delete_category(
|
|||||||
db.delete(category)
|
db.delete(category)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Category deleted successfully"}
|
return {"message": "Category deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload-image")
|
||||||
|
async def upload_category_image(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
admin: User = Depends(get_current_admin_user),
|
||||||
|
):
|
||||||
|
"""Upload a category image and return the URL"""
|
||||||
|
# Validate file type
|
||||||
|
allowed_types = ["image/jpeg", "image/png", "image/jpg", "image/webp", "image/gif"]
|
||||||
|
if file.content_type not in allowed_types:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"File type {file.content_type} not allowed. Allowed types: {', '.join(allowed_types)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Save file and get full URL
|
||||||
|
file_path = save_upload_file(file, folder="categories", backend_url=settings.backend_url)
|
||||||
|
return {"url": file_path, "message": "Image uploaded successfully"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error uploading file: {str(e)}")
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from app.services.product import (
|
|||||||
)
|
)
|
||||||
from app.services.auth import get_current_admin_user
|
from app.services.auth import get_current_admin_user
|
||||||
from app.utils import save_upload_file, generate_slug
|
from app.utils import save_upload_file, generate_slug
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/products", tags=["products"])
|
router = APIRouter(prefix="/api/products", tags=["products"])
|
||||||
|
|
||||||
@ -152,7 +153,7 @@ async def upload_product_image(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Save file and get path
|
# Save file and get path
|
||||||
file_path = save_upload_file(file, folder="products")
|
file_path = save_upload_file(file, folder="products", backend_url=settings.backend_url)
|
||||||
# Return full URL
|
# Return full URL
|
||||||
return {"url": file_path, "message": "Image uploaded successfully"}
|
return {"url": file_path, "message": "Image uploaded successfully"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -175,7 +176,7 @@ async def upload_multiple_images(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_path = save_upload_file(file, folder="products")
|
file_path = save_upload_file(file, folder="products", backend_url=settings.backend_url)
|
||||||
uploaded_urls.append(file_path)
|
uploaded_urls.append(file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"File {file.filename}: {str(e)}")
|
errors.append(f"File {file.filename}: {str(e)}")
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -13,8 +13,8 @@ def generate_slug(text: str) -> str:
|
|||||||
slug = re.sub(r'^-+|-+$', '', slug)
|
slug = re.sub(r'^-+|-+$', '', slug)
|
||||||
return slug
|
return slug
|
||||||
|
|
||||||
def save_upload_file(upload_file, folder: str = "products") -> str:
|
def save_upload_file(upload_file, folder: str = "products", backend_url: str = None) -> str:
|
||||||
"""Save uploaded file and return the file path"""
|
"""Save uploaded file and return the file URL"""
|
||||||
# Create uploads directory if it doesn't exist
|
# Create uploads directory if it doesn't exist
|
||||||
upload_dir = Path("uploads") / folder
|
upload_dir = Path("uploads") / folder
|
||||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@ -28,5 +28,10 @@ def save_upload_file(upload_file, folder: str = "products") -> str:
|
|||||||
with open(file_path, "wb") as buffer:
|
with open(file_path, "wb") as buffer:
|
||||||
buffer.write(upload_file.file.read())
|
buffer.write(upload_file.file.read())
|
||||||
|
|
||||||
# Return relative path for URL
|
# Return full URL if backend_url provided, otherwise relative path
|
||||||
return f"/uploads/{folder}/{unique_filename}"
|
relative_path = f"/uploads/{folder}/{unique_filename}"
|
||||||
|
if backend_url:
|
||||||
|
# Remove trailing slash from backend_url if present
|
||||||
|
backend_url = backend_url.rstrip('/')
|
||||||
|
return f"{backend_url}{relative_path}"
|
||||||
|
return relative_path
|
||||||
|
|||||||
@ -67,3 +67,4 @@ EXPOSE 80
|
|||||||
|
|
||||||
# Start nginx
|
# Start nginx
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
export default function CategoryCard({ category }) {
|
export default function CategoryCard({ category }) {
|
||||||
const categoryImage = category.image || `https://via.placeholder.com/300x300?text=${category.name}`
|
const [imageError, setImageError] = useState(false)
|
||||||
|
const fallbackImage = `https://via.placeholder.com/300x300?text=${encodeURIComponent(category.name)}`
|
||||||
|
const categoryImage = (category.image && !imageError) ? category.image : fallbackImage
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/products?category=${category.slug}`} className="category-card">
|
<Link to={`/products?category=${category.slug}`} className="category-card">
|
||||||
<img src={categoryImage} alt={category.name} />
|
<img
|
||||||
|
src={categoryImage}
|
||||||
|
alt={category.name}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
<h3>{category.name}</h3>
|
<h3>{category.name}</h3>
|
||||||
<p>{category.description}</p>
|
<p>{category.description}</p>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import '../styles/global.css'
|
import '../styles/global.css'
|
||||||
|
|
||||||
export default function ProductCard({ product }) {
|
export default function ProductCard({ product }) {
|
||||||
|
const [imageError, setImageError] = useState(false)
|
||||||
const price = product.discount_price || product.price
|
const price = product.discount_price || product.price
|
||||||
const discount =
|
const discount =
|
||||||
product.discount_price && product.is_on_sale
|
product.discount_price && product.is_on_sale
|
||||||
@ -11,11 +12,20 @@ export default function ProductCard({ product }) {
|
|||||||
)
|
)
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
|
const fallbackImage = `https://via.placeholder.com/400x400?text=${encodeURIComponent(product.name)}`
|
||||||
|
const imageUrl = (product.images && product.images.length > 0 && !imageError)
|
||||||
|
? product.images[0]
|
||||||
|
: fallbackImage
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/product/${product.id}`} className="product-card-link">
|
<Link to={`/product/${product.id}`} className="product-card-link">
|
||||||
<div className="product-card">
|
<div className="product-card">
|
||||||
<div className="product-image-container">
|
<div className="product-image-container">
|
||||||
<img src={product.images[0]} alt={product.name} />
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={product.name}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
{product.is_on_sale && discount > 0 && (
|
{product.is_on_sale && discount > 0 && (
|
||||||
<div className="discount-badge">{discount}% OFF</div>
|
<div className="discount-badge">{discount}% OFF</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -167,10 +167,6 @@ export default function Admin() {
|
|||||||
const newImages = [...uploadedImages]
|
const newImages = [...uploadedImages]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get backend URL from environment
|
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
|
||||||
const backendUrl = apiUrl.replace('/api', '') // Remove /api suffix to get base URL
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i]
|
const file = files[i]
|
||||||
const formDataUpload = new FormData()
|
const formDataUpload = new FormData()
|
||||||
@ -182,11 +178,8 @@ export default function Admin() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add the full URL
|
// Backend now returns full URLs
|
||||||
const imageUrl = response.data.url.startsWith('http')
|
newImages.push(response.data.url)
|
||||||
? response.data.url
|
|
||||||
: `${backendUrl}${response.data.url}`
|
|
||||||
newImages.push(imageUrl)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadedImages(newImages)
|
setUploadedImages(newImages)
|
||||||
@ -341,16 +334,11 @@ export default function Admin() {
|
|||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/products/upload-image', formData, {
|
const response = await api.post('/categories/upload-image', formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
})
|
})
|
||||||
// Prepend backend URL for image display
|
// Backend now returns full URLs
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
setCategoryFormData({ ...categoryFormData, image: response.data.url })
|
||||||
const backendUrl = apiUrl.replace('/api', '') // Remove /api suffix to get base URL
|
|
||||||
const imageUrl = response.data.url.startsWith('http')
|
|
||||||
? response.data.url
|
|
||||||
: `${backendUrl}${response.data.url}`
|
|
||||||
setCategoryFormData({ ...categoryFormData, image: imageUrl })
|
|
||||||
setToast({ type: 'success', message: 'Image uploaded successfully!' })
|
setToast({ type: 'success', message: 'Image uploaded successfully!' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading image:', error)
|
console.error('Error uploading image:', error)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user