@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,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