Skip to content

Commit c89c3e3

Browse files
committed
Added S3 Image Upload API and Frontend
1 parent 3d56346 commit c89c3e3

File tree

16 files changed

+225
-26
lines changed

16 files changed

+225
-26
lines changed

Backend/EcommerceInventory/.env

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
DEBUG='True'
2+
# SECRET_KEY='django-insecure-n==@#u@5goj0o27k8isfo%6!k3fs(2u@3r7+n!ovu-uu1#p=gd'
3+
# DATABASE_NAME='ecommerce'
4+
# DATABASE_USER='ecommerce'
5+
# DATABASE_PASSWORD='ecommerce'
6+
# DATABASE_HOST='ecommerce.cfamkce00xa3.ap-south-1.rds.amazonaws.com'
7+
# DATABASE_PORT='3306'
28
SECRET_KEY='django-insecure-n==@#u@5goj0o27k8isfo%6!k3fs(2u@3r7+n!ovu-uu1#p=gd'
3-
DATABASE_NAME='ecommerce'
4-
DATABASE_USER='ecommerce'
5-
DATABASE_PASSWORD='ecommerce'
6-
DATABASE_HOST='ecommerce.cfamkce00xa3.ap-south-1.rds.amazonaws.com'
7-
DATABASE_PORT='3306'
9+
DATABASE_NAME='ecommerceproject'
10+
DATABASE_USER='ecommerceproject'
11+
DATABASE_PASSWORD='ecommerceproject'
12+
DATABASE_HOST='localhost'
13+
DATABASE_PORT='3306'
14+
15+
AWS_ACCESS_KEY_ID='ACCESS_KEY_ID'
16+
AWS_ACESS_KEY_SECRET='AWS_ACESS_KEY_SECRET'
17+
AWS_STORAGE_BUCKET_NAME='AWS_BUCKET_NAME'
18+
AWS_S3_REGION_NAME='AWS_S3_REGION_NAME'
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Backend/EcommerceInventory/EcommerceInventory/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,9 @@
172172
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
173173
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
174174
}
175+
176+
177+
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
178+
AWS_ACESS_KEY_SECRET = os.getenv('AWS_ACESS_KEY_SECRET')
179+
AWS_STORAGE_BUCKET_NAME=os.getenv('AWS_STORAGE_BUCKET_NAME')
180+
AWS_S3_REGION_NAME=os.getenv('AWS_S3_REGION_NAME')

Backend/EcommerceInventory/EcommerceInventory/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,22 @@
1717
from django.contrib import admin
1818
from django.urls import include, path, re_path
1919

20-
from EcommerceInventory.views import index
20+
from EcommerceInventory.views import index,FileUploadViewInS3
2121
from EcommerceInventory import settings
2222
from UserServices.Controller.DynamicFormController import DynamicFormController
2323
from UserServices.Controller.SuperAdminDynamicFormController import SuperAdminDynamicFormController
2424
from UserServices.Controller.SidebarController import ModuleView
2525
from django.conf.urls.static import static
2626

27+
2728
urlpatterns = [
2829
path('admin/', admin.site.urls),
2930
path('api/auth/', include('UserServices.urls')),
3031
path('api/getForm/<str:modelName>/',DynamicFormController.as_view(),name='dynamicForm'),
3132
path('api/superAdminForm/<str:modelName>/',SuperAdminDynamicFormController.as_view(),name='superadmindynamicForm'),
3233
path('api/getMenus/',ModuleView.as_view(),name='sidebarmenu'),
3334
path('api/products/',include('ProductServices.urls')),
35+
path('api/uploads/',FileUploadViewInS3.as_view(),name='fileupload')
3436
]
3537

