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
|
||||
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"
|
||||
access_token_expire_minutes: int = 30
|
||||
frontend_url: str = "http://localhost:5173"
|
||||
backend_url: str = "http://localhost:8000"
|
||||
|
||||
# Admin user credentials (created on first startup)
|
||||
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 typing import List
|
||||
from app.database.database import get_db
|
||||
from app.models import Category, User
|
||||
from app.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate
|
||||
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"])
|
||||
|
||||
@ -69,3 +71,25 @@ def delete_category(
|
||||
db.delete(category)
|
||||
db.commit()
|
||||
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.utils import save_upload_file, generate_slug
|
||||
from app.config import settings
|
||||
|
||||
router = APIRouter(prefix="/api/products", tags=["products"])
|
||||
|
||||
@ -152,7 +153,7 @@ async def upload_product_image(
|
||||
|
||||
try:
|
||||
# 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 {"url": file_path, "message": "Image uploaded successfully"}
|
||||
except Exception as e:
|
||||
@ -175,7 +176,7 @@ async def upload_multiple_images(
|
||||
continue
|
||||
|
||||
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)
|
||||
except Exception as 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)
|
||||
return slug
|
||||
|
||||
def save_upload_file(upload_file, folder: str = "products") -> str:
|
||||
"""Save uploaded file and return the file path"""
|
||||
def save_upload_file(upload_file, folder: str = "products", backend_url: str = None) -> str:
|
||||
"""Save uploaded file and return the file URL"""
|
||||
# Create uploads directory if it doesn't exist
|
||||
upload_dir = Path("uploads") / folder
|
||||
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:
|
||||
buffer.write(upload_file.file.read())
|
||||
|
||||
# Return relative path for URL
|
||||
return f"/uploads/{folder}/{unique_filename}"
|
||||
# Return full URL if backend_url provided, otherwise relative path
|
||||
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
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
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 (
|
||||
<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>
|
||||
<p>{category.description}</p>
|
||||
</Link>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import '../styles/global.css'
|
||||
|
||||
export default function ProductCard({ product }) {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const price = product.discount_price || product.price
|
||||
const discount =
|
||||
product.discount_price && product.is_on_sale
|
||||
@ -11,11 +12,20 @@ export default function ProductCard({ product }) {
|
||||
)
|
||||
: 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 (
|
||||
<Link to={`/product/${product.id}`} className="product-card-link">
|
||||
<div className="product-card">
|
||||
<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 && (
|
||||
<div className="discount-badge">{discount}% OFF</div>
|
||||
)}
|
||||
|
||||
@ -167,10 +167,6 @@ export default function Admin() {
|
||||
const newImages = [...uploadedImages]
|
||||
|
||||
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++) {
|
||||
const file = files[i]
|
||||
const formDataUpload = new FormData()
|
||||
@ -182,11 +178,8 @@ export default function Admin() {
|
||||
},
|
||||
})
|
||||
|
||||
// Add the full URL
|
||||
const imageUrl = response.data.url.startsWith('http')
|
||||
? response.data.url
|
||||
: `${backendUrl}${response.data.url}`
|
||||
newImages.push(imageUrl)
|
||||
// Backend now returns full URLs
|
||||
newImages.push(response.data.url)
|
||||
}
|
||||
|
||||
setUploadedImages(newImages)
|
||||
@ -341,16 +334,11 @@ export default function Admin() {
|
||||
formData.append('file', file)
|
||||
|
||||
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' }
|
||||
})
|
||||
// Prepend backend URL for image display
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
||||
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 })
|
||||
// Backend now returns full URLs
|
||||
setCategoryFormData({ ...categoryFormData, image: response.data.url })
|
||||
setToast({ type: 'success', message: 'Image uploaded successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user