@tradly/asset 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EXAMPLE_USAGE.jsx +119 -0
- package/MIGRATION_GUIDE.md +236 -0
- package/README.md +292 -0
- package/STYLING_GUIDE.md +210 -0
- package/package.json +40 -0
- package/src/components/FileUpload.jsx +114 -0
- package/src/components/Icons.jsx +23 -0
- package/src/components/ImagesSkeleton.jsx +18 -0
- package/src/components/MediaGallery.jsx +125 -0
- package/src/components/MediaPopup.jsx +101 -0
- package/src/components/MediaTab.jsx +152 -0
- package/src/components/Pagination.jsx +175 -0
- package/src/components/VideosGallery.jsx +121 -0
- package/src/index.js +25 -0
- package/src/services/apiService.js +270 -0
package/STYLING_GUIDE.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Styling Guide - Customizing Media Gallery with Tailwind CSS
|
|
2
|
+
|
|
3
|
+
The `@tradly/media-gallery` package supports full customization of all components using Tailwind CSS class names. You can customize styles at every level of the component hierarchy.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
All components accept `className` props that allow you to override default styles. The package uses sensible Tailwind defaults, but you can customize everything to match your design system.
|
|
8
|
+
|
|
9
|
+
## MediaPopup Styling Props
|
|
10
|
+
|
|
11
|
+
The main popup component supports these styling props:
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<MediaPopup
|
|
15
|
+
// ... other props
|
|
16
|
+
overlayClassName="..." // Overlay/backdrop styles
|
|
17
|
+
containerClassName="..." // Main popup container
|
|
18
|
+
headerClassName="..." // Header container
|
|
19
|
+
titleClassName="..." // Title text
|
|
20
|
+
closeButtonClassName="..." // Close button
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Example
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
<MediaPopup
|
|
28
|
+
isOpen={isOpen}
|
|
29
|
+
onClose={() => setIsOpen(false)}
|
|
30
|
+
onSelect={handleSelect}
|
|
31
|
+
apiService={apiService}
|
|
32
|
+
overlayClassName="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm"
|
|
33
|
+
containerClassName="max-w-4xl bg-gray-900 text-white rounded-xl shadow-2xl"
|
|
34
|
+
titleClassName="text-3xl font-extrabold text-blue-400"
|
|
35
|
+
closeButtonClassName="text-white hover:text-red-400 hover:bg-red-900/20"
|
|
36
|
+
/>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## MediaTab Styling Props
|
|
40
|
+
|
|
41
|
+
Customize the tab navigation:
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
<MediaTab
|
|
45
|
+
// ... other props
|
|
46
|
+
className="..." // Tab group container
|
|
47
|
+
tabListClassName="..." // Tab list container
|
|
48
|
+
tabButtonClassName="..." // Base tab button styles
|
|
49
|
+
tabButtonActiveClassName="..." // Active tab button styles
|
|
50
|
+
tabButtonInactiveClassName="..." // Inactive tab button styles
|
|
51
|
+
tabPanelClassName="..." // Tab panel container
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Example
|
|
56
|
+
|
|
57
|
+
```jsx
|
|
58
|
+
<MediaTab
|
|
59
|
+
options={['image', 'video']}
|
|
60
|
+
apiService={apiService}
|
|
61
|
+
tabListClassName="bg-gray-800 border-b-2 border-blue-500"
|
|
62
|
+
tabButtonClassName="text-gray-300 hover:text-white px-6 py-3 transition-all"
|
|
63
|
+
tabButtonActiveClassName="text-blue-400 border-b-4 border-blue-400 font-bold"
|
|
64
|
+
tabButtonInactiveClassName="text-gray-400"
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## MediaGallery / VideosGallery Styling Props
|
|
69
|
+
|
|
70
|
+
Customize the media grid and items:
|
|
71
|
+
|
|
72
|
+
```jsx
|
|
73
|
+
<ImagesGallery
|
|
74
|
+
// ... other props
|
|
75
|
+
className="..." // Container
|
|
76
|
+
gridClassName="..." // Grid layout
|
|
77
|
+
imageItemClassName="..." // Individual image item
|
|
78
|
+
paginationContainerClassName="..." // Pagination container
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Example
|
|
83
|
+
|
|
84
|
+
```jsx
|
|
85
|
+
<ImagesGallery
|
|
86
|
+
apiService={apiService}
|
|
87
|
+
gridClassName="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4"
|
|
88
|
+
imageItemClassName="rounded-lg border-2 border-gray-300 hover:border-blue-500 transition-all cursor-pointer transform hover:scale-105"
|
|
89
|
+
paginationContainerClassName="bg-gray-100 p-6 rounded-lg mt-4"
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## FileUpload Styling Props
|
|
94
|
+
|
|
95
|
+
Customize the upload button:
|
|
96
|
+
|
|
97
|
+
```jsx
|
|
98
|
+
<FileUpload
|
|
99
|
+
// ... other props
|
|
100
|
+
className="..." // Container
|
|
101
|
+
buttonClassName="..." // Upload button
|
|
102
|
+
iconContainerClassName="..." // Icon container
|
|
103
|
+
titleClassName="..." // Title text
|
|
104
|
+
loadingClassName="..." // Loading state
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
<FileUpload
|
|
112
|
+
accept="image/*"
|
|
113
|
+
title="Upload Image"
|
|
114
|
+
apiService={apiService}
|
|
115
|
+
buttonClassName="border-2 border-dashed border-blue-500 bg-blue-50 hover:bg-blue-100 rounded-xl p-8"
|
|
116
|
+
iconContainerClassName="bg-blue-500 p-4 rounded-full"
|
|
117
|
+
titleClassName="text-blue-600 font-semibold mt-4"
|
|
118
|
+
loadingClassName="bg-blue-100 border-2 border-blue-300 rounded-xl"
|
|
119
|
+
/>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Pagination Styling Props
|
|
123
|
+
|
|
124
|
+
Customize pagination controls:
|
|
125
|
+
|
|
126
|
+
```jsx
|
|
127
|
+
<Pagination
|
|
128
|
+
// ... other props
|
|
129
|
+
className="..." // Container
|
|
130
|
+
navClassName="..." // Nav element
|
|
131
|
+
previousButtonClassName="..." // Previous button
|
|
132
|
+
nextButtonClassName="..." // Next button
|
|
133
|
+
pageButtonClassName="..." // Page number buttons
|
|
134
|
+
pageButtonActiveClassName="..." // Active page button
|
|
135
|
+
ellipsisClassName="..." // Ellipsis (...)
|
|
136
|
+
/>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Example
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
<Pagination
|
|
143
|
+
nextPage={handlePageChange}
|
|
144
|
+
current_page={currentPage}
|
|
145
|
+
pageCount={totalPages}
|
|
146
|
+
navClassName="flex items-center space-x-2"
|
|
147
|
+
previousButtonClassName="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg"
|
|
148
|
+
nextButtonClassName="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg"
|
|
149
|
+
pageButtonClassName="px-4 py-2 bg-white border border-gray-300 hover:bg-gray-100 rounded"
|
|
150
|
+
pageButtonActiveClassName="px-4 py-2 bg-blue-500 text-white font-bold rounded"
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Complete Customization Example
|
|
155
|
+
|
|
156
|
+
Here's a fully customized example:
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
import { MediaPopup, MediaApiService } from '@tradly/media-gallery'
|
|
160
|
+
|
|
161
|
+
function CustomStyledGallery() {
|
|
162
|
+
const apiService = new MediaApiService({
|
|
163
|
+
authKey: 'your-key',
|
|
164
|
+
bearerToken: 'your-token',
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<MediaPopup
|
|
169
|
+
isOpen={isOpen}
|
|
170
|
+
onClose={() => setIsOpen(false)}
|
|
171
|
+
onSelect={handleSelect}
|
|
172
|
+
apiService={apiService}
|
|
173
|
+
// Popup styles
|
|
174
|
+
overlayClassName="fixed inset-0 bg-black/60 backdrop-blur-sm"
|
|
175
|
+
containerClassName="max-w-5xl bg-gradient-to-br from-gray-900 to-gray-800 rounded-2xl shadow-2xl border border-gray-700"
|
|
176
|
+
headerClassName="flex items-center justify-between p-6 border-b border-gray-700"
|
|
177
|
+
titleClassName="text-2xl font-bold text-white"
|
|
178
|
+
closeButtonClassName="text-gray-400 hover:text-white hover:bg-gray-700 rounded-full p-2 transition-all"
|
|
179
|
+
// Tab styles (passed to MediaTab internally)
|
|
180
|
+
tabListClassName="bg-gray-800/50 border-b border-gray-700"
|
|
181
|
+
tabButtonClassName="text-gray-400 hover:text-white px-6 py-4 transition-colors"
|
|
182
|
+
tabButtonActiveClassName="text-blue-400 border-b-2 border-blue-400 font-semibold"
|
|
183
|
+
tabButtonInactiveClassName="text-gray-500"
|
|
184
|
+
// Gallery styles (passed to MediaGallery internally)
|
|
185
|
+
gridClassName="grid grid-cols-3 md:grid-cols-5 gap-4 p-6"
|
|
186
|
+
imageItemClassName="rounded-xl border-2 border-gray-700 hover:border-blue-500 cursor-pointer transition-all hover:scale-105 shadow-lg"
|
|
187
|
+
paginationContainerClassName="bg-gray-800/50 p-4 border-t border-gray-700"
|
|
188
|
+
/>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Styling Tips
|
|
194
|
+
|
|
195
|
+
1. **Use Tailwind's responsive prefixes**: `md:`, `lg:`, etc.
|
|
196
|
+
2. **Combine with your design system**: Use your existing color palette and spacing
|
|
197
|
+
3. **Maintain accessibility**: Keep contrast ratios and focus states
|
|
198
|
+
4. **Test hover states**: Ensure interactive elements have clear hover feedback
|
|
199
|
+
5. **Use transitions**: Add `transition-all` or `transition-colors` for smooth animations
|
|
200
|
+
|
|
201
|
+
## Default Classes Reference
|
|
202
|
+
|
|
203
|
+
If you want to see the default classes, check the component source files. All default classes use Tailwind utility classes and can be completely overridden.
|
|
204
|
+
|
|
205
|
+
## Notes
|
|
206
|
+
|
|
207
|
+
- All className props are optional
|
|
208
|
+
- If not provided, sensible defaults are used
|
|
209
|
+
- You can override individual parts without affecting others
|
|
210
|
+
- Classes are merged/applied using the `||` operator, so your custom classes completely replace defaults
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tradly/asset",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A reusable media gallery component for uploading and selecting images, videos, and files with Tradly authentication",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "echo 'Build script - configure with your bundler'",
|
|
10
|
+
"dev": "echo 'Dev script - configure with your bundler'"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"media",
|
|
14
|
+
"gallery",
|
|
15
|
+
"upload",
|
|
16
|
+
"images",
|
|
17
|
+
"videos",
|
|
18
|
+
"files",
|
|
19
|
+
"tradly"
|
|
20
|
+
],
|
|
21
|
+
"author": "tradly",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"homepage": "https://tradly.app/",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/TRADLY-PLATFORM"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=16.8.0",
|
|
33
|
+
"react-dom": ">=16.8.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@headlessui/react": "^1.7.0",
|
|
37
|
+
"axios": "^0.24.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {}
|
|
40
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { CameraIcon } from './Icons'
|
|
3
|
+
|
|
4
|
+
const FileUpload = ({
|
|
5
|
+
loadMedia,
|
|
6
|
+
accept,
|
|
7
|
+
title,
|
|
8
|
+
apiService,
|
|
9
|
+
onUploadStart,
|
|
10
|
+
onUploadComplete,
|
|
11
|
+
onUploadError,
|
|
12
|
+
// Styling props
|
|
13
|
+
className,
|
|
14
|
+
buttonClassName,
|
|
15
|
+
iconContainerClassName,
|
|
16
|
+
titleClassName,
|
|
17
|
+
loadingClassName,
|
|
18
|
+
}) => {
|
|
19
|
+
const [files, setFiles] = useState([])
|
|
20
|
+
const [isLoading, setISLoading] = useState(false)
|
|
21
|
+
const [length, setLength] = useState(0)
|
|
22
|
+
|
|
23
|
+
// Upload files
|
|
24
|
+
const uploadFiles = async (fileList) => {
|
|
25
|
+
if (onUploadStart) {
|
|
26
|
+
onUploadStart(fileList)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setISLoading(true)
|
|
30
|
+
setLength(fileList.length)
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Use the API service to upload files
|
|
34
|
+
// The apiService.uploadMedia handles:
|
|
35
|
+
// 1. Generate S3 signed URLs via tradly.app.generateS3ImageURL
|
|
36
|
+
// 2. Upload files to S3
|
|
37
|
+
// 3. Save media metadata to API
|
|
38
|
+
const uploadedUrls = await apiService.uploadMedia(fileList, apiService.authKey)
|
|
39
|
+
|
|
40
|
+
if (onUploadComplete) {
|
|
41
|
+
onUploadComplete(uploadedUrls)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setLength(0)
|
|
45
|
+
if (loadMedia) {
|
|
46
|
+
loadMedia()
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Upload error:', error)
|
|
50
|
+
if (onUploadError) {
|
|
51
|
+
onUploadError(error)
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
setISLoading(false)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Default classes with customization support
|
|
59
|
+
const defaultContainerClass = 'min-w-40 h-40'
|
|
60
|
+
const defaultButtonClass =
|
|
61
|
+
'w-full h-full flex flex-col justify-center items-center text-sm border border-primary border-dashed rounded-lg hover:bg-gray-50 transition-colors'
|
|
62
|
+
const defaultIconContainerClass = 'p-[10px] bg-primary rounded-full'
|
|
63
|
+
const defaultTitleClass = 'mt-2'
|
|
64
|
+
const defaultLoadingClass =
|
|
65
|
+
'flex items-center justify-center h-40 bg-gray-200 rounded-lg shadow-md animate-pulse'
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<div className={className || defaultContainerClass}>
|
|
70
|
+
<input
|
|
71
|
+
required
|
|
72
|
+
id={`media_select_${files?.length}`}
|
|
73
|
+
type="file"
|
|
74
|
+
className="hidden"
|
|
75
|
+
accept={accept}
|
|
76
|
+
placeholder=""
|
|
77
|
+
onChange={async (e) => {
|
|
78
|
+
e.stopPropagation()
|
|
79
|
+
const all_files = Array.from(e.target.files)
|
|
80
|
+
if (all_files?.length > 0) {
|
|
81
|
+
await uploadFiles(all_files)
|
|
82
|
+
}
|
|
83
|
+
// Reset input
|
|
84
|
+
e.target.value = ''
|
|
85
|
+
}}
|
|
86
|
+
multiple={true}
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className={buttonClassName || defaultButtonClass}
|
|
92
|
+
onClick={() => document.getElementById(`media_select_${files?.length}`).click()}
|
|
93
|
+
disabled={isLoading}
|
|
94
|
+
>
|
|
95
|
+
<span className={iconContainerClassName || defaultIconContainerClass}>
|
|
96
|
+
<CameraIcon />
|
|
97
|
+
</span>
|
|
98
|
+
<span className={titleClassName || defaultTitleClass}>{title}</span>
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{isLoading &&
|
|
103
|
+
Array.from({ length: length }).map((_, index) => {
|
|
104
|
+
return (
|
|
105
|
+
<div key={index} className={loadingClassName || defaultLoadingClass}>
|
|
106
|
+
File Uploading... {index + 1}/{length}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
})}
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default FileUpload
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export const CloseIcon = ({ className = 'w-8 h-8' }) => (
|
|
4
|
+
<svg
|
|
5
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
6
|
+
fill="none"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
strokeWidth="1.5"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
className={className}
|
|
11
|
+
>
|
|
12
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
13
|
+
</svg>
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
export const CameraIcon = () => (
|
|
17
|
+
<svg width="22" height="18" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
18
|
+
<path
|
|
19
|
+
d="M7.75033 0.166672L5.76783 2.33334H2.33366C1.14199 2.33334 0.166992 3.30834 0.166992 4.50001V17.5C0.166992 18.6917 1.14199 19.6667 2.33366 19.6667H19.667C20.8587 19.6667 21.8337 18.6917 21.8337 17.5V4.50001C21.8337 3.30834 20.8587 2.33334 19.667 2.33334H16.2328L14.2503 0.166672H7.75033ZM11.0003 16.4167C8.01033 16.4167 5.58366 13.99 5.58366 11C5.58366 8.01001 8.01033 5.58334 11.0003 5.58334C13.9903 5.58334 16.417 8.01001 16.417 11C16.417 13.99 13.9903 16.4167 11.0003 16.4167Z"
|
|
20
|
+
fill="white"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const ImagesSkeleton = ({ per_page = 30 }) => {
|
|
4
|
+
const gridItems = []
|
|
5
|
+
|
|
6
|
+
for (let i = 0; i < per_page; i++) {
|
|
7
|
+
gridItems.push(
|
|
8
|
+
<div
|
|
9
|
+
key={i}
|
|
10
|
+
className="w-full min-h-[160px] bg-[#3B3269] bg-opacity-[20%] rounded-card animate-pulse"
|
|
11
|
+
></div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return gridItems
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default ImagesSkeleton
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import FileUpload from './FileUpload'
|
|
3
|
+
import ImagesSkeleton from './ImagesSkeleton'
|
|
4
|
+
import Pagination from './Pagination'
|
|
5
|
+
|
|
6
|
+
const IMAGE_MIME_TYPES = [
|
|
7
|
+
'image/png',
|
|
8
|
+
'image/jpeg',
|
|
9
|
+
'image/webp',
|
|
10
|
+
'image/svg+xml',
|
|
11
|
+
'image/gif',
|
|
12
|
+
'image/avif',
|
|
13
|
+
'image/x-icon',
|
|
14
|
+
'image/vnd.microsoft.icon',
|
|
15
|
+
'image/heic',
|
|
16
|
+
'image/heif',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const ImagesGallery = ({
|
|
20
|
+
update_data,
|
|
21
|
+
closePopup,
|
|
22
|
+
apiService,
|
|
23
|
+
onError,
|
|
24
|
+
// Styling props
|
|
25
|
+
className,
|
|
26
|
+
gridClassName,
|
|
27
|
+
imageItemClassName,
|
|
28
|
+
paginationContainerClassName,
|
|
29
|
+
}) => {
|
|
30
|
+
const [images, setImages] = useState([])
|
|
31
|
+
const [total_count, setTotalCount] = useState(0)
|
|
32
|
+
const [currentPage, setCurrentPage] = useState(1)
|
|
33
|
+
const [isLoading, setISLoading] = useState(false)
|
|
34
|
+
|
|
35
|
+
// Fetch images
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
loadMedia()
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const loadMedia = async (page_number = 1) => {
|
|
41
|
+
try {
|
|
42
|
+
const response = await apiService.fetchMedia({
|
|
43
|
+
mimeTypes: IMAGE_MIME_TYPES,
|
|
44
|
+
page: page_number,
|
|
45
|
+
setISLoading,
|
|
46
|
+
isLoading,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Handle different response formats
|
|
50
|
+
// Response might be: { media: [...], count: ... } or { data: { media: [...], count: ... } } or directly [...]
|
|
51
|
+
const mediaData = response?.media || response?.data?.media || response?.data || response || []
|
|
52
|
+
const count = response?.count || response?.data?.count || response?.total || 0
|
|
53
|
+
|
|
54
|
+
setImages(Array.isArray(mediaData) ? mediaData : [])
|
|
55
|
+
setTotalCount(count)
|
|
56
|
+
setCurrentPage(page_number)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Error loading media:', error)
|
|
59
|
+
if (onError) {
|
|
60
|
+
onError(error)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleImageClick = (image) => {
|
|
66
|
+
if (update_data) {
|
|
67
|
+
update_data(image.url || image)
|
|
68
|
+
}
|
|
69
|
+
if (closePopup) {
|
|
70
|
+
closePopup()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Default classes with customization support
|
|
75
|
+
const defaultContainerClass = 'h-full flex flex-col justify-between'
|
|
76
|
+
const defaultGridClass = 'grid grid-cols-[repeat(auto-fill,minmax(calc(160px),1fr))] gap-5'
|
|
77
|
+
const defaultImageItemClass =
|
|
78
|
+
'cursor-pointer w-full h-40 object-contain p-3 overflow-hidden bg-white rounded-md shadow-md hover:shadow-lg transition-shadow'
|
|
79
|
+
const defaultPaginationContainerClass = 'mb-4 bg-gray-100/90 p-4 sticky bottom-0'
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={className || defaultContainerClass}>
|
|
83
|
+
<div className={gridClassName || defaultGridClass}>
|
|
84
|
+
<FileUpload
|
|
85
|
+
loadMedia={loadMedia}
|
|
86
|
+
accept="image/*"
|
|
87
|
+
title="Add Image"
|
|
88
|
+
apiService={apiService}
|
|
89
|
+
onUploadError={onError}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
{isLoading ? (
|
|
93
|
+
<ImagesSkeleton />
|
|
94
|
+
) : (
|
|
95
|
+
images?.map((image, index) => {
|
|
96
|
+
const imageUrl = typeof image === 'string' ? image : image.url
|
|
97
|
+
const imageKey = image.id || image.url || index
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<img
|
|
101
|
+
key={imageKey}
|
|
102
|
+
onClick={() => handleImageClick(image)}
|
|
103
|
+
className={imageItemClassName || defaultImageItemClass}
|
|
104
|
+
src={imageUrl}
|
|
105
|
+
alt={image.name || 'Media item'}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className={paginationContainerClassName || defaultPaginationContainerClass}>
|
|
113
|
+
{total_count > 0 && (
|
|
114
|
+
<Pagination
|
|
115
|
+
nextPage={(value) => loadMedia(value)}
|
|
116
|
+
pageCount={Math.ceil(total_count / 30)}
|
|
117
|
+
current_page={currentPage}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default ImagesGallery
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import MediaTab from './MediaTab'
|
|
4
|
+
import { CloseIcon } from './Icons'
|
|
5
|
+
|
|
6
|
+
const MediaPopup = ({
|
|
7
|
+
isOpen,
|
|
8
|
+
onClose,
|
|
9
|
+
onSelect,
|
|
10
|
+
currentData,
|
|
11
|
+
options = ['image'],
|
|
12
|
+
apiService,
|
|
13
|
+
onError,
|
|
14
|
+
title = 'Media Gallery',
|
|
15
|
+
// Styling props
|
|
16
|
+
className,
|
|
17
|
+
overlayClassName,
|
|
18
|
+
containerClassName,
|
|
19
|
+
headerClassName,
|
|
20
|
+
titleClassName,
|
|
21
|
+
closeButtonClassName,
|
|
22
|
+
// Pass-through styling props for child components
|
|
23
|
+
tabListClassName,
|
|
24
|
+
tabButtonClassName,
|
|
25
|
+
tabButtonActiveClassName,
|
|
26
|
+
tabButtonInactiveClassName,
|
|
27
|
+
tabPanelClassName,
|
|
28
|
+
gridClassName,
|
|
29
|
+
imageItemClassName,
|
|
30
|
+
videoItemClassName,
|
|
31
|
+
paginationContainerClassName,
|
|
32
|
+
}) => {
|
|
33
|
+
if (!isOpen) return null
|
|
34
|
+
|
|
35
|
+
const handleUpdate = (data) => {
|
|
36
|
+
if (onSelect) {
|
|
37
|
+
onSelect(data)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleClose = () => {
|
|
42
|
+
if (onClose) {
|
|
43
|
+
onClose()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Default classes with customization support
|
|
48
|
+
const defaultOverlayClass = 'fixed inset-0 w-screen h-screen bg-black opacity-10'
|
|
49
|
+
const defaultContainerClass =
|
|
50
|
+
'origin-top-right z-[9999] absolute inset-0 max-w-3xl mx-auto my-auto min-h-[200px] h-[600px] bg-white rounded shadow-inner cursor-auto p-6'
|
|
51
|
+
const defaultHeaderClass = 'flex items-center justify-between gap-4'
|
|
52
|
+
const defaultTitleClass = 'text-[#000] font-bold text-2xl'
|
|
53
|
+
const defaultCloseButtonClass =
|
|
54
|
+
'bg-transparent rounded-full hover:bg-gray-100 p-1 transition-colors'
|
|
55
|
+
|
|
56
|
+
return createPortal(
|
|
57
|
+
<>
|
|
58
|
+
<div
|
|
59
|
+
className={overlayClassName || defaultOverlayClass}
|
|
60
|
+
style={{ zIndex: 9998 }}
|
|
61
|
+
onClick={handleClose}
|
|
62
|
+
/>
|
|
63
|
+
<div className={containerClassName || defaultContainerClass} style={{ zIndex: 9999 }}>
|
|
64
|
+
{/* Header */}
|
|
65
|
+
<div className={headerClassName || defaultHeaderClass}>
|
|
66
|
+
<p className={titleClassName || defaultTitleClass}>{title}</p>
|
|
67
|
+
<button
|
|
68
|
+
className={closeButtonClassName || defaultCloseButtonClass}
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={handleClose}
|
|
71
|
+
aria-label="Close"
|
|
72
|
+
>
|
|
73
|
+
<CloseIcon />
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<MediaTab
|
|
77
|
+
imagePopup={isOpen}
|
|
78
|
+
update_data={handleUpdate}
|
|
79
|
+
current_data={currentData}
|
|
80
|
+
closePopup={handleClose}
|
|
81
|
+
options={options}
|
|
82
|
+
apiService={apiService}
|
|
83
|
+
onError={onError}
|
|
84
|
+
// Pass styling props through
|
|
85
|
+
tabListClassName={tabListClassName}
|
|
86
|
+
tabButtonClassName={tabButtonClassName}
|
|
87
|
+
tabButtonActiveClassName={tabButtonActiveClassName}
|
|
88
|
+
tabButtonInactiveClassName={tabButtonInactiveClassName}
|
|
89
|
+
tabPanelClassName={tabPanelClassName}
|
|
90
|
+
gridClassName={gridClassName}
|
|
91
|
+
imageItemClassName={imageItemClassName}
|
|
92
|
+
videoItemClassName={videoItemClassName}
|
|
93
|
+
paginationContainerClassName={paginationContainerClassName}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</>,
|
|
97
|
+
document.body
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default MediaPopup
|