3638
if settings.DEBUG:
Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,38 @@
11
from django.shortcuts import render
2+
from rest_framework.views import APIView
3+
from rest_framework.parsers import MultiPartParser,FormParser
4+
from rest_framework.response import Response
5+
from boto3.session import Session
6+
from EcommerceInventory.settings import AWS_ACCESS_KEY_ID,AWS_ACESS_KEY_SECRET,AWS_S3_REGION_NAME,AWS_STORAGE_BUCKET_NAME
7+
import os
8+
29
def index(request):
3-
return render(request, 'index.html')
10+
return render(request, 'index.html')
11+
12+
class FileUploadViewInS3(APIView):
13+
parser_classes=(MultiPartParser,FormParser)
14+
15+
def post(self,request,*args,**kwargs):
16+
uploaded_files_urls=[]
17+
for file_key in request.FILES:
18+
file_obj=request.FILES[file_key]
19+
s3_client=Session(
20+
aws_access_key_id=AWS_ACCESS_KEY_ID,
21+
aws_secret_access_key=AWS_ACESS_KEY_SECRET,
22+
region_name=AWS_S3_REGION_NAME
23+
).client("s3")
24+
25+
uniqueFileName=os.urandom(24).hex()+"_"+file_obj.name.replace(" ","_")
26+
file_path="uploads/"+uniqueFileName
27+
28+
s3_client.upload_fileobj(
29+
file_obj,
30+
AWS_STORAGE_BUCKET_NAME,
31+
file_path,
32+
ExtraArgs={
33+
'ContentType':file_obj.content_type
34+
}
35+
)
36+
s3url=f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/{file_path}"
37+
uploaded_files_urls.append(s3url)
38+
return Response({'message':'File uploaded successfully','urls':uploaded_files_urls},status=200)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {useFormContext} from 'react-hook-form';
2+
import { Box,FormControl,InputLabel,Select,MenuItem, FormControlLabel, Switch, TextField,Alert, Typography, IconButton, LinearProgress } from "@mui/material";
3+
import useApi from '../hooks/APIHandler';
4+
import { useEffect,useState } from 'react';
5+
import DescriptionIcon from '@mui/icons-material/Description';
6+
import { Delete } from '@mui/icons-material';
7+
import Button from '@mui/material/Button';
8+
import { toast } from 'react-toastify';
9+
10+
const FileInputComponent = ({field}) => {
11+
const {register,formState:{errors},watch,setValue} = useFormContext();
12+
const {callApi,loading}=useApi();
13+
const [selectedFiles,setSelectedFiles]=useState([]);
14+
const [filePreviews,setFilePreviews]=useState([]);
15+
const [fileUploaded,setFileUploaded]=useState(false);
16+
17+
const handleDeleteImage=(index)=>{
18+
// Delete File Actual
19+
const updatedFiles=[...selectedFiles]
20+
updatedFiles.splice(index,1);
21+
setSelectedFiles(updatedFiles);
22+
23+
//Delete File preview
24+
const updatedPreviews=[...filePreviews];
25+
updatedPreviews.splice(index,1);
26+
setFilePreviews(updatedPreviews);
27+
28+
setFileUploaded(false);
29+
}
30+
31+
const uploadFiles=async()=>{
32+
try{
33+
const formData=new FormData();
34+
selectedFiles.forEach((file,index)=>{
35+
formData.append(`file${index}`,file);
36+
})
37+
38+
const response=await callApi({url:'uploads/',method:'post',body:formData,header:{'Content-Type':'multipart/form-data'}});
39+
setValue(field.name,JSON.stringify(response?.data?.urls));
40+
toast.success(response?.data?.message);
41+
setFileUploaded(true);
42+
}
43+
catch(err){
44+
toast.error(err?.message);
45+
}
46+
}
47+
48+
const deleteAllFiles=()=>{
49+
setSelectedFiles([]);
50+
setFilePreviews([]);
51+
setFileUploaded(false);
52+
}
53+
54+
useEffect(()=>{
55+
if(!selectedFiles.length && watch(field.name)){
56+
console.log(watch(field.name));
57+
const fileArray=Array.from(watch(field.name)) || [];
58+
setSelectedFiles(fileArray);
59+
const preview=fileArray.map((file,index)=>({
60+
url:URL.createObjectURL(file),
61+
name:file.name,
62+
type:file.type.split('/')[0]
63+
}))
64+
console.log(preview);
65+
setFilePreviews(preview);
66+
setFileUploaded(false);
67+
}
68+
},[watch(field.name)]);
69+
70+
return (
71+
<>
72+
{
73+
filePreviews.length>0 && filePreviews.map((file,index)=>(
74+
<Box key={index} sx={{display:'flex',alignItems:'center',mb:2}}>
75+
{
76+
file.type==='image'?<img src={file.url} alt={file.name} style={{width:'60px',height:'60px'}} />:<DescriptionIcon sx={{width:'60px',height:'60px'}} />
77+
}
78+
<Typography variant='body1' p={1}>{file.name}</Typography>
79+
<IconButton onClick={()=>handleDeleteImage(index)} sx={{color:'red'}}>
80+
<Delete/>
81+
</IconButton>
82+
</Box>
83+
))
84+
}
85+
{!filePreviews.length &&
86+
<Box p={1} mb={1}>
87+
<Typography variant='title'>{field.label}</Typography>
88+
<Box component={"div"} className='fileInput' mt={1}>
89+
<input type='file' multiple {...register(field.name,{required:field.required})} />
90+
</Box>
91+
{
92+
!!errors[field.name] && <Alert variant="outlined" severity='error'>
93+
This Field is Required
94+
</Alert>
95+
}
96+
</Box>
97+
}
98+
{
99+
selectedFiles.length>0 && !fileUploaded && (
100+
loading?<LinearProgress sx={{width:'100%'}}/>:
101+
<Box mt={2} display="flex" justifyContent="space-between">
102+
<Button onClick={uploadFiles} variant='contained' color='primary'>Upload Files</Button>
103+
<Button onClick={deleteAllFiles} color='primary' variant='contained'>Delete All Files</Button>
104+
</Box>
105+
)
106+
}
107+
</>
108+
)
109+
}
110+
export default FileInputComponent;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { isValidUrl,getImageUrl } from "../utils/Helper";
2+
import { Typography } from "@mui/material";
3+
const RenderImage=({data,name})=>{
4+
return (data && data!=='' && isValidUrl(data))?<img src={getImageUrl(data)} alt={name} style={{width:70,height:70,padding:'5px'}}/>:<Typography variant="body2" pt={3} pb={3}>No Image</Typography>
5+
}
6+
export default RenderImage;

