@umituz/react-native-onboarding 3.6.11 → 3.6.13
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/layouts/collageLayout.ts +81 -0
- package/src/infrastructure/utils/layouts/gridLayouts.ts +68 -0
- package/src/infrastructure/utils/layouts/honeycombLayout.ts +36 -0
- package/src/infrastructure/utils/layouts/index.ts +12 -0
- package/src/infrastructure/utils/layouts/layoutTypes.ts +25 -0
- package/src/infrastructure/utils/layouts/masonryLayout.ts +37 -0
- package/src/infrastructure/utils/layouts/scatteredLayout.ts +34 -0
- package/src/infrastructure/utils/layouts/screenDimensions.ts +11 -0
- package/src/infrastructure/utils/layouts/tilesLayout.ts +34 -0
- package/src/presentation/components/BackgroundImageCollage.tsx +55 -130
- package/src/presentation/components/OnboardingBackground.tsx +4 -1
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +10 -7
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.13",
|
|
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/layouts";
|
|
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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collage Layout Generator
|
|
3
|
+
* Random collage with varying sizes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateCollageLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { borderRadius = 8 } = config;
|
|
14
|
+
const layouts: ImageLayoutItem[] = [];
|
|
15
|
+
const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.15;
|
|
16
|
+
const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.35;
|
|
17
|
+
|
|
18
|
+
images.forEach((source) => {
|
|
19
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
20
|
+
const position = findNonOverlappingPosition(layouts, size);
|
|
21
|
+
|
|
22
|
+
layouts.push({
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left: position.left,
|
|
27
|
+
top: position.top,
|
|
28
|
+
width: size,
|
|
29
|
+
height: size,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return layouts;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const findNonOverlappingPosition = (
|
|
39
|
+
existing: ImageLayoutItem[],
|
|
40
|
+
size: number,
|
|
41
|
+
): { left: number; top: number } => {
|
|
42
|
+
const maxX = SCREEN_WIDTH - size;
|
|
43
|
+
const maxY = SCREEN_HEIGHT - size;
|
|
44
|
+
let attempts = 0;
|
|
45
|
+
let left = 0;
|
|
46
|
+
let top = 0;
|
|
47
|
+
|
|
48
|
+
while (attempts < 50) {
|
|
49
|
+
left = Math.random() * maxX;
|
|
50
|
+
top = Math.random() * maxY;
|
|
51
|
+
|
|
52
|
+
if (!hasOverlap(existing, left, top, size)) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
attempts++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { left, top };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const hasOverlap = (
|
|
62
|
+
existing: ImageLayoutItem[],
|
|
63
|
+
left: number,
|
|
64
|
+
top: number,
|
|
65
|
+
size: number,
|
|
66
|
+
): boolean => {
|
|
67
|
+
const threshold = 20;
|
|
68
|
+
|
|
69
|
+
for (const item of existing) {
|
|
70
|
+
const { style } = item;
|
|
71
|
+
if (
|
|
72
|
+
left < style.left + style.width - threshold &&
|
|
73
|
+
left + size > style.left + threshold &&
|
|
74
|
+
top < style.top + style.height - threshold &&
|
|
75
|
+
top + size > style.top + threshold
|
|
76
|
+
) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid Layout Generators
|
|
3
|
+
* Standard and dense grid layouts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateGridLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { columns, gap = 0, borderRadius = 0 } = config;
|
|
14
|
+
const count = images.length;
|
|
15
|
+
const cols = columns ?? Math.ceil(Math.sqrt(count));
|
|
16
|
+
const rows = Math.ceil(count / cols);
|
|
17
|
+
const totalGapX = gap * (cols - 1);
|
|
18
|
+
const totalGapY = gap * (rows - 1);
|
|
19
|
+
const cellWidth = (SCREEN_WIDTH - totalGapX) / cols;
|
|
20
|
+
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
21
|
+
|
|
22
|
+
return images.map((source, index) => {
|
|
23
|
+
const col = index % cols;
|
|
24
|
+
const row = Math.floor(index / cols);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
source,
|
|
28
|
+
style: {
|
|
29
|
+
position: "absolute" as const,
|
|
30
|
+
left: col * (cellWidth + gap),
|
|
31
|
+
top: row * (cellHeight + gap),
|
|
32
|
+
width: cellWidth,
|
|
33
|
+
height: cellHeight,
|
|
34
|
+
borderRadius,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const generateDenseGridLayout = (
|
|
41
|
+
images: unknown[],
|
|
42
|
+
config: LayoutConfig = {},
|
|
43
|
+
): ImageLayoutItem[] => {
|
|
44
|
+
const { columns = 6, gap = 2, borderRadius = 4 } = config;
|
|
45
|
+
const count = images.length;
|
|
46
|
+
const rows = Math.ceil(count / columns);
|
|
47
|
+
const totalGapX = gap * (columns - 1);
|
|
48
|
+
const totalGapY = gap * (rows - 1);
|
|
49
|
+
const cellWidth = (SCREEN_WIDTH - totalGapX) / columns;
|
|
50
|
+
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
51
|
+
|
|
52
|
+
return images.map((source, index) => {
|
|
53
|
+
const col = index % columns;
|
|
54
|
+
const row = Math.floor(index / columns);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
source,
|
|
58
|
+
style: {
|
|
59
|
+
position: "absolute" as const,
|
|
60
|
+
left: col * (cellWidth + gap),
|
|
61
|
+
top: row * (cellHeight + gap),
|
|
62
|
+
width: cellWidth,
|
|
63
|
+
height: cellHeight,
|
|
64
|
+
borderRadius,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honeycomb Layout Generator
|
|
3
|
+
* Hexagonal pattern layout
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateHoneycombLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { borderRadius = 50 } = config;
|
|
14
|
+
const size = 80;
|
|
15
|
+
const horizontalSpacing = size * 0.85;
|
|
16
|
+
const verticalSpacing = size * 0.75;
|
|
17
|
+
const columns = Math.floor(SCREEN_WIDTH / horizontalSpacing);
|
|
18
|
+
|
|
19
|
+
return images.map((source, index) => {
|
|
20
|
+
const row = Math.floor(index / columns);
|
|
21
|
+
const col = index % columns;
|
|
22
|
+
const offsetX = row % 2 === 1 ? horizontalSpacing / 2 : 0;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
source,
|
|
26
|
+
style: {
|
|
27
|
+
position: "absolute" as const,
|
|
28
|
+
left: col * horizontalSpacing + offsetX,
|
|
29
|
+
top: row * verticalSpacing,
|
|
30
|
+
width: size,
|
|
31
|
+
height: size,
|
|
32
|
+
borderRadius,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Generators - Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { ImageLayoutItem, ImageLayoutStyle, LayoutConfig } from "./layoutTypes";
|
|
6
|
+
|
|
7
|
+
export { generateGridLayout, generateDenseGridLayout } from "./gridLayouts";
|
|
8
|
+
export { generateMasonryLayout } from "./masonryLayout";
|
|
9
|
+
export { generateCollageLayout } from "./collageLayout";
|
|
10
|
+
export { generateScatteredLayout } from "./scatteredLayout";
|
|
11
|
+
export { generateTilesLayout } from "./tilesLayout";
|
|
12
|
+
export { generateHoneycombLayout } from "./honeycombLayout";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Types
|
|
3
|
+
* Type definitions for image layout generators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ImageLayoutStyle {
|
|
7
|
+
position: "absolute";
|
|
8
|
+
top: number;
|
|
9
|
+
left: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
borderRadius?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ImageLayoutItem {
|
|
16
|
+
source: unknown;
|
|
17
|
+
style: ImageLayoutStyle;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LayoutConfig {
|
|
21
|
+
columns?: number;
|
|
22
|
+
gap?: number;
|
|
23
|
+
borderRadius?: number;
|
|
24
|
+
randomizeSize?: boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Masonry Layout Generator
|
|
3
|
+
* Pinterest-style masonry layout
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateMasonryLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { columns = 3, gap = 2, borderRadius = 4 } = config;
|
|
14
|
+
const colWidth = (SCREEN_WIDTH - gap * (columns - 1)) / columns;
|
|
15
|
+
const columnHeights = new Array(columns).fill(0);
|
|
16
|
+
|
|
17
|
+
return images.map((source) => {
|
|
18
|
+
const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
|
|
19
|
+
const aspectRatio = 0.7 + Math.random() * 0.6;
|
|
20
|
+
const height = colWidth * aspectRatio;
|
|
21
|
+
|
|
22
|
+
const layout: ImageLayoutItem = {
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left: shortestCol * (colWidth + gap),
|
|
27
|
+
top: columnHeights[shortestCol],
|
|
28
|
+
width: colWidth,
|
|
29
|
+
height,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
columnHeights[shortestCol] += height + gap;
|
|
35
|
+
return layout;
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scattered Layout Generator
|
|
3
|
+
* Random small images scattered across screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateScatteredLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { borderRadius = 6 } = config;
|
|
14
|
+
const minSize = 60;
|
|
15
|
+
const maxSize = 100;
|
|
16
|
+
|
|
17
|
+
return images.map((source) => {
|
|
18
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
19
|
+
const left = Math.random() * (SCREEN_WIDTH - size);
|
|
20
|
+
const top = Math.random() * (SCREEN_HEIGHT - size);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left,
|
|
27
|
+
top,
|
|
28
|
+
width: size,
|
|
29
|
+
height: size,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen Dimensions
|
|
3
|
+
* Centralized screen dimension values
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Dimensions } from "react-native";
|
|
7
|
+
|
|
8
|
+
const dimensions = Dimensions.get("window");
|
|
9
|
+
|
|
10
|
+
export const SCREEN_WIDTH = dimensions.width;
|
|
11
|
+
export const SCREEN_HEIGHT = dimensions.height;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiles Layout Generator
|
|
3
|
+
* Fixed size tiles centered on screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateTilesLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { columns = 5, gap = 4, borderRadius = 8 } = config;
|
|
14
|
+
const tileSize = (SCREEN_WIDTH - gap * (columns + 1)) / columns;
|
|
15
|
+
const rows = Math.ceil(images.length / columns);
|
|
16
|
+
const startY = (SCREEN_HEIGHT - rows * (tileSize + gap)) / 2;
|
|
17
|
+
|
|
18
|
+
return images.map((source, index) => {
|
|
19
|
+
const col = index % columns;
|
|
20
|
+
const row = Math.floor(index / columns);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left: gap + col * (tileSize + gap),
|
|
27
|
+
top: startY + row * (tileSize + gap),
|
|
28
|
+
width: tileSize,
|
|
29
|
+
height: tileSize,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
@@ -4,48 +4,72 @@
|
|
|
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/layouts";
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
type LayoutGenerator = (images: unknown[], config: LayoutConfig) => ImageLayoutItem[];
|
|
41
|
+
|
|
42
|
+
const LAYOUT_GENERATORS: Record<CollageLayout, LayoutGenerator> = {
|
|
43
|
+
grid: generateGridLayout,
|
|
44
|
+
dense: generateDenseGridLayout,
|
|
45
|
+
masonry: generateMasonryLayout,
|
|
46
|
+
collage: generateCollageLayout,
|
|
47
|
+
scattered: generateScatteredLayout,
|
|
48
|
+
tiles: generateTilesLayout,
|
|
49
|
+
honeycomb: generateHoneycombLayout,
|
|
50
|
+
};
|
|
27
51
|
|
|
28
52
|
export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
29
53
|
images,
|
|
30
|
-
layout =
|
|
54
|
+
layout = "grid",
|
|
55
|
+
columns,
|
|
56
|
+
gap,
|
|
57
|
+
borderRadius,
|
|
58
|
+
opacity = 1,
|
|
31
59
|
}) => {
|
|
60
|
+
const safeImages = ensureArray(images);
|
|
61
|
+
|
|
32
62
|
const imageLayouts = useMemo(() => {
|
|
33
|
-
if (
|
|
63
|
+
if (safeImages.length === 0) return [];
|
|
34
64
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
case 'collage':
|
|
41
|
-
return generateCollageLayout(images);
|
|
42
|
-
default:
|
|
43
|
-
return generateGridLayout(images);
|
|
44
|
-
}
|
|
45
|
-
}, [images, layout]);
|
|
65
|
+
const generator = LAYOUT_GENERATORS[layout] ?? generateGridLayout;
|
|
66
|
+
return generator(safeImages, { columns, gap, borderRadius });
|
|
67
|
+
}, [safeImages, layout, columns, gap, borderRadius]);
|
|
68
|
+
|
|
69
|
+
if (imageLayouts.length === 0) return null;
|
|
46
70
|
|
|
47
71
|
return (
|
|
48
|
-
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
72
|
+
<View style={[StyleSheet.absoluteFill, { opacity }]} pointerEvents="none">
|
|
49
73
|
{imageLayouts.map((item, index) => (
|
|
50
74
|
<Image
|
|
51
75
|
key={index}
|
|
@@ -58,102 +82,3 @@ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
|
58
82
|
</View>
|
|
59
83
|
);
|
|
60
84
|
};
|
|
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
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
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
|
-
import { useOnboardingProvider } from "
|
|
5
|
-
import
|
|
4
|
+
import { useOnboardingProvider } from "@/presentation/providers/OnboardingProvider";
|
|
5
|
+
import { ensureArray } from "@/infrastructure/utils/arrayUtils";
|
|
6
|
+
import type { OnboardingQuestion, QuestionOption } from "@/domain/entities/OnboardingQuestion";
|
|
6
7
|
|
|
7
8
|
export interface MultipleChoiceQuestionProps {
|
|
8
9
|
question: OnboardingQuestion;
|
|
@@ -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 (
|