@umituz/react-native-onboarding 3.6.11 → 3.6.12
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/package.json +1 -1
- package/src/domain/entities/OnboardingSlide.ts +22 -2
- package/src/index.ts +8 -1
- package/src/infrastructure/utils/arrayUtils.ts +28 -0
- package/src/infrastructure/utils/imageLayoutUtils.ts +280 -0
- package/src/presentation/components/BackgroundImageCollage.tsx +63 -129
- package/src/presentation/components/OnboardingBackground.tsx +4 -1
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +8 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.12",
|
|
4
4
|
"description": "Advanced onboarding flow for React Native apps with personalization questions, theme-aware colors, animations, and customizable slides. SOLID, DRY, KISS principles applied.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -95,12 +95,32 @@ export interface OnboardingSlide {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Layout pattern for multiple background images
|
|
98
|
-
* 'grid' - Equal sized grid
|
|
98
|
+
* 'grid' - Equal sized grid (auto columns)
|
|
99
|
+
* 'dense' - Dense grid with many small images (6 columns)
|
|
99
100
|
* 'masonry' - Pinterest-style masonry layout
|
|
100
101
|
* 'collage' - Random sizes and positions
|
|
102
|
+
* 'scattered' - Small randomly placed images
|
|
103
|
+
* 'tiles' - Fixed size tiles centered
|
|
104
|
+
* 'honeycomb' - Hexagonal pattern
|
|
101
105
|
* Default: 'grid'
|
|
102
106
|
*/
|
|
103
|
-
backgroundImagesLayout?: 'grid' | 'masonry' | 'collage';
|
|
107
|
+
backgroundImagesLayout?: 'grid' | 'dense' | 'masonry' | 'collage' | 'scattered' | 'tiles' | 'honeycomb';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Number of columns for grid-based layouts
|
|
111
|
+
* Only applies to: grid, dense, masonry, tiles
|
|
112
|
+
*/
|
|
113
|
+
backgroundImagesColumns?: number;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Gap between images in pixels
|
|
117
|
+
*/
|
|
118
|
+
backgroundImagesGap?: number;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Border radius for images
|
|
122
|
+
*/
|
|
123
|
+
backgroundImagesBorderRadius?: number;
|
|
104
124
|
|
|
105
125
|
/**
|
|
106
126
|
* Optional background video (URL or require path)
|
package/src/index.ts
CHANGED
|
@@ -69,9 +69,16 @@ export { OnboardingScreen, type OnboardingScreenProps } from "./presentation/scr
|
|
|
69
69
|
export { OnboardingHeader, type OnboardingHeaderProps } from "./presentation/components/OnboardingHeader";
|
|
70
70
|
export { OnboardingFooter, type OnboardingFooterProps } from "./presentation/components/OnboardingFooter";
|
|
71
71
|
export { OnboardingProvider, type OnboardingProviderProps, useOnboardingProvider } from "./presentation/providers/OnboardingProvider";
|
|
72
|
-
export { BackgroundImageCollage } from "./presentation/components/BackgroundImageCollage";
|
|
72
|
+
export { BackgroundImageCollage, type CollageLayout, type BackgroundImageCollageProps } from "./presentation/components/BackgroundImageCollage";
|
|
73
73
|
export type { OnboardingTheme, OnboardingColors } from "./presentation/types/OnboardingTheme";
|
|
74
74
|
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// UTILITIES - Helper functions
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
export { ensureArray, safeIncludes, safeFilter } from "./infrastructure/utils/arrayUtils";
|
|
80
|
+
export type { ImageLayoutItem, LayoutConfig } from "./infrastructure/utils/imageLayoutUtils";
|
|
81
|
+
|
|
75
82
|
// Export OnboardingSlide component
|
|
76
83
|
// Note: TypeScript doesn't allow exporting both a type and a value with the same name
|
|
77
84
|
// The type is exported above as OnboardingSlide
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array Utilities
|
|
3
|
+
* Safe array operations for onboarding components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ensures the value is a valid array, returns empty array if not
|
|
8
|
+
*/
|
|
9
|
+
export const ensureArray = <T>(value: T[] | undefined | null): T[] => {
|
|
10
|
+
return Array.isArray(value) ? value : [];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safe includes check that handles undefined/null values
|
|
15
|
+
*/
|
|
16
|
+
export const safeIncludes = <T>(array: T[] | undefined | null, item: T): boolean => {
|
|
17
|
+
return ensureArray(array).includes(item);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safe filter that handles undefined/null values
|
|
22
|
+
*/
|
|
23
|
+
export const safeFilter = <T>(
|
|
24
|
+
array: T[] | undefined | null,
|
|
25
|
+
predicate: (item: T) => boolean,
|
|
26
|
+
): T[] => {
|
|
27
|
+
return ensureArray(array).filter(predicate);
|
|
28
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Layout Utilities
|
|
3
|
+
* Layout generators for BackgroundImageCollage component
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Dimensions } from "react-native";
|
|
7
|
+
|
|
8
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
9
|
+
|
|
10
|
+
export interface ImageLayoutStyle {
|
|
11
|
+
position: "absolute";
|
|
12
|
+
top: number;
|
|
13
|
+
left: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
borderRadius?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ImageLayoutItem {
|
|
20
|
+
source: unknown;
|
|
21
|
+
style: ImageLayoutStyle;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LayoutConfig {
|
|
25
|
+
columns?: number;
|
|
26
|
+
gap?: number;
|
|
27
|
+
borderRadius?: number;
|
|
28
|
+
randomizeSize?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a uniform grid layout
|
|
33
|
+
*/
|
|
34
|
+
export const generateGridLayout = (
|
|
35
|
+
images: unknown[],
|
|
36
|
+
config: LayoutConfig = {},
|
|
37
|
+
): ImageLayoutItem[] => {
|
|
38
|
+
const { columns, gap = 0, borderRadius = 0 } = config;
|
|
39
|
+
const count = images.length;
|
|
40
|
+
const cols = columns ?? Math.ceil(Math.sqrt(count));
|
|
41
|
+
const rows = Math.ceil(count / cols);
|
|
42
|
+
const totalGapX = gap * (cols - 1);
|
|
43
|
+
const totalGapY = gap * (rows - 1);
|
|
44
|
+
const cellWidth = (SCREEN_WIDTH - totalGapX) / cols;
|
|
45
|
+
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
46
|
+
|
|
47
|
+
return images.map((source, index) => {
|
|
48
|
+
const col = index % cols;
|
|
49
|
+
const row = Math.floor(index / cols);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
source,
|
|
53
|
+
style: {
|
|
54
|
+
position: "absolute" as const,
|
|
55
|
+
left: col * (cellWidth + gap),
|
|
56
|
+
top: row * (cellHeight + gap),
|
|
57
|
+
width: cellWidth,
|
|
58
|
+
height: cellHeight,
|
|
59
|
+
borderRadius,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a dense grid layout with many small images
|
|
67
|
+
*/
|
|
68
|
+
export const generateDenseGridLayout = (
|
|
69
|
+
images: unknown[],
|
|
70
|
+
config: LayoutConfig = {},
|
|
71
|
+
): ImageLayoutItem[] => {
|
|
72
|
+
const { columns = 6, gap = 2, borderRadius = 4 } = config;
|
|
73
|
+
const count = images.length;
|
|
74
|
+
const rows = Math.ceil(count / columns);
|
|
75
|
+
const totalGapX = gap * (columns - 1);
|
|
76
|
+
const totalGapY = gap * (rows - 1);
|
|
77
|
+
const cellWidth = (SCREEN_WIDTH - totalGapX) / columns;
|
|
78
|
+
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
79
|
+
|
|
80
|
+
return images.map((source, index) => {
|
|
81
|
+
const col = index % columns;
|
|
82
|
+
const row = Math.floor(index / columns);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
source,
|
|
86
|
+
style: {
|
|
87
|
+
position: "absolute" as const,
|
|
88
|
+
left: col * (cellWidth + gap),
|
|
89
|
+
top: row * (cellHeight + gap),
|
|
90
|
+
width: cellWidth,
|
|
91
|
+
height: cellHeight,
|
|
92
|
+
borderRadius,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate a masonry-style layout (Pinterest-like)
|
|
100
|
+
*/
|
|
101
|
+
export const generateMasonryLayout = (
|
|
102
|
+
images: unknown[],
|
|
103
|
+
config: LayoutConfig = {},
|
|
104
|
+
): ImageLayoutItem[] => {
|
|
105
|
+
const { columns = 3, gap = 2, borderRadius = 4 } = config;
|
|
106
|
+
const colWidth = (SCREEN_WIDTH - gap * (columns - 1)) / columns;
|
|
107
|
+
const columnHeights = new Array(columns).fill(0);
|
|
108
|
+
|
|
109
|
+
return images.map((source) => {
|
|
110
|
+
const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
|
|
111
|
+
const aspectRatio = 0.7 + Math.random() * 0.6;
|
|
112
|
+
const height = colWidth * aspectRatio;
|
|
113
|
+
|
|
114
|
+
const layout: ImageLayoutItem = {
|
|
115
|
+
source,
|
|
116
|
+
style: {
|
|
117
|
+
position: "absolute" as const,
|
|
118
|
+
left: shortestCol * (colWidth + gap),
|
|
119
|
+
top: columnHeights[shortestCol],
|
|
120
|
+
width: colWidth,
|
|
121
|
+
height,
|
|
122
|
+
borderRadius,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
columnHeights[shortestCol] += height + gap;
|
|
127
|
+
return layout;
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate a random collage layout with varying sizes
|
|
133
|
+
*/
|
|
134
|
+
export const generateCollageLayout = (
|
|
135
|
+
images: unknown[],
|
|
136
|
+
config: LayoutConfig = {},
|
|
137
|
+
): ImageLayoutItem[] => {
|
|
138
|
+
const { borderRadius = 8 } = config;
|
|
139
|
+
const layouts: ImageLayoutItem[] = [];
|
|
140
|
+
const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.15;
|
|
141
|
+
const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.35;
|
|
142
|
+
|
|
143
|
+
images.forEach((source) => {
|
|
144
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
145
|
+
const maxX = SCREEN_WIDTH - size;
|
|
146
|
+
const maxY = SCREEN_HEIGHT - size;
|
|
147
|
+
|
|
148
|
+
let attempts = 0;
|
|
149
|
+
let overlaps = true;
|
|
150
|
+
let left = 0;
|
|
151
|
+
let top = 0;
|
|
152
|
+
|
|
153
|
+
while (overlaps && attempts < 50) {
|
|
154
|
+
left = Math.random() * maxX;
|
|
155
|
+
top = Math.random() * maxY;
|
|
156
|
+
overlaps = false;
|
|
157
|
+
|
|
158
|
+
for (const existing of layouts) {
|
|
159
|
+
const overlapThreshold = 20;
|
|
160
|
+
if (
|
|
161
|
+
left < existing.style.left + existing.style.width - overlapThreshold &&
|
|
162
|
+
left + size > existing.style.left + overlapThreshold &&
|
|
163
|
+
top < existing.style.top + existing.style.height - overlapThreshold &&
|
|
164
|
+
top + size > existing.style.top + overlapThreshold
|
|
165
|
+
) {
|
|
166
|
+
overlaps = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
attempts++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
layouts.push({
|
|
175
|
+
source,
|
|
176
|
+
style: {
|
|
177
|
+
position: "absolute" as const,
|
|
178
|
+
left,
|
|
179
|
+
top,
|
|
180
|
+
width: size,
|
|
181
|
+
height: size,
|
|
182
|
+
borderRadius,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return layouts;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generate a scattered layout with small images
|
|
192
|
+
*/
|
|
193
|
+
export const generateScatteredLayout = (
|
|
194
|
+
images: unknown[],
|
|
195
|
+
config: LayoutConfig = {},
|
|
196
|
+
): ImageLayoutItem[] => {
|
|
197
|
+
const { borderRadius = 6 } = config;
|
|
198
|
+
const minSize = 60;
|
|
199
|
+
const maxSize = 100;
|
|
200
|
+
|
|
201
|
+
return images.map((source) => {
|
|
202
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
203
|
+
const left = Math.random() * (SCREEN_WIDTH - size);
|
|
204
|
+
const top = Math.random() * (SCREEN_HEIGHT - size);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
source,
|
|
208
|
+
style: {
|
|
209
|
+
position: "absolute" as const,
|
|
210
|
+
left,
|
|
211
|
+
top,
|
|
212
|
+
width: size,
|
|
213
|
+
height: size,
|
|
214
|
+
borderRadius,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate a tiles layout with fixed small size
|
|
222
|
+
*/
|
|
223
|
+
export const generateTilesLayout = (
|
|
224
|
+
images: unknown[],
|
|
225
|
+
config: LayoutConfig = {},
|
|
226
|
+
): ImageLayoutItem[] => {
|
|
227
|
+
const { columns = 5, gap = 4, borderRadius = 8 } = config;
|
|
228
|
+
const tileSize = (SCREEN_WIDTH - gap * (columns + 1)) / columns;
|
|
229
|
+
const rows = Math.ceil(images.length / columns);
|
|
230
|
+
const startY = (SCREEN_HEIGHT - rows * (tileSize + gap)) / 2;
|
|
231
|
+
|
|
232
|
+
return images.map((source, index) => {
|
|
233
|
+
const col = index % columns;
|
|
234
|
+
const row = Math.floor(index / columns);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
source,
|
|
238
|
+
style: {
|
|
239
|
+
position: "absolute" as const,
|
|
240
|
+
left: gap + col * (tileSize + gap),
|
|
241
|
+
top: startY + row * (tileSize + gap),
|
|
242
|
+
width: tileSize,
|
|
243
|
+
height: tileSize,
|
|
244
|
+
borderRadius,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Generate a honeycomb/hex layout
|
|
252
|
+
*/
|
|
253
|
+
export const generateHoneycombLayout = (
|
|
254
|
+
images: unknown[],
|
|
255
|
+
config: LayoutConfig = {},
|
|
256
|
+
): ImageLayoutItem[] => {
|
|
257
|
+
const { borderRadius = 50 } = config;
|
|
258
|
+
const size = 80;
|
|
259
|
+
const horizontalSpacing = size * 0.85;
|
|
260
|
+
const verticalSpacing = size * 0.75;
|
|
261
|
+
const columns = Math.floor(SCREEN_WIDTH / horizontalSpacing);
|
|
262
|
+
|
|
263
|
+
return images.map((source, index) => {
|
|
264
|
+
const row = Math.floor(index / columns);
|
|
265
|
+
const col = index % columns;
|
|
266
|
+
const offsetX = row % 2 === 1 ? horizontalSpacing / 2 : 0;
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
source,
|
|
270
|
+
style: {
|
|
271
|
+
position: "absolute" as const,
|
|
272
|
+
left: col * horizontalSpacing + offsetX,
|
|
273
|
+
top: row * verticalSpacing,
|
|
274
|
+
width: size,
|
|
275
|
+
height: size,
|
|
276
|
+
borderRadius,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
};
|
|
@@ -4,48 +4,81 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
|
-
import { View, StyleSheet
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import { Image } from "expo-image";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
import {
|
|
10
|
+
generateGridLayout,
|
|
11
|
+
generateDenseGridLayout,
|
|
12
|
+
generateMasonryLayout,
|
|
13
|
+
generateCollageLayout,
|
|
14
|
+
generateScatteredLayout,
|
|
15
|
+
generateTilesLayout,
|
|
16
|
+
generateHoneycombLayout,
|
|
17
|
+
type ImageLayoutItem,
|
|
18
|
+
type LayoutConfig,
|
|
19
|
+
} from "../../infrastructure/utils/imageLayoutUtils";
|
|
20
|
+
import { ensureArray } from "../../infrastructure/utils/arrayUtils";
|
|
21
|
+
|
|
22
|
+
export type CollageLayout =
|
|
23
|
+
| "grid"
|
|
24
|
+
| "dense"
|
|
25
|
+
| "masonry"
|
|
26
|
+
| "collage"
|
|
27
|
+
| "scattered"
|
|
28
|
+
| "tiles"
|
|
29
|
+
| "honeycomb";
|
|
30
|
+
|
|
31
|
+
export interface BackgroundImageCollageProps {
|
|
32
|
+
images: unknown[];
|
|
33
|
+
layout?: CollageLayout;
|
|
34
|
+
columns?: number;
|
|
35
|
+
gap?: number;
|
|
36
|
+
borderRadius?: number;
|
|
37
|
+
opacity?: number;
|
|
15
38
|
}
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
const getLayoutGenerator = (layout: CollageLayout) => {
|
|
41
|
+
const generators: Record<
|
|
42
|
+
CollageLayout,
|
|
43
|
+
(images: unknown[], config: LayoutConfig) => ImageLayoutItem[]
|
|
44
|
+
> = {
|
|
45
|
+
grid: generateGridLayout,
|
|
46
|
+
dense: generateDenseGridLayout,
|
|
47
|
+
masonry: generateMasonryLayout,
|
|
48
|
+
collage: generateCollageLayout,
|
|
49
|
+
scattered: generateScatteredLayout,
|
|
50
|
+
tiles: generateTilesLayout,
|
|
51
|
+
honeycomb: generateHoneycombLayout,
|
|
25
52
|
};
|
|
26
|
-
|
|
53
|
+
|
|
54
|
+
return generators[layout] ?? generateGridLayout;
|
|
55
|
+
};
|
|
27
56
|
|
|
28
57
|
export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
29
58
|
images,
|
|
30
|
-
layout =
|
|
59
|
+
layout = "grid",
|
|
60
|
+
columns,
|
|
61
|
+
gap,
|
|
62
|
+
borderRadius,
|
|
63
|
+
opacity = 1,
|
|
31
64
|
}) => {
|
|
65
|
+
const safeImages = ensureArray(images);
|
|
66
|
+
|
|
32
67
|
const imageLayouts = useMemo(() => {
|
|
33
|
-
if (
|
|
68
|
+
if (safeImages.length === 0) return [];
|
|
69
|
+
|
|
70
|
+
const generator = getLayoutGenerator(layout);
|
|
71
|
+
const config: LayoutConfig = { columns, gap, borderRadius };
|
|
34
72
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return generateCollageLayout(images);
|
|
42
|
-
default:
|
|
43
|
-
return generateGridLayout(images);
|
|
44
|
-
}
|
|
45
|
-
}, [images, layout]);
|
|
73
|
+
return generator(safeImages, config);
|
|
74
|
+
}, [safeImages, layout, columns, gap, borderRadius]);
|
|
75
|
+
|
|
76
|
+
if (imageLayouts.length === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
46
79
|
|
|
47
80
|
return (
|
|
48
|
-
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
81
|
+
<View style={[StyleSheet.absoluteFill, { opacity }]} pointerEvents="none">
|
|
49
82
|
{imageLayouts.map((item, index) => (
|
|
50
83
|
<Image
|
|
51
84
|
key={index}
|
|
@@ -58,102 +91,3 @@ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
|
58
91
|
</View>
|
|
59
92
|
);
|
|
60
93
|
};
|
|
61
|
-
|
|
62
|
-
const generateGridLayout = (images: any[]): ImageLayout[] => {
|
|
63
|
-
const count = images.length;
|
|
64
|
-
const cols = Math.ceil(Math.sqrt(count));
|
|
65
|
-
const rows = Math.ceil(count / cols);
|
|
66
|
-
const cellWidth = SCREEN_WIDTH / cols;
|
|
67
|
-
const cellHeight = SCREEN_HEIGHT / rows;
|
|
68
|
-
|
|
69
|
-
return images.map((source, index) => {
|
|
70
|
-
const col = index % cols;
|
|
71
|
-
const row = Math.floor(index / cols);
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
source,
|
|
75
|
-
style: {
|
|
76
|
-
position: 'absolute' as const,
|
|
77
|
-
left: col * cellWidth,
|
|
78
|
-
top: row * cellHeight,
|
|
79
|
-
width: cellWidth,
|
|
80
|
-
height: cellHeight,
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const generateMasonryLayout = (images: any[]): ImageLayout[] => {
|
|
87
|
-
const cols = 3;
|
|
88
|
-
const colWidth = SCREEN_WIDTH / cols;
|
|
89
|
-
const columnHeights = new Array(cols).fill(0);
|
|
90
|
-
|
|
91
|
-
return images.map((source, _index) => {
|
|
92
|
-
const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
|
|
93
|
-
const height = colWidth * (0.7 + Math.random() * 0.6);
|
|
94
|
-
|
|
95
|
-
const layout: ImageLayout = {
|
|
96
|
-
source,
|
|
97
|
-
style: {
|
|
98
|
-
position: 'absolute' as const,
|
|
99
|
-
left: shortestCol * colWidth,
|
|
100
|
-
top: columnHeights[shortestCol],
|
|
101
|
-
width: colWidth,
|
|
102
|
-
height,
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
columnHeights[shortestCol] += height;
|
|
107
|
-
return layout;
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const generateCollageLayout = (images: any[]): ImageLayout[] => {
|
|
112
|
-
const layouts: ImageLayout[] = [];
|
|
113
|
-
const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.25;
|
|
114
|
-
const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.45;
|
|
115
|
-
|
|
116
|
-
images.forEach((source, _index) => {
|
|
117
|
-
const size = minSize + Math.random() * (maxSize - minSize);
|
|
118
|
-
const maxX = SCREEN_WIDTH - size;
|
|
119
|
-
const maxY = SCREEN_HEIGHT - size;
|
|
120
|
-
|
|
121
|
-
let attempts = 0;
|
|
122
|
-
let overlaps = true;
|
|
123
|
-
let left = 0;
|
|
124
|
-
let top = 0;
|
|
125
|
-
|
|
126
|
-
while (overlaps && attempts < 50) {
|
|
127
|
-
left = Math.random() * maxX;
|
|
128
|
-
top = Math.random() * maxY;
|
|
129
|
-
overlaps = false;
|
|
130
|
-
|
|
131
|
-
for (const existing of layouts) {
|
|
132
|
-
if (
|
|
133
|
-
left < existing.style.left + existing.style.width &&
|
|
134
|
-
left + size > existing.style.left &&
|
|
135
|
-
top < existing.style.top + existing.style.height &&
|
|
136
|
-
top + size > existing.style.top
|
|
137
|
-
) {
|
|
138
|
-
overlaps = true;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
attempts++;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
layouts.push({
|
|
147
|
-
source,
|
|
148
|
-
style: {
|
|
149
|
-
position: 'absolute' as const,
|
|
150
|
-
left,
|
|
151
|
-
top,
|
|
152
|
-
width: size,
|
|
153
|
-
height: size,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
return layouts;
|
|
159
|
-
};
|
|
@@ -40,7 +40,10 @@ export const OnboardingBackground: React.FC<OnboardingBackgroundProps> = ({
|
|
|
40
40
|
return (
|
|
41
41
|
<BackgroundImageCollage
|
|
42
42
|
images={currentSlide.backgroundImages}
|
|
43
|
-
layout={currentSlide.backgroundImagesLayout ||
|
|
43
|
+
layout={currentSlide.backgroundImagesLayout || "grid"}
|
|
44
|
+
columns={currentSlide.backgroundImagesColumns}
|
|
45
|
+
gap={currentSlide.backgroundImagesGap}
|
|
46
|
+
borderRadius={currentSlide.backgroundImagesBorderRadius}
|
|
44
47
|
/>
|
|
45
48
|
);
|
|
46
49
|
}
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
4
4
|
import { useOnboardingProvider } from "../../providers/OnboardingProvider";
|
|
5
|
+
import { ensureArray } from "../../../infrastructure/utils/arrayUtils";
|
|
5
6
|
import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
|
|
6
7
|
|
|
7
8
|
export interface MultipleChoiceQuestionProps {
|
|
@@ -12,17 +13,19 @@ export interface MultipleChoiceQuestionProps {
|
|
|
12
13
|
|
|
13
14
|
export const MultipleChoiceQuestion = ({
|
|
14
15
|
question,
|
|
15
|
-
value
|
|
16
|
+
value,
|
|
16
17
|
onChange,
|
|
17
18
|
}: MultipleChoiceQuestionProps) => {
|
|
18
19
|
const {
|
|
19
20
|
theme: { colors },
|
|
20
21
|
} = useOnboardingProvider();
|
|
21
22
|
|
|
23
|
+
const safeValue = ensureArray(value);
|
|
24
|
+
|
|
22
25
|
const handleToggle = (optionId: string) => {
|
|
23
|
-
const newValue =
|
|
24
|
-
?
|
|
25
|
-
: [...
|
|
26
|
+
const newValue = safeValue.includes(optionId)
|
|
27
|
+
? safeValue.filter((id) => id !== optionId)
|
|
28
|
+
: [...safeValue, optionId];
|
|
26
29
|
|
|
27
30
|
if (question.validation?.maxSelections && newValue.length > question.validation.maxSelections) {
|
|
28
31
|
return;
|
|
@@ -30,7 +33,7 @@ export const MultipleChoiceQuestion = ({
|
|
|
30
33
|
onChange(newValue);
|
|
31
34
|
};
|
|
32
35
|
const renderOption = (option: QuestionOption) => {
|
|
33
|
-
const isSelected =
|
|
36
|
+
const isSelected = safeValue.includes(option.id);
|
|
34
37
|
const isEmoji = option.iconType === 'emoji';
|
|
35
38
|
|
|
36
39
|
return (
|