Frontend/ecommerce_inventory/src/components/StepFileComponents.js

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,12 @@
11
import {useFormContext} from 'react-hook-form';
22
import { Box,FormControl,InputLabel,Select,MenuItem, FormControlLabel, Switch, TextField,Alert } from "@mui/material";
3+
import FileInputComponent from './FileInputComponents';
34

4-
const StepFileComponents = ({formConfig,fieldType}) => {
5-
const {register,formState:{errors}} = useFormContext();
6-
const fileFields=formConfig.data.file;
5+
const StepFileComponents = ({formConfig }) => {
76
return (
87
<Box>
9-
{fileFields.map((field,index)=>(
10-
<>
11-
<Box component={"div"} className='fileInput'>
12-
<label>{field.label}</label>
13-
<input type='file' {...register(field.name,{required:field.required})} />
14-
</Box>
15-
{
16-
!!errors[field.name] && <Alert variant="outlined" severity='error'>
17-
This Field is Required
18-
</Alert>
19-
}
20-
</>
8+
{formConfig?.data?.file?.map((field,index)=>(
9+
<FileInputComponent field={field} key={index} />
2110
))}
2211
</Box>
2312
)

Frontend/ecommerce_inventory/src/pages/DynamicForm.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ const DynamicForm=()=>{
103103
</> : <LinearProgress/>}
104104
<Box mt={2} display="flex" justifyContent="space-between">
105105
{currentStep>0 && (<Button type='button' variant="contained" color="primary" onClick={()=>goToStep(currentStep-1)}><ArrowBackIosIcon sx={{fontSize:'18px',marginRight:'5px'}}/> Back</Button>)}
106-
{currentStep<steps.length-1?<Button type='button' variant="contained" color="primary" onClick={()=>nextStep()}> Next <ArrowForwardIosIcon sx={{fontSize:'18px',marginLeft:'5px'}}/></Button>:<Button variant="contained" color="primary" type="submit"><SaveIcon sx={{fontSize:'18px',marginRight:'5px'}}/> Submit</Button>}
106+
{currentStep<steps.length-1 && <Button type='button' variant="contained" color="primary" onClick={()=>nextStep()}> Next <ArrowForwardIosIcon sx={{fontSize:'18px',marginLeft:'5px'}}/></Button>}
107+
{<Button sx={{display:currentStep==steps.length-1?'block':'none'}} variant="contained" color="primary" type="submit"><SaveIcon sx={{fontSize:'20px',marginRight:'5px',margingTop:'8px'}}/> Submit</Button>}
107108
</Box>
108109
</form>
109110
</FormProvider>

Frontend/ecommerce_inventory/src/pages/category/ExpandableRow.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Box, Collapse, Typography } from "@mui/material"
33
import { DataGrid, GridRow, GridToolbar } from "@mui/x-data-grid"
44
import { IconButton } from "@mui/material"
55
import { Add, Delete, Edit } from "@mui/icons-material"
6+
import RenderImage from "../../components/RenderImage"
67

78
const ExpanableRow=({row,props,onEditClick,onDeleteClick})=>{
89
let columns=[]
@@ -11,8 +12,11 @@ const ExpanableRow=({row,props,onEditClick,onDeleteClick})=>{
1112
field:key,
1213
headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),
1314
width:150,
14-
})).filter((item)=>item.field!=='children')
15+
})).filter((item)=>item.field!=='children').filter((item)=>item.field!=='image');
1516

