@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.
@@ -0,0 +1,152 @@
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
@@ -0,0 +1,175 @@
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
@@ -0,0 +1,121 @@
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
package/src/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @tradly/media-gallery
3
+ *
4
+ * A reusable media gallery component for uploading and selecting
5
+ * images, videos, and files with Tradly authentication
6
+ */
7
+
8
+ import MediaPopup from './components/MediaPopup'
9
+ import MediaTab from './components/MediaTab'
10
+ import ImagesGallery from './components/MediaGallery'
11
+ import VideosGallery from './components/VideosGallery'
12
+ import FileUpload from './components/FileUpload'
13
+ import MediaApiService from './services/apiService'
14
+
15
+ // Export main component
16
+ export { MediaPopup }
17
+
18
+ // Export sub-components for advanced usage
19
+ export { MediaTab, ImagesGallery, VideosGallery, FileUpload }
20
+
21
+ // Export API service
22
+ export { MediaApiService }
23
+
24
+ // Export default as MediaPopup for convenience
25
+ export default MediaPopup