@tradly/asset 1.0.3 → 1.0.5
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/dist/components/FileUpload.js +163 -0
- package/dist/components/Icons.js +39 -0
- package/dist/components/ImagesSkeleton.js +21 -0
- package/dist/components/MediaGallery.js +153 -0
- package/dist/components/MediaPopup.js +104 -0
- package/dist/components/MediaTab.js +141 -0
- package/dist/components/Pagination.js +141 -0
- package/dist/components/VideosGallery.js +153 -0
- package/dist/esm/components/FileUpload.js +155 -0
- package/dist/esm/components/Icons.js +32 -0
- package/dist/esm/components/ImagesSkeleton.js +14 -0
- package/dist/esm/components/MediaGallery.js +144 -0
- package/dist/esm/components/MediaPopup.js +97 -0
- package/dist/esm/components/MediaTab.js +132 -0
- package/dist/esm/components/Pagination.js +134 -0
- package/dist/esm/components/VideosGallery.js +144 -0
- package/{index.js → dist/esm/index.js} +6 -6
- package/dist/esm/services/apiService.js +373 -0
- package/dist/index.js +60 -0
- package/dist/services/apiService.js +379 -0
- package/package.json +17 -6
- package/EXAMPLE_USAGE.jsx +0 -128
- package/MIGRATION_GUIDE.md +0 -249
- package/STYLING_GUIDE.md +0 -219
- package/src/components/FileUpload.jsx +0 -114
- package/src/components/Icons.jsx +0 -23
- package/src/components/ImagesSkeleton.jsx +0 -18
- package/src/components/MediaGallery.jsx +0 -125
- package/src/components/MediaPopup.jsx +0 -101
- package/src/components/MediaTab.jsx +0 -152
- package/src/components/Pagination.jsx +0 -175
- package/src/components/VideosGallery.jsx +0 -121
- package/src/services/apiService.js +0 -270
|
@@ -1,125 +0,0 @@
|
|
|
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
|
|
@@ -1,101 +0,0 @@
|
|
|
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
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { Tab } from '@headlessui/react'
|
|
3
|
-
import ImagesGallery from './MediaGallery'
|
|
4
|
-
import VideosGallery from './VideosGallery'
|
|
5
|
-
|
|
6
|
-
function classNames(...classes) {
|
|
7
|
-
return classes.filter(Boolean).join(' ')
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const MediaTab = ({
|
|
11
|
-
imagePopup,
|
|
12
|
-
current_data,
|
|
13
|
-
update_data,
|
|
14
|
-
closePopup,
|
|
15
|
-
options,
|
|
16
|
-
apiService,
|
|
17
|
-
onError,
|
|
18
|
-
// Styling props
|
|
19
|
-
className,
|
|
20
|
-
tabListClassName,
|
|
21
|
-
tabButtonClassName,
|
|
22
|
-
tabButtonActiveClassName,
|
|
23
|
-
tabButtonInactiveClassName,
|
|
24
|
-
tabPanelClassName,
|
|
25
|
-
// Pass-through styling props for gallery components
|
|
26
|
-
gridClassName,
|
|
27
|
-
imageItemClassName,
|
|
28
|
-
videoItemClassName,
|
|
29
|
-
paginationContainerClassName,
|
|
30
|
-
}) => {
|
|
31
|
-
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
32
|
-
|
|
33
|
-
// Default classes with customization support
|
|
34
|
-
const defaultTabGroupClass = 'h-[520px] overflow-y-auto scrollbar-none'
|
|
35
|
-
const defaultTabListClass = 'sticky top-0 z-30 bg-white w-full flex border-b-2 border-gray-200'
|
|
36
|
-
const defaultTabButtonBaseClass =
|
|
37
|
-
'rounded-lg py-2.5 px-8 text-base font-medium leading-5 flex flex-col md:flex-row items-center justify-center gap-3 ring-0 focus:outline-none'
|
|
38
|
-
const defaultTabButtonActiveClass =
|
|
39
|
-
'relative text-primary after:content-[""] after:h-1 after:w-full after:block after:absolute after:-bottom-0.5 after:bg-primary after:rounded-card'
|
|
40
|
-
const defaultTabButtonInactiveClass = 'text-[#4F4F4F]'
|
|
41
|
-
const defaultTabPanelClass = 'h-full py-4 relative'
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<Tab.Group
|
|
45
|
-
as="div"
|
|
46
|
-
key={selectedIndex}
|
|
47
|
-
className={className || defaultTabGroupClass}
|
|
48
|
-
onChange={setSelectedIndex}
|
|
49
|
-
selectedIndex={selectedIndex}
|
|
50
|
-
>
|
|
51
|
-
<Tab.List className={tabListClassName || defaultTabListClass}>
|
|
52
|
-
{options.includes('image') && (
|
|
53
|
-
<Tab
|
|
54
|
-
as="p"
|
|
55
|
-
id="image-tab-button"
|
|
56
|
-
className={({ selected }) =>
|
|
57
|
-
classNames(
|
|
58
|
-
tabButtonClassName || defaultTabButtonBaseClass,
|
|
59
|
-
selected
|
|
60
|
-
? tabButtonActiveClassName || defaultTabButtonActiveClass
|
|
61
|
-
: tabButtonInactiveClassName || defaultTabButtonInactiveClass
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
>
|
|
65
|
-
<span>Images</span>
|
|
66
|
-
</Tab>
|
|
67
|
-
)}
|
|
68
|
-
{options.includes('video') && (
|
|
69
|
-
<Tab
|
|
70
|
-
as="p"
|
|
71
|
-
id="video-tab-button"
|
|
72
|
-
className={({ selected }) =>
|
|
73
|
-
classNames(
|
|
74
|
-
tabButtonClassName || defaultTabButtonBaseClass,
|
|
75
|
-
selected
|
|
76
|
-
? tabButtonActiveClassName || defaultTabButtonActiveClass
|
|
77
|
-
: tabButtonInactiveClassName || defaultTabButtonInactiveClass
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
>
|
|
81
|
-
<span>Videos</span>
|
|
82
|
-
</Tab>
|
|
83
|
-
)}
|
|
84
|
-
{options.includes('file') && (
|
|
85
|
-
<Tab
|
|
86
|
-
as="p"
|
|
87
|
-
id="file-tab-button"
|
|
88
|
-
className={({ selected }) =>
|
|
89
|
-
classNames(
|
|
90
|
-
tabButtonClassName || defaultTabButtonBaseClass,
|
|
91
|
-
selected
|
|
92
|
-
? tabButtonActiveClassName || defaultTabButtonActiveClass
|
|
93
|
-
: tabButtonInactiveClassName || defaultTabButtonInactiveClass
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
>
|
|
97
|
-
<span>Files</span>
|
|
98
|
-
</Tab>
|
|
99
|
-
)}
|
|
100
|
-
</Tab.List>
|
|
101
|
-
<Tab.Panels className="h-full" selectedIndex={selectedIndex}>
|
|
102
|
-
{options.includes('image') && (
|
|
103
|
-
<Tab.Panel className={tabPanelClassName || defaultTabPanelClass}>
|
|
104
|
-
<ImagesGallery
|
|
105
|
-
imagePopup={imagePopup}
|
|
106
|
-
update_data={update_data}
|
|
107
|
-
current_data={current_data}
|
|
108
|
-
closePopup={closePopup}
|
|
109
|
-
apiService={apiService}
|
|
110
|
-
onError={onError}
|
|
111
|
-
gridClassName={gridClassName}
|
|
112
|
-
imageItemClassName={imageItemClassName}
|
|
113
|
-
paginationContainerClassName={paginationContainerClassName}
|
|
114
|
-
/>
|
|
115
|
-
</Tab.Panel>
|
|
116
|
-
)}
|
|
117
|
-
{options.includes('video') && (
|
|
118
|
-
<Tab.Panel className={tabPanelClassName || defaultTabPanelClass}>
|
|
119
|
-
<VideosGallery
|
|
120
|
-
imagePopup={imagePopup}
|
|
121
|
-
update_data={update_data}
|
|
122
|
-
current_data={current_data}
|
|
123
|
-
closePopup={closePopup}
|
|
124
|
-
apiService={apiService}
|
|
125
|
-
onError={onError}
|
|
126
|
-
gridClassName={gridClassName}
|
|
127
|
-
videoItemClassName={videoItemClassName}
|
|
128
|
-
paginationContainerClassName={paginationContainerClassName}
|
|
129
|
-
/>
|
|
130
|
-
</Tab.Panel>
|
|
131
|
-
)}
|
|
132
|
-
{options.includes('file') && (
|
|
133
|
-
<Tab.Panel className={tabPanelClassName || defaultTabPanelClass}>
|
|
134
|
-
<ImagesGallery
|
|
135
|
-
imagePopup={imagePopup}
|
|
136
|
-
update_data={update_data}
|
|
137
|
-
current_data={current_data}
|
|
138
|
-
closePopup={closePopup}
|
|
139
|
-
apiService={apiService}
|
|
140
|
-
onError={onError}
|
|
141
|
-
gridClassName={gridClassName}
|
|
142
|
-
imageItemClassName={imageItemClassName}
|
|
143
|
-
paginationContainerClassName={paginationContainerClassName}
|
|
144
|
-
/>
|
|
145
|
-
</Tab.Panel>
|
|
146
|
-
)}
|
|
147
|
-
</Tab.Panels>
|
|
148
|
-
</Tab.Group>
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export default MediaTab
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
const Pagination = ({
|
|
4
|
-
className,
|
|
5
|
-
nextPage,
|
|
6
|
-
current_page,
|
|
7
|
-
pageCount,
|
|
8
|
-
// Styling props
|
|
9
|
-
navClassName,
|
|
10
|
-
previousButtonClassName,
|
|
11
|
-
nextButtonClassName,
|
|
12
|
-
pageButtonClassName,
|
|
13
|
-
pageButtonActiveClassName,
|
|
14
|
-
ellipsisClassName,
|
|
15
|
-
}) => {
|
|
16
|
-
const totalPages = Math.ceil(pageCount) || 1
|
|
17
|
-
const currentPage = current_page || 1
|
|
18
|
-
|
|
19
|
-
// Generate page numbers to display
|
|
20
|
-
const getPageNumbers = () => {
|
|
21
|
-
const pages = []
|
|
22
|
-
const maxVisible = 5
|
|
23
|
-
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2))
|
|
24
|
-
let endPage = Math.min(totalPages, startPage + maxVisible - 1)
|
|
25
|
-
|
|
26
|
-
if (endPage - startPage < maxVisible - 1) {
|
|
27
|
-
startPage = Math.max(1, endPage - maxVisible + 1)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Add first page and ellipsis
|
|
31
|
-
if (startPage > 1) {
|
|
32
|
-
pages.push(1)
|
|
33
|
-
if (startPage > 2) {
|
|
34
|
-
pages.push('...')
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Add visible pages
|
|
39
|
-
for (let i = startPage; i <= endPage; i++) {
|
|
40
|
-
pages.push(i)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Add ellipsis and last page
|
|
44
|
-
if (endPage < totalPages) {
|
|
45
|
-
if (endPage < totalPages - 1) {
|
|
46
|
-
pages.push('...')
|
|
47
|
-
}
|
|
48
|
-
pages.push(totalPages)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return pages
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const handlePageClick = (page) => {
|
|
55
|
-
if (page !== currentPage && page >= 1 && page <= totalPages) {
|
|
56
|
-
nextPage(page)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const handlePrevious = () => {
|
|
61
|
-
if (currentPage > 1) {
|
|
62
|
-
nextPage(currentPage - 1)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const handleNext = () => {
|
|
67
|
-
if (currentPage < totalPages) {
|
|
68
|
-
nextPage(currentPage + 1)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (totalPages <= 1) return null
|
|
73
|
-
|
|
74
|
-
const pageNumbers = getPageNumbers()
|
|
75
|
-
|
|
76
|
-
// Default classes with customization support
|
|
77
|
-
const defaultContainerClass = 'flex justify-center'
|
|
78
|
-
const defaultNavClass =
|
|
79
|
-
'relative z-0 inline-flex flex-wrap justify-center rounded-md shadow-sm -space-x-px'
|
|
80
|
-
const defaultPreviousButtonClass =
|
|
81
|
-
'relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50'
|
|
82
|
-
const defaultNextButtonClass =
|
|
83
|
-
'relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50'
|
|
84
|
-
const defaultPageButtonClass =
|
|
85
|
-
'relative inline-flex items-center px-4 py-2 border bg-white border-gray-300 text-gray-500 hover:bg-gray-50 text-sm font-medium'
|
|
86
|
-
const defaultPageButtonActiveClass =
|
|
87
|
-
'z-10 bg-primary border-primary text-white relative inline-flex items-center px-4 py-2 border text-md font-semibold'
|
|
88
|
-
const defaultEllipsisClass =
|
|
89
|
-
'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700'
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<div className={className || defaultContainerClass}>
|
|
93
|
-
<nav className={navClassName || defaultNavClass}>
|
|
94
|
-
{/* Previous Button */}
|
|
95
|
-
<button
|
|
96
|
-
onClick={handlePrevious}
|
|
97
|
-
disabled={currentPage === 1}
|
|
98
|
-
className={`${previousButtonClassName || defaultPreviousButtonClass} ${
|
|
99
|
-
currentPage === 1 ? 'opacity-50 cursor-not-allowed' : ''
|
|
100
|
-
}`}
|
|
101
|
-
aria-label="Previous page"
|
|
102
|
-
>
|
|
103
|
-
<svg
|
|
104
|
-
className="h-5 w-5"
|
|
105
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
106
|
-
viewBox="0 0 20 20"
|
|
107
|
-
fill="currentColor"
|
|
108
|
-
aria-hidden="true"
|
|
109
|
-
>
|
|
110
|
-
<path
|
|
111
|
-
fillRule="evenodd"
|
|
112
|
-
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
|
113
|
-
clipRule="evenodd"
|
|
114
|
-
/>
|
|
115
|
-
</svg>
|
|
116
|
-
</button>
|
|
117
|
-
|
|
118
|
-
{/* Page Numbers */}
|
|
119
|
-
{pageNumbers.map((page, index) => {
|
|
120
|
-
if (page === '...') {
|
|
121
|
-
return (
|
|
122
|
-
<span key={`ellipsis-${index}`} className={ellipsisClassName || defaultEllipsisClass}>
|
|
123
|
-
...
|
|
124
|
-
</span>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const isActive = page === currentPage
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<button
|
|
132
|
-
key={page}
|
|
133
|
-
onClick={() => handlePageClick(page)}
|
|
134
|
-
className={
|
|
135
|
-
isActive
|
|
136
|
-
? pageButtonActiveClassName || defaultPageButtonActiveClass
|
|
137
|
-
: pageButtonClassName || defaultPageButtonClass
|
|
138
|
-
}
|
|
139
|
-
aria-label={`Page ${page}`}
|
|
140
|
-
aria-current={isActive ? 'page' : undefined}
|
|
141
|
-
>
|
|
142
|
-
{page}
|
|
143
|
-
</button>
|
|
144
|
-
)
|
|
145
|
-
})}
|
|
146
|
-
|
|
147
|
-
{/* Next Button */}
|
|
148
|
-
<button
|
|
149
|
-
onClick={handleNext}
|
|
150
|
-
disabled={currentPage === totalPages}
|
|
151
|
-
className={`${nextButtonClassName || defaultNextButtonClass} ${
|
|
152
|
-
currentPage === totalPages ? 'opacity-50 cursor-not-allowed' : ''
|
|
153
|
-
}`}
|
|
154
|
-
aria-label="Next page"
|
|
155
|
-
>
|
|
156
|
-
<svg
|
|
157
|
-
className="h-5 w-5"
|
|
158
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
159
|
-
viewBox="0 0 20 20"
|
|
160
|
-
fill="currentColor"
|
|
161
|
-
aria-hidden="true"
|
|
162
|
-
>
|
|
163
|
-
<path
|
|
164
|
-
fillRule="evenodd"
|
|
165
|
-
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
166
|
-
clipRule="evenodd"
|
|
167
|
-
/>
|
|
168
|
-
</svg>
|
|
169
|
-
</button>
|
|
170
|
-
</nav>
|
|
171
|
-
</div>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export default Pagination
|
|
@@ -1,121 +0,0 @@
|
|
|
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 VIDEO_MIME_TYPES = [
|
|
7
|
-
'video/mp4',
|
|
8
|
-
'video/quicktime',
|
|
9
|
-
'video/x-ms-wmv',
|
|
10
|
-
'video/h265',
|
|
11
|
-
'video/hevc',
|
|
12
|
-
'video/webm',
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
const VideosGallery = ({
|
|
16
|
-
update_data,
|
|
17
|
-
closePopup,
|
|
18
|
-
apiService,
|
|
19
|
-
onError,
|
|
20
|
-
// Styling props
|
|
21
|
-
className,
|
|
22
|
-
gridClassName,
|
|
23
|
-
videoItemClassName,
|
|
24
|
-
paginationContainerClassName,
|
|
25
|
-
}) => {
|
|
26
|
-
const [videos, setVideos] = useState([])
|
|
27
|
-
const [total_count, setTotalCount] = useState(0)
|
|
28
|
-
const [currentPage, setCurrentPage] = useState(1)
|
|
29
|
-
const [isLoading, setISLoading] = useState(false)
|
|
30
|
-
|
|
31
|
-
// Fetch videos
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
loadMedia()
|
|
34
|
-
}, [])
|
|
35
|
-
|
|
36
|
-
const loadMedia = async (page_number = 1) => {
|
|
37
|
-
try {
|
|
38
|
-
const response = await apiService.fetchMedia({
|
|
39
|
-
mimeTypes: VIDEO_MIME_TYPES,
|
|
40
|
-
page: page_number,
|
|
41
|
-
setISLoading,
|
|
42
|
-
isLoading,
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// Handle different response formats
|
|
46
|
-
// Response might be: { media: [...], count: ... } or { data: { media: [...], count: ... } } or directly [...]
|
|
47
|
-
const mediaData = response?.media || response?.data?.media || response?.data || response || []
|
|
48
|
-
const count = response?.count || response?.data?.count || response?.total || 0
|
|
49
|
-
|
|
50
|
-
setVideos(Array.isArray(mediaData) ? mediaData : [])
|
|
51
|
-
setTotalCount(count)
|
|
52
|
-
setCurrentPage(page_number)
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error('Error loading videos:', error)
|
|
55
|
-
if (onError) {
|
|
56
|
-
onError(error)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const handleVideoClick = (video) => {
|
|
62
|
-
if (update_data) {
|
|
63
|
-
update_data(video.url || video)
|
|
64
|
-
}
|
|
65
|
-
if (closePopup) {
|
|
66
|
-
closePopup()
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Default classes with customization support
|
|
71
|
-
const defaultContainerClass = 'h-full flex flex-col justify-between'
|
|
72
|
-
const defaultGridClass = 'grid grid-cols-[repeat(auto-fill,minmax(calc(180px),1fr))] gap-5'
|
|
73
|
-
const defaultVideoItemClass =
|
|
74
|
-
'cursor-pointer w-full h-40 object-contain overflow-hidden bg-white rounded-md shadow-md hover:shadow-lg transition-shadow'
|
|
75
|
-
const defaultPaginationContainerClass = 'mb-4 bg-gray-100/90 p-4 sticky bottom-0'
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className={className || defaultContainerClass}>
|
|
79
|
-
<div className={gridClassName || defaultGridClass}>
|
|
80
|
-
<FileUpload
|
|
81
|
-
loadMedia={loadMedia}
|
|
82
|
-
accept="video/*"
|
|
83
|
-
title="Add Video"
|
|
84
|
-
apiService={apiService}
|
|
85
|
-
onUploadError={onError}
|
|
86
|
-
/>
|
|
87
|
-
|
|
88
|
-
{isLoading ? (
|
|
89
|
-
<ImagesSkeleton />
|
|
90
|
-
) : (
|
|
91
|
-
videos?.map((video, index) => {
|
|
92
|
-
const videoUrl = typeof video === 'string' ? video : video.url
|
|
93
|
-
const videoKey = video.id || video.url || index
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<video
|
|
97
|
-
key={videoKey}
|
|
98
|
-
onClick={() => handleVideoClick(video)}
|
|
99
|
-
className={videoItemClassName || defaultVideoItemClass}
|
|
100
|
-
controls
|
|
101
|
-
src={videoUrl}
|
|
102
|
-
/>
|
|
103
|
-
)
|
|
104
|
-
})
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div className={paginationContainerClassName || defaultPaginationContainerClass}>
|
|
109
|
-
{total_count > 0 && (
|
|
110
|
-
<Pagination
|
|
111
|
-
nextPage={(value) => loadMedia(value)}
|
|
112
|
-
pageCount={Math.ceil(total_count / 30)}
|
|
113
|
-
current_page={currentPage}
|
|
114
|
-
/>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export default VideosGallery
|