17+
columns.push({field:'image',headerName:'Image',width:150,sortable:false,renderCell:(params)=>{
18+
return <RenderImage data={params.row.image} name={params.row.name}/>
19+
}});
1620
columns=[{field:'action',headerName:'Action',width:180,sortable:false,renderCell:(params)=>{
1721
return <>
1822
<IconButton onClick={()=>onEditClick(params)}>

Frontend/ecommerce_inventory/src/pages/category/ManageCategories.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Edit from '@mui/icons-material/Edit';
1010
import ExpandLessRounded from '@mui/icons-material/ExpandLessRounded';
1111
import ExpandMoreRounded from '@mui/icons-material/ExpandMoreRounded';
1212
import ExpanableRow from "./ExpandableRow";
13+
import RenderImage from "../../components/RenderImage";
1314

1415
const ManageCategories = () => {
1516
const [data,setData]=useState([]);
@@ -105,7 +106,7 @@ const ManageCategories = () => {
105106
}
106107
else if(key==='image'){
107108
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:150,sortable:false,renderCell:(params)=>{
108-
return (params.row.image && params.row.image!=='' && isValidUrl(params.row.image))?<img src={params.row.image} alt={params.row.name} style={{width:70,height:70,padding:'5px'}}/>:<Typography variant="body2" pt={3} pb={3}>No Image</Typography>
109+
return <RenderImage data={params.row.image} name={params.row.name}/>
109110
}})
110111
}
111112
else{

Frontend/ecommerce_inventory/src/utils/Helper.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,48 @@ export const getUser=()=>{
4343

4444
export const isValidUrl=(url)=>{
4545
try{
46-
new URL(url);
46+
if(Array.isArray(url)){
47+
let image=url.filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);
48+
new URL(image[0]);
49+
}
50+
else if(checkIsJson(url) && JSON.parse(url).length>0){
51+
let image=JSON.parse(url).filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);
52+
new URL(image[0]);
53+
}
54+
else{
55+
new URL(url);
56+
}
4757
return true;
4858
}
4959
catch(e){
5060
return false;
5161
}
5262
}
5363

64+
export const getImageUrl=(url)=>{
65+
if(Array.isArray(url)){
66+
let image=url.filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);
67+
return image[0];
68+
}
69+
else if(checkIsJson(url) && JSON.parse(url).length>0){
70+
let image=JSON.parse(url).filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);
71+
return image[0];
72+
}
73+
else{
74+
return url;
75+
}
76+
}
77+
78+
export const checkIsJson=(str)=>{
79+
try{
80+
JSON.parse(str);
81+
}
82+
catch(e){
83+
return false;
84+
}
85+
return true;
86+
}
87+
5488
export const getFormTypes=()=>{
5589
return [
5690
{component:StepSelectComponents,label:"Basic Details",fieldType:'select'},

0 commit comments

Comments
 (0)