@yogiswara/honcho-editor-ui 1.3.7 → 1.3.9

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,36 @@
1
+ import { Controller } from "./editor/useHonchoEditor";
2
+ import { Gallery } from "./editor/type";
3
+ /**
4
+ * Return type for the useGallerySwipe hook
5
+ * Provides image navigation functionality with pagination support
6
+ */
7
+ interface UseGallerySwipeReturn {
8
+ /** Current image data object containing all image information */
9
+ currentImageData: Gallery | null;
10
+ /** Whether next image navigation is available (considers pagination) */
11
+ isNextAvailable: boolean;
12
+ /** Whether previous image navigation is available */
13
+ isPrevAvailable: boolean;
14
+ /** Function to navigate to the next image (async due to potential API calls) */
15
+ onSwipeNext: () => Promise<void>;
16
+ /** Function to navigate to the previous image (async due to potential API calls) */
17
+ onSwipePrev: () => Promise<void>;
18
+ /** Loading state during image transitions and API calls */
19
+ isLoading: boolean;
20
+ /** Error message if any operation fails */
21
+ error: string | null;
22
+ }
23
+ /**
24
+ * Custom hook for handling image gallery navigation with automatic pagination
25
+ *
26
+ * This hook manages image swipe/navigation functionality across a paginated gallery.
27
+ * It handles the complexity of finding images across multiple pages and loading
28
+ * additional pages as needed during navigation.
29
+ *
30
+ * @param firebaseUid - User's Firebase UID (can be null during initialization)
31
+ * @param initImageId - Initial image ID to start with (can be null during initialization)
32
+ * @param controller - Controller instance for API calls (can be null during initialization)
33
+ * @returns Object containing current image data and navigation functions
34
+ */
35
+ export declare function useGallerySwipe(firebaseUid: string | null, initImageId: string | null, controller: Controller | null): UseGallerySwipeReturn;
36
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Example usage of the useGallerySwipe hook
3
+ * This shows various integration patterns and use cases
4
+ */
5
+ /**
6
+ * Basic example: Simple image gallery with navigation buttons
7
+ */
8
+ export declare function BasicGalleryExample(): import("react/jsx-runtime").JSX.Element;
9
+ /**
10
+ * Advanced example: Gallery with keyboard navigation and swipe gestures
11
+ */
12
+ export declare function AdvancedGalleryExample(): import("react/jsx-runtime").JSX.Element;
13
+ /**
14
+ * Integration example: Using with existing useHonchoEditor
15
+ */
16
+ export declare function EditorIntegrationExample(): import("react/jsx-runtime").JSX.Element;
17
+ /**
18
+ * Mobile-optimized example with touch gestures
19
+ */
20
+ export declare function MobileGalleryExample(): import("react/jsx-runtime").JSX.Element;
21
+ /**
22
+ * Error handling and loading states example
23
+ */
24
+ export declare function ErrorHandlingExample(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,184 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useGallerySwipe } from './useGallerySwipe';
3
+ import { useState, useEffect } from 'react';
4
+ /**
5
+ * Example usage of the useGallerySwipe hook
6
+ * This shows various integration patterns and use cases
7
+ */
8
+ /**
9
+ * Basic example: Simple image gallery with navigation buttons
10
+ */
11
+ export function BasicGalleryExample() {
12
+ const [firebaseUid, setFirebaseUid] = useState('user123');
13
+ const [currentImageId, setCurrentImageId] = useState('image_001');
14
+ const [controller, setController] = useState(null);
15
+ // Initialize controller (replace with your actual controller initialization)
16
+ useEffect(() => {
17
+ // Your controller initialization logic here
18
+ const initController = async () => {
19
+ // const controllerInstance = new YourController();
20
+ // setController(controllerInstance);
21
+ };
22
+ initController();
23
+ }, []);
24
+ const { currentImageData, isNextAvailable, isPrevAvailable, onSwipeNext, onSwipePrev, isLoading, error } = useGallerySwipe(firebaseUid, currentImageId, controller);
25
+ return (_jsxs("div", { className: "gallery-container", children: [error && (_jsxs("div", { className: "error-message", style: { color: 'red' }, children: ["Error: ", error] })), isLoading && (_jsx("div", { className: "loading-indicator", children: "Loading..." })), currentImageData && (_jsxs("div", { className: "image-display", children: [_jsx("img", { src: currentImageData.download?.path || currentImageData.raw_edited?.path, alt: `Image ${currentImageData.id}`, style: { maxWidth: '100%', height: 'auto' } }), _jsxs("p", { children: ["Image ID: ", currentImageData.id] }), _jsxs("p", { children: ["Event ID: ", currentImageData.event_id] })] })), _jsxs("div", { className: "navigation-controls", children: [_jsx("button", { onClick: onSwipePrev, disabled: !isPrevAvailable || isLoading, className: "nav-button prev", children: "\u2190 Previous" }), _jsx("button", { onClick: onSwipeNext, disabled: !isNextAvailable || isLoading, className: "nav-button next", children: "Next \u2192" })] })] }));
26
+ }
27
+ /**
28
+ * Advanced example: Gallery with keyboard navigation and swipe gestures
29
+ */
30
+ export function AdvancedGalleryExample() {
31
+ const [firebaseUid] = useState('user456');
32
+ const [selectedImageId, setSelectedImageId] = useState('initial_image_id');
33
+ const [controller] = useState(null); // Replace with actual controller
34
+ const gallerySwipe = useGallerySwipe(firebaseUid, selectedImageId, controller);
35
+ // Keyboard navigation
36
+ useEffect(() => {
37
+ const handleKeyPress = async (event) => {
38
+ if (gallerySwipe.isLoading)
39
+ return;
40
+ switch (event.key) {
41
+ case 'ArrowLeft':
42
+ event.preventDefault();
43
+ if (gallerySwipe.isPrevAvailable) {
44
+ await gallerySwipe.onSwipePrev();
45
+ }
46
+ break;
47
+ case 'ArrowRight':
48
+ event.preventDefault();
49
+ if (gallerySwipe.isNextAvailable) {
50
+ await gallerySwipe.onSwipeNext();
51
+ }
52
+ break;
53
+ }
54
+ };
55
+ window.addEventListener('keydown', handleKeyPress);
56
+ return () => window.removeEventListener('keydown', handleKeyPress);
57
+ }, [gallerySwipe]);
58
+ // Touch/swipe gesture handlers (basic implementation)
59
+ const [touchStart, setTouchStart] = useState(null);
60
+ const [touchEnd, setTouchEnd] = useState(null);
61
+ const handleTouchStart = (e) => {
62
+ setTouchEnd(null);
63
+ setTouchStart(e.targetTouches[0].clientX);
64
+ };
65
+ const handleTouchMove = (e) => {
66
+ setTouchEnd(e.targetTouches[0].clientX);
67
+ };
68
+ const handleTouchEnd = async () => {
69
+ if (!touchStart || !touchEnd)
70
+ return;
71
+ const distance = touchStart - touchEnd;
72
+ const isLeftSwipe = distance > 50;
73
+ const isRightSwipe = distance < -50;
74
+ if (isLeftSwipe && gallerySwipe.isNextAvailable) {
75
+ await gallerySwipe.onSwipeNext();
76
+ }
77
+ if (isRightSwipe && gallerySwipe.isPrevAvailable) {
78
+ await gallerySwipe.onSwipePrev();
79
+ }
80
+ };
81
+ return (_jsxs("div", { className: "advanced-gallery", children: [_jsxs("div", { className: "gallery-header", children: [_jsx("h2", { children: "Advanced Gallery Navigation" }), _jsxs("div", { className: "status-indicators", children: [_jsx("span", { className: `status ${gallerySwipe.isLoading ? 'loading' : 'ready'}`, children: gallerySwipe.isLoading ? 'Loading...' : 'Ready' }), _jsxs("span", { className: "navigation-status", children: ["Prev: ", gallerySwipe.isPrevAvailable ? '✓' : '✗', " | Next: ", gallerySwipe.isNextAvailable ? '✓' : '✗'] })] })] }), _jsxs("div", { className: "image-container", onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: {
82
+ touchAction: 'pan-x',
83
+ userSelect: 'none',
84
+ position: 'relative'
85
+ }, children: [gallerySwipe.currentImageData ? (_jsxs("div", { className: "image-wrapper", children: [_jsx("img", { src: gallerySwipe.currentImageData.raw_edited?.path ||
86
+ gallerySwipe.currentImageData.download?.path, alt: `Gallery image ${gallerySwipe.currentImageData.id}`, style: {
87
+ width: '100%',
88
+ height: 'auto',
89
+ display: 'block'
90
+ } }), _jsxs("div", { className: "image-info", children: [_jsxs("p", { children: ["ID: ", gallerySwipe.currentImageData.id] }), _jsxs("p", { children: ["Event: ", gallerySwipe.currentImageData.event_id] })] })] })) : (_jsx("div", { className: "no-image", children: "No image loaded" })), gallerySwipe.error && (_jsx("div", { className: "error-overlay", children: gallerySwipe.error }))] }), _jsx("div", { className: "instructions", children: _jsx("p", { children: "Use arrow keys, navigation buttons, or swipe to navigate" }) })] }));
91
+ }
92
+ /**
93
+ * Integration example: Using with existing useHonchoEditor
94
+ */
95
+ export function EditorIntegrationExample() {
96
+ const [firebaseUid] = useState('editor_user');
97
+ const [currentImageId, setCurrentImageId] = useState('editor_image_001');
98
+ const [controller] = useState(null);
99
+ // Gallery navigation hook
100
+ const { currentImageData, isNextAvailable, isPrevAvailable, onSwipeNext, onSwipePrev, isLoading: galleryLoading, error: galleryError } = useGallerySwipe(firebaseUid, currentImageId, controller);
101
+ // Update current image ID when gallery navigation changes
102
+ useEffect(() => {
103
+ if (currentImageData?.id && currentImageData.id !== currentImageId) {
104
+ setCurrentImageId(currentImageData.id);
105
+ // Trigger any editor updates here
106
+ // e.g., loadImageFromId(firebaseUid, currentImageData.id);
107
+ }
108
+ }, [currentImageData, currentImageId, firebaseUid]);
109
+ // Example: Integrate with your existing editor handlers
110
+ const handleImageChange = async (direction) => {
111
+ try {
112
+ if (direction === 'next' && isNextAvailable) {
113
+ await onSwipeNext();
114
+ }
115
+ else if (direction === 'prev' && isPrevAvailable) {
116
+ await onSwipePrev();
117
+ }
118
+ }
119
+ catch (error) {
120
+ console.error('Navigation error:', error);
121
+ }
122
+ };
123
+ return (_jsxs("div", { className: "editor-integration", children: [_jsxs("div", { className: "editor-header", children: [_jsx("button", { onClick: () => handleImageChange('prev'), disabled: !isPrevAvailable || galleryLoading, className: "nav-btn", children: "\u27E8 Prev Image" }), _jsx("span", { className: "current-image-info", children: currentImageData ? `Image: ${currentImageData.id}` : 'No image' }), _jsx("button", { onClick: () => handleImageChange('next'), disabled: !isNextAvailable || galleryLoading, className: "nav-btn", children: "Next Image \u27E9" })] }), galleryError && (_jsxs("div", { className: "error-banner", children: ["Gallery Error: ", galleryError] })), _jsx("div", { className: "editor-content", children: currentImageData && (_jsxs("div", { className: "editor-canvas", children: [_jsxs("p", { children: ["Editing: ", currentImageData.id] }), _jsxs("p", { children: ["Event: ", currentImageData.event_id] })] })) }), galleryLoading && (_jsx("div", { className: "loading-overlay", children: "Loading next image..." }))] }));
124
+ }
125
+ /**
126
+ * Mobile-optimized example with touch gestures
127
+ */
128
+ export function MobileGalleryExample() {
129
+ const [firebaseUid] = useState('mobile_user');
130
+ const [imageId] = useState('mobile_image_001');
131
+ const [controller] = useState(null);
132
+ const gallery = useGallerySwipe(firebaseUid, imageId, controller);
133
+ // Mobile-specific gesture handling
134
+ const [swipeThreshold] = useState(75); // Minimum distance for swipe
135
+ const [touchStartX, setTouchStartX] = useState(0);
136
+ const [touchStartY, setTouchStartY] = useState(0);
137
+ const handleTouchStart = (e) => {
138
+ setTouchStartX(e.touches[0].clientX);
139
+ setTouchStartY(e.touches[0].clientY);
140
+ };
141
+ const handleTouchEnd = async (e) => {
142
+ const touchEndX = e.changedTouches[0].clientX;
143
+ const touchEndY = e.changedTouches[0].clientY;
144
+ const deltaX = touchStartX - touchEndX;
145
+ const deltaY = Math.abs(touchStartY - touchEndY);
146
+ // Only process horizontal swipes (ignore vertical)
147
+ if (deltaY < 100 && Math.abs(deltaX) > swipeThreshold) {
148
+ if (deltaX > 0 && gallery.isNextAvailable) {
149
+ // Swipe left - next image
150
+ await gallery.onSwipeNext();
151
+ }
152
+ else if (deltaX < 0 && gallery.isPrevAvailable) {
153
+ // Swipe right - previous image
154
+ await gallery.onSwipePrev();
155
+ }
156
+ }
157
+ };
158
+ return (_jsxs("div", { className: "mobile-gallery", style: { touchAction: 'pan-y' }, children: [_jsx("div", { className: "mobile-header", children: _jsxs("div", { className: "navigation-dots", children: [_jsx("span", { className: `dot ${gallery.isPrevAvailable ? 'active' : 'inactive'}`, children: "\u25CF" }), _jsx("span", { className: "current-dot", children: "\u25CF" }), _jsx("span", { className: `dot ${gallery.isNextAvailable ? 'active' : 'inactive'}`, children: "\u25CF" })] }) }), _jsxs("div", { className: "swipe-area", onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd, style: {
159
+ width: '100%',
160
+ height: '70vh',
161
+ position: 'relative',
162
+ overflow: 'hidden'
163
+ }, children: [gallery.currentImageData && (_jsx("img", { src: gallery.currentImageData.download?.path, alt: "Gallery image", style: {
164
+ width: '100%',
165
+ height: '100%',
166
+ objectFit: 'contain',
167
+ userSelect: 'none',
168
+ pointerEvents: 'none'
169
+ } })), gallery.isLoading && (_jsxs("div", { className: "mobile-loading", children: [_jsx("div", { className: "spinner", children: "\u25D0" }), _jsx("p", { children: "Loading..." })] })), gallery.error && (_jsx("div", { className: "mobile-error", children: _jsxs("p", { children: ["\u26A0\uFE0F ", gallery.error] }) }))] }), _jsx("div", { className: "mobile-footer", children: _jsxs("p", { className: "swipe-hint", children: ["Swipe left/right to navigate \u2022 ", gallery.currentImageData?.id || 'No image'] }) })] }));
170
+ }
171
+ /**
172
+ * Error handling and loading states example
173
+ */
174
+ export function ErrorHandlingExample() {
175
+ const [firebaseUid] = useState('test_user');
176
+ const [imageId] = useState('test_image');
177
+ const [controller] = useState(null);
178
+ const { currentImageData, isNextAvailable, isPrevAvailable, onSwipeNext, onSwipePrev, isLoading, error } = useGallerySwipe(firebaseUid, imageId, controller);
179
+ const handleRetry = () => {
180
+ // Force re-initialization by changing a key prop or calling a refresh function
181
+ window.location.reload(); // Simple retry approach
182
+ };
183
+ return (_jsxs("div", { className: "error-handling-example", children: [_jsxs("div", { className: "status-panel", children: [_jsx("h3", { children: "Gallery Status" }), _jsxs("div", { className: "status-grid", children: [_jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Loading:" }), _jsx("span", { className: isLoading ? 'status-active' : 'status-inactive', children: isLoading ? 'Yes' : 'No' })] }), _jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Error:" }), _jsx("span", { className: error ? 'status-error' : 'status-ok', children: error || 'None' })] }), _jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Current Image:" }), _jsx("span", { children: currentImageData?.id || 'None' })] }), _jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Navigation:" }), _jsxs("span", { children: ["Prev: ", isPrevAvailable ? '✓' : '✗', " | Next: ", isNextAvailable ? '✓' : '✗'] })] })] }), error && (_jsx("div", { className: "error-actions", children: _jsx("button", { onClick: handleRetry, className: "retry-button", children: "Retry" }) }))] }), _jsx("div", { className: "gallery-content", children: currentImageData ? (_jsx("div", { className: "image-preview", children: _jsx("img", { src: currentImageData.download?.path, alt: "Current", style: { maxWidth: '300px', maxHeight: '200px' } }) })) : (_jsx("div", { className: "no-content", children: isLoading ? 'Loading gallery...' : 'No image available' })) }), _jsxs("div", { className: "navigation-test", children: [_jsx("button", { onClick: onSwipePrev, disabled: !isPrevAvailable || isLoading, children: "Test Previous" }), _jsx("button", { onClick: onSwipeNext, disabled: !isNextAvailable || isLoading, children: "Test Next" })] })] }));
184
+ }
@@ -0,0 +1,321 @@
1
+ import { useState, useEffect, useCallback, useRef } from "react";
2
+ /**
3
+ * Custom hook for handling image gallery navigation with automatic pagination
4
+ *
5
+ * This hook manages image swipe/navigation functionality across a paginated gallery.
6
+ * It handles the complexity of finding images across multiple pages and loading
7
+ * additional pages as needed during navigation.
8
+ *
9
+ * @param firebaseUid - User's Firebase UID (can be null during initialization)
10
+ * @param initImageId - Initial image ID to start with (can be null during initialization)
11
+ * @param controller - Controller instance for API calls (can be null during initialization)
12
+ * @returns Object containing current image data and navigation functions
13
+ */
14
+ export function useGallerySwipe(firebaseUid, initImageId, controller) {
15
+ // Core state for current image and navigation
16
+ const [currentImageId, setCurrentImageId] = useState(initImageId);
17
+ const [currentImageData, setCurrentImageData] = useState(null);
18
+ const [currentEventId, setCurrentEventId] = useState(null);
19
+ // Pagination and image list state
20
+ const [currentImageList, setCurrentImageList] = useState([]);
21
+ const [currentPage, setCurrentPage] = useState(1);
22
+ const [hasNextPage, setHasNextPage] = useState(false);
23
+ // UI state management
24
+ const [isLoading, setIsLoading] = useState(false);
25
+ const [error, setError] = useState(null);
26
+ const [isInitialized, setIsInitialized] = useState(false);
27
+ // Refs to track parameter changes and avoid unnecessary re-initialization
28
+ // This prevents the hook from re-initializing when the same values are passed
29
+ const prevFirebaseUid = useRef(null);
30
+ const prevInitImageId = useRef(null);
31
+ /**
32
+ * Get the index of the current image in the loaded image list
33
+ * Returns -1 if the current image is not found in the list
34
+ */
35
+ const getCurrentImageIndex = useCallback(() => {
36
+ if (!currentImageId || currentImageList.length === 0)
37
+ return -1;
38
+ return currentImageList.findIndex(img => img.id === currentImageId);
39
+ }, [currentImageId, currentImageList]);
40
+ /**
41
+ * Fetch image pages sequentially until the target image is found
42
+ * This is necessary because we don't know which page contains the initial image
43
+ *
44
+ * @param imageId - The target image ID to find
45
+ * @param eventId - The event ID to search within
46
+ * @returns Array of all gallery images up to and including the page containing the target image
47
+ */
48
+ const getImageListUntilFound = useCallback(async (imageId, eventId) => {
49
+ let page = 1;
50
+ let allImages = [];
51
+ let isFound = false;
52
+ // Search through pages until we find the target image or reach safety limit
53
+ while (!isFound && page <= 100) { // Safety limit to prevent infinite loop
54
+ try {
55
+ const response = await controller.getImageList(firebaseUid, eventId, page);
56
+ if (response.gallery && response.gallery.length > 0) {
57
+ // Accumulate all images from previous pages
58
+ allImages = [...allImages, ...response.gallery];
59
+ // Check if target image is found in current page
60
+ isFound = response.gallery.some(img => img.id === imageId);
61
+ if (isFound) {
62
+ // Update pagination state when target is found
63
+ setCurrentPage(page);
64
+ setHasNextPage(response.next_page !== 0 && response.next_page > response.current_page);
65
+ break;
66
+ }
67
+ // If no next page exists, stop searching
68
+ if (response.next_page === 0 || response.next_page <= response.current_page) {
69
+ break;
70
+ }
71
+ page++;
72
+ }
73
+ else {
74
+ // Empty response, stop searching
75
+ break;
76
+ }
77
+ }
78
+ catch (err) {
79
+ console.error(`Error fetching page ${page}:`, err);
80
+ break;
81
+ }
82
+ }
83
+ return allImages;
84
+ }, [controller, firebaseUid]);
85
+ /**
86
+ * Load the next page of images when user reaches the end of current list
87
+ * This enables seamless navigation across page boundaries
88
+ *
89
+ * @returns Array of new images from the next page, or empty array if no more pages
90
+ */
91
+ const loadNextPage = useCallback(async () => {
92
+ // Check prerequisites before attempting to load next page
93
+ if (!hasNextPage || !currentEventId || !controller || !firebaseUid) {
94
+ return [];
95
+ }
96
+ try {
97
+ const nextPageNum = currentPage + 1;
98
+ const response = await controller.getImageList(firebaseUid, currentEventId, nextPageNum);
99
+ if (response.gallery && response.gallery.length > 0) {
100
+ // Update pagination state with new page information
101
+ setCurrentPage(nextPageNum);
102
+ setHasNextPage(response.next_page !== 0 && response.next_page > response.current_page);
103
+ return response.gallery;
104
+ }
105
+ }
106
+ catch (err) {
107
+ console.error('Error loading next page:', err);
108
+ }
109
+ return [];
110
+ }, [hasNextPage, currentEventId, controller, firebaseUid, currentPage]);
111
+ /**
112
+ * Initialize or re-initialize the gallery when parameters change
113
+ * This function:
114
+ * 1. Fetches initial image data
115
+ * 2. Discovers the event ID from the image
116
+ * 3. Loads all image pages until the initial image is found
117
+ * 4. Sets up pagination state
118
+ *
119
+ * Only re-initializes when parameters actually change to avoid unnecessary API calls
120
+ */
121
+ const initializeGallery = useCallback(async () => {
122
+ // Early return if required parameters are missing
123
+ if (!firebaseUid || !initImageId || !controller) {
124
+ setCurrentImageData(null);
125
+ setCurrentImageList([]);
126
+ setIsInitialized(false);
127
+ return;
128
+ }
129
+ // Check if re-initialization is needed by comparing with previous values
130
+ const needsReinit = prevFirebaseUid.current !== firebaseUid ||
131
+ prevInitImageId.current !== initImageId ||
132
+ !isInitialized;
133
+ if (!needsReinit)
134
+ return;
135
+ setIsLoading(true);
136
+ setError(null);
137
+ try {
138
+ // Step 1: Get initial image data to discover event ID
139
+ const gallery = await controller.onGetImage(firebaseUid, initImageId);
140
+ if (!gallery) {
141
+ throw new Error('Failed to fetch initial image data');
142
+ }
143
+ // Step 2: Set up initial state with discovered data
144
+ setCurrentImageData(gallery);
145
+ setCurrentImageId(initImageId);
146
+ setCurrentEventId(gallery.event_id);
147
+ // Step 3: Get complete image list by searching through pages
148
+ // This ensures we have navigation context for the current image
149
+ const allImages = await getImageListUntilFound(initImageId, gallery.event_id);
150
+ setCurrentImageList(allImages);
151
+ // Step 4: Update tracking refs to prevent unnecessary re-initialization
152
+ prevFirebaseUid.current = firebaseUid;
153
+ prevInitImageId.current = initImageId;
154
+ setIsInitialized(true);
155
+ }
156
+ catch (err) {
157
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
158
+ setError(errorMessage);
159
+ console.error('Error initializing gallery:', err);
160
+ }
161
+ finally {
162
+ setIsLoading(false);
163
+ }
164
+ }, [firebaseUid, initImageId, controller, getImageListUntilFound, isInitialized]);
165
+ /**
166
+ * Navigate to the next image in the gallery
167
+ * Handles two scenarios:
168
+ * 1. Next image exists in current loaded list - navigate directly
169
+ * 2. At end of current list - load next page and navigate to first image of new page
170
+ *
171
+ * @returns Promise that resolves when navigation is complete
172
+ */
173
+ const onSwipeNext = useCallback(async () => {
174
+ // Prevent action if no current image or already loading
175
+ if (!currentImageId || isLoading)
176
+ return;
177
+ setIsLoading(true);
178
+ setError(null);
179
+ try {
180
+ const currentIndex = getCurrentImageIndex();
181
+ if (currentIndex === -1) {
182
+ throw new Error('Current image not found in list');
183
+ }
184
+ // Scenario 1: At the last image of current list
185
+ if (currentIndex === currentImageList.length - 1) {
186
+ // Try to load next page for more images
187
+ const newImages = await loadNextPage();
188
+ if (newImages.length > 0) {
189
+ // Extend current list with new images
190
+ const updatedList = [...currentImageList, ...newImages];
191
+ setCurrentImageList(updatedList);
192
+ // Navigate to first image of the new page
193
+ const nextImage = newImages[0];
194
+ setCurrentImageId(nextImage.id);
195
+ // Fetch complete data for the new current image
196
+ const nextImageData = await controller.onGetImage(firebaseUid, nextImage.id);
197
+ if (nextImageData) {
198
+ setCurrentImageData(nextImageData);
199
+ }
200
+ }
201
+ else {
202
+ // No more pages available - end of gallery reached
203
+ setError('No more images available');
204
+ }
205
+ }
206
+ else {
207
+ // Scenario 2: Navigate to next image in current list
208
+ const nextImage = currentImageList[currentIndex + 1];
209
+ setCurrentImageId(nextImage.id);
210
+ // Fetch complete data for the next image
211
+ const nextImageData = await controller.onGetImage(firebaseUid, nextImage.id);
212
+ if (nextImageData) {
213
+ setCurrentImageData(nextImageData);
214
+ }
215
+ }
216
+ }
217
+ catch (err) {
218
+ const errorMessage = err instanceof Error ? err.message : 'Error moving to next image';
219
+ setError(errorMessage);
220
+ console.error('Error in onSwipeNext:', err);
221
+ }
222
+ finally {
223
+ setIsLoading(false);
224
+ }
225
+ }, [currentImageId, isLoading, getCurrentImageIndex, currentImageList, loadNextPage, controller, firebaseUid]);
226
+ /**
227
+ * Navigate to the previous image in the gallery
228
+ * Only works within the currently loaded image list
229
+ * (Previous pages are not loaded on-demand for backward navigation)
230
+ *
231
+ * @returns Promise that resolves when navigation is complete
232
+ */
233
+ const onSwipePrev = useCallback(async () => {
234
+ // Prevent action if no current image or already loading
235
+ if (!currentImageId || isLoading)
236
+ return;
237
+ setIsLoading(true);
238
+ setError(null);
239
+ try {
240
+ const currentIndex = getCurrentImageIndex();
241
+ if (currentIndex === -1) {
242
+ throw new Error('Current image not found in list');
243
+ }
244
+ if (currentIndex > 0) {
245
+ // Navigate to previous image in the current list
246
+ const prevImage = currentImageList[currentIndex - 1];
247
+ setCurrentImageId(prevImage.id);
248
+ // Fetch complete data for the previous image
249
+ const prevImageData = await controller.onGetImage(firebaseUid, prevImage.id);
250
+ if (prevImageData) {
251
+ setCurrentImageData(prevImageData);
252
+ }
253
+ }
254
+ else {
255
+ // Already at the first image of loaded list
256
+ setError('Already at the first image');
257
+ }
258
+ }
259
+ catch (err) {
260
+ const errorMessage = err instanceof Error ? err.message : 'Error moving to previous image';
261
+ setError(errorMessage);
262
+ console.error('Error in onSwipePrev:', err);
263
+ }
264
+ finally {
265
+ setIsLoading(false);
266
+ }
267
+ }, [currentImageId, isLoading, getCurrentImageIndex, currentImageList, controller, firebaseUid]);
268
+ /**
269
+ * Calculate if next image navigation is available
270
+ * Returns true if:
271
+ * 1. There's a next image in the current loaded list, OR
272
+ * 2. We're at the end of current list but more pages are available
273
+ *
274
+ * @returns Boolean indicating if next navigation is possible
275
+ */
276
+ const isNextAvailable = useCallback(() => {
277
+ if (isLoading)
278
+ return false;
279
+ const currentIndex = getCurrentImageIndex();
280
+ if (currentIndex === -1)
281
+ return false;
282
+ // If we're not at the last image, next is definitely available
283
+ if (currentIndex < currentImageList.length - 1)
284
+ return true;
285
+ // If we're at the last image but there are more pages, next is still available
286
+ return hasNextPage;
287
+ }, [isLoading, getCurrentImageIndex, currentImageList.length, hasNextPage]);
288
+ /**
289
+ * Calculate if previous image navigation is available
290
+ * Returns true if there's a previous image in the currently loaded list
291
+ *
292
+ * @returns Boolean indicating if previous navigation is possible
293
+ */
294
+ const isPrevAvailable = useCallback(() => {
295
+ if (isLoading)
296
+ return false;
297
+ const currentIndex = getCurrentImageIndex();
298
+ return currentIndex > 0;
299
+ }, [isLoading, getCurrentImageIndex]);
300
+ // Initialize when dependencies change
301
+ useEffect(() => {
302
+ initializeGallery();
303
+ }, [initializeGallery]);
304
+ // Update currentImageId when initImageId changes (but only if different)
305
+ // This allows the hook to respond to external changes to the initial image ID
306
+ useEffect(() => {
307
+ if (initImageId && initImageId !== currentImageId) {
308
+ setCurrentImageId(initImageId);
309
+ }
310
+ }, [initImageId, currentImageId]);
311
+ // Return all the functionality and state that components need
312
+ return {
313
+ currentImageData,
314
+ isNextAvailable: isNextAvailable(),
315
+ isPrevAvailable: isPrevAvailable(),
316
+ onSwipeNext,
317
+ onSwipePrev,
318
+ isLoading,
319
+ error
320
+ };
321
+ }
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,5 @@
1
+ import { ColorAdjustment } from "../hooks/editor/type";
2
+ import { AdjustmentState } from "../hooks/editor/useHonchoEditor";
3
+ import { AdjustmentValues } from "../lib/editor/honcho-editor";
4
+ export declare function mapAdjustmentStateToAdjustmentEditor(state: AdjustmentState): AdjustmentValues;
5
+ export declare function mapColorAdjustmentToAdjustmentState(colors: ColorAdjustment): AdjustmentState;
@@ -0,0 +1,32 @@
1
+ export function mapAdjustmentStateToAdjustmentEditor(state) {
2
+ return {
3
+ temperature: state.tempScore,
4
+ tint: state.tintScore,
5
+ saturation: state.saturationScore,
6
+ vibrance: state.vibranceScore,
7
+ exposure: state.exposureScore,
8
+ contrast: state.contrastScore,
9
+ highlights: state.highlightsScore,
10
+ shadows: state.shadowsScore,
11
+ whites: state.whitesScore,
12
+ blacks: state.blacksScore,
13
+ clarity: state.clarityScore,
14
+ sharpness: state.sharpnessScore,
15
+ };
16
+ }
17
+ export function mapColorAdjustmentToAdjustmentState(colors) {
18
+ return {
19
+ tempScore: colors.temperature,
20
+ tintScore: colors.tint,
21
+ vibranceScore: colors.vibrance,
22
+ saturationScore: colors.saturation,
23
+ exposureScore: colors.exposure,
24
+ highlightsScore: colors.highlights,
25
+ shadowsScore: colors.shadows,
26
+ whitesScore: colors.whites,
27
+ blacksScore: colors.blacks,
28
+ contrastScore: colors.contrast,
29
+ clarityScore: colors.clarity,
30
+ sharpnessScore: colors.sharpness
31
+ };
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yogiswara/honcho-editor-ui",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "A complete UI component library for the Honcho photo editor.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -10,7 +10,10 @@
10
10
  "public"
11
11
  ],
12
12
  "scripts": {
13
- "build": "tsc"
13
+ "build": "tsc",
14
+ "test": "jest",
15
+ "test:watch": "jest --watch",
16
+ "test:coverage": "jest --coverage"
14
17
  },
15
18
  "peerDependencies": {
16
19
  "@emotion/react": "^11.11.1",
@@ -26,12 +29,18 @@
26
29
  "@emotion/styled": "^11.11.0",
27
30
  "@mui/icons-material": "^6.1.2",
28
31
  "@mui/material": "^6.1.2",
32
+ "@testing-library/jest-dom": "^6.6.4",
33
+ "@testing-library/react": "^16.3.0",
34
+ "@types/jest": "^30.0.0",
29
35
  "@types/node": "^20",
30
36
  "@types/react": "^18",
31
37
  "@types/react-dom": "^18",
38
+ "jest": "^30.0.5",
39
+ "jest-environment-jsdom": "^30.0.5",
32
40
  "next": "^13.5.6 || ^14.0.0",
33
41
  "react": "^18",
34
42
  "react-dom": "^18",
43
+ "ts-jest": "^29.4.1",
35
44
  "tsup": "^8.0.0",
36
45
  "typescript": "^5"
37
46
  },