@umituz/react-native-design-system 2.6.128 → 2.7.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.
- package/package.json +4 -2
- package/src/exports/onboarding.ts +6 -0
- package/src/index.ts +5 -0
- package/src/onboarding/domain/entities/OnboardingOptions.ts +104 -0
- package/src/onboarding/domain/entities/OnboardingQuestion.ts +165 -0
- package/src/onboarding/domain/entities/OnboardingSlide.ts +152 -0
- package/src/onboarding/domain/entities/OnboardingUserData.ts +43 -0
- package/src/onboarding/hooks/useOnboardingFlow.ts +50 -0
- package/src/onboarding/index.ts +108 -0
- package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
- package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
- package/src/onboarding/infrastructure/hooks/useOnboardingAnswers.ts +69 -0
- package/src/onboarding/infrastructure/hooks/useOnboardingNavigation.ts +75 -0
- package/src/onboarding/infrastructure/services/SlideManager.ts +53 -0
- package/src/onboarding/infrastructure/services/ValidationManager.ts +127 -0
- package/src/onboarding/infrastructure/storage/OnboardingStore.ts +99 -0
- package/src/onboarding/infrastructure/storage/OnboardingStoreActions.ts +50 -0
- package/src/onboarding/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
- package/src/onboarding/infrastructure/storage/OnboardingStoreState.ts +22 -0
- package/src/onboarding/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
- package/src/onboarding/infrastructure/storage/actions/answerActions.ts +47 -0
- package/src/onboarding/infrastructure/storage/actions/completeAction.ts +45 -0
- package/src/onboarding/infrastructure/storage/actions/index.ts +22 -0
- package/src/onboarding/infrastructure/storage/actions/initializeAction.ts +40 -0
- package/src/onboarding/infrastructure/storage/actions/resetAction.ts +37 -0
- package/src/onboarding/infrastructure/storage/actions/skipAction.ts +46 -0
- package/src/onboarding/infrastructure/storage/actions/storageHelpers.ts +60 -0
- package/src/onboarding/infrastructure/utils/arrayUtils.ts +28 -0
- package/src/onboarding/infrastructure/utils/backgroundUtils.ts +38 -0
- package/src/onboarding/infrastructure/utils/layouts/collageLayout.ts +81 -0
- package/src/onboarding/infrastructure/utils/layouts/gridLayouts.ts +78 -0
- package/src/onboarding/infrastructure/utils/layouts/honeycombLayout.ts +36 -0
- package/src/onboarding/infrastructure/utils/layouts/index.ts +12 -0
- package/src/onboarding/infrastructure/utils/layouts/layoutTypes.ts +37 -0
- package/src/onboarding/infrastructure/utils/layouts/masonryLayout.ts +37 -0
- package/src/onboarding/infrastructure/utils/layouts/scatteredLayout.ts +34 -0
- package/src/onboarding/infrastructure/utils/layouts/screenDimensions.ts +11 -0
- package/src/onboarding/infrastructure/utils/layouts/tilesLayout.ts +34 -0
- package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +90 -0
- package/src/onboarding/presentation/components/BackgroundVideo.tsx +24 -0
- package/src/onboarding/presentation/components/BaseSlide.tsx +47 -0
- package/src/onboarding/presentation/components/OnboardingBackground.tsx +91 -0
- package/src/onboarding/presentation/components/OnboardingFooter.tsx +151 -0
- package/src/onboarding/presentation/components/OnboardingHeader.tsx +92 -0
- package/src/onboarding/presentation/components/OnboardingResetSetting.tsx +70 -0
- package/src/onboarding/presentation/components/OnboardingScreenContent.tsx +146 -0
- package/src/onboarding/presentation/components/OnboardingSlide.tsx +124 -0
- package/src/onboarding/presentation/components/QuestionRenderer.tsx +60 -0
- package/src/onboarding/presentation/components/QuestionSlide.tsx +67 -0
- package/src/onboarding/presentation/components/QuestionSlideHeader.tsx +75 -0
- package/src/onboarding/presentation/components/questions/MultipleChoiceQuestion.tsx +74 -0
- package/src/onboarding/presentation/components/questions/QuestionOptionItem.tsx +115 -0
- package/src/onboarding/presentation/components/questions/RatingQuestion.tsx +66 -0
- package/src/onboarding/presentation/components/questions/SingleChoiceQuestion.tsx +117 -0
- package/src/onboarding/presentation/components/questions/TextInputQuestion.tsx +71 -0
- package/src/onboarding/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
- package/src/onboarding/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
- package/src/onboarding/presentation/hooks/useOnboardingGestures.ts +45 -0
- package/src/onboarding/presentation/hooks/useOnboardingScreenHandlers.ts +114 -0
- package/src/onboarding/presentation/hooks/useOnboardingScreenState.ts +146 -0
- package/src/onboarding/presentation/providers/OnboardingProvider.tsx +51 -0
- package/src/onboarding/presentation/screens/OnboardingScreen.tsx +189 -0
- package/src/onboarding/presentation/types/OnboardingProps.ts +46 -0
- package/src/onboarding/presentation/types/OnboardingTheme.ts +27 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid Layout Generators
|
|
3
|
+
* Standard and dense grid layouts with safe area support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig, ImageSourceType } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateGridLayout = (
|
|
10
|
+
images: ImageSourceType[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { columns, gap = 0, borderRadius = 0, safeAreaInsets } = config;
|
|
14
|
+
const count = images.length;
|
|
15
|
+
const cols = columns ?? Math.ceil(Math.sqrt(count));
|
|
16
|
+
const rows = Math.ceil(count / cols);
|
|
17
|
+
|
|
18
|
+
const insets = safeAreaInsets ?? { top: 0, bottom: 0, left: 0, right: 0 };
|
|
19
|
+
const availableWidth = SCREEN_WIDTH - insets.left - insets.right;
|
|
20
|
+
const availableHeight = SCREEN_HEIGHT - insets.top - insets.bottom;
|
|
21
|
+
|
|
22
|
+
const totalGapX = gap * (cols - 1);
|
|
23
|
+
const totalGapY = gap * (rows - 1);
|
|
24
|
+
const cellWidth = (availableWidth - totalGapX) / cols;
|
|
25
|
+
const cellHeight = (availableHeight - totalGapY) / rows;
|
|
26
|
+
|
|
27
|
+
return images.map((source, index) => {
|
|
28
|
+
const col = index % cols;
|
|
29
|
+
const row = Math.floor(index / cols);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
source,
|
|
33
|
+
style: {
|
|
34
|
+
position: "absolute" as const,
|
|
35
|
+
left: insets.left + col * (cellWidth + gap),
|
|
36
|
+
top: insets.top + row * (cellHeight + gap),
|
|
37
|
+
width: cellWidth,
|
|
38
|
+
height: cellHeight,
|
|
39
|
+
borderRadius,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const generateDenseGridLayout = (
|
|
46
|
+
images: ImageSourceType[],
|
|
47
|
+
config: LayoutConfig = {},
|
|
48
|
+
): ImageLayoutItem[] => {
|
|
49
|
+
const { columns = 6, gap = 2, borderRadius = 4, safeAreaInsets } = config;
|
|
50
|
+
const count = images.length;
|
|
51
|
+
const rows = Math.ceil(count / columns);
|
|
52
|
+
|
|
53
|
+
const insets = safeAreaInsets ?? { top: 0, bottom: 0, left: 0, right: 0 };
|
|
54
|
+
const availableWidth = SCREEN_WIDTH - insets.left - insets.right;
|
|
55
|
+
const availableHeight = SCREEN_HEIGHT - insets.top - insets.bottom;
|
|
56
|
+
|
|
57
|
+
const totalGapX = gap * (columns - 1);
|
|
58
|
+
const totalGapY = gap * (rows - 1);
|
|
59
|
+
const cellWidth = (availableWidth - totalGapX) / columns;
|
|
60
|
+
const cellHeight = (availableHeight - totalGapY) / rows;
|
|
61
|
+
|
|
62
|
+
return images.map((source, index) => {
|
|
63
|
+
const col = index % columns;
|
|
64
|
+
const row = Math.floor(index / columns);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
source,
|
|
68
|
+
style: {
|
|
69
|
+
position: "absolute" as const,
|
|
70
|
+
left: insets.left + col * (cellWidth + gap),
|
|
71
|
+
top: insets.top + row * (cellHeight + gap),
|
|
72
|
+
width: cellWidth,
|
|
73
|
+
height: cellHeight,
|
|
74
|
+
borderRadius,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
};
|
|
@@ -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, ImageSourceType } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateHoneycombLayout = (
|
|
10
|
+
images: ImageSourceType[],
|
|
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, ImageSourceType } 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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Types
|
|
3
|
+
* Type definitions for image layout generators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ImageSource } from "expo-image";
|
|
7
|
+
|
|
8
|
+
export type ImageSourceType = ImageSource | number | string;
|
|
9
|
+
|
|
10
|
+
export interface SafeAreaInsets {
|
|
11
|
+
top: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
left: number;
|
|
14
|
+
right: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ImageLayoutStyle {
|
|
18
|
+
position: "absolute";
|
|
19
|
+
top: number;
|
|
20
|
+
left: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
borderRadius?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ImageLayoutItem {
|
|
27
|
+
source: ImageSourceType;
|
|
28
|
+
style: ImageLayoutStyle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LayoutConfig {
|
|
32
|
+
columns?: number;
|
|
33
|
+
gap?: number;
|
|
34
|
+
borderRadius?: number;
|
|
35
|
+
randomizeSize?: boolean;
|
|
36
|
+
safeAreaInsets?: SafeAreaInsets;
|
|
37
|
+
}
|
|
@@ -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, ImageSourceType } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateMasonryLayout = (
|
|
10
|
+
images: ImageSourceType[],
|
|
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, ImageSourceType } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateScatteredLayout = (
|
|
10
|
+
images: ImageSourceType[],
|
|
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 using design system utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getScreenDimensions } from "@umituz/react-native-design-system";
|
|
7
|
+
|
|
8
|
+
const dimensions = getScreenDimensions();
|
|
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, ImageSourceType } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateTilesLayout = (
|
|
10
|
+
images: ImageSourceType[],
|
|
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
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Image Collage Component
|
|
3
|
+
* Displays multiple images in various layout patterns with safe area support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { Image } from "expo-image";
|
|
9
|
+
import { useSafeAreaInsets } from "@umituz/react-native-design-system";
|
|
10
|
+
import {
|
|
11
|
+
generateGridLayout,
|
|
12
|
+
generateDenseGridLayout,
|
|
13
|
+
generateMasonryLayout,
|
|
14
|
+
generateCollageLayout,
|
|
15
|
+
generateScatteredLayout,
|
|
16
|
+
generateTilesLayout,
|
|
17
|
+
generateHoneycombLayout,
|
|
18
|
+
type ImageLayoutItem,
|
|
19
|
+
type LayoutConfig,
|
|
20
|
+
type ImageSourceType,
|
|
21
|
+
} from "../../infrastructure/utils/layouts";
|
|
22
|
+
|
|
23
|
+
export type CollageLayout =
|
|
24
|
+
| "grid"
|
|
25
|
+
| "dense"
|
|
26
|
+
| "masonry"
|
|
27
|
+
| "collage"
|
|
28
|
+
| "scattered"
|
|
29
|
+
| "tiles"
|
|
30
|
+
| "honeycomb";
|
|
31
|
+
|
|
32
|
+
export interface BackgroundImageCollageProps {
|
|
33
|
+
images: ImageSourceType[];
|
|
34
|
+
layout?: CollageLayout;
|
|
35
|
+
columns?: number;
|
|
36
|
+
gap?: number;
|
|
37
|
+
borderRadius?: number;
|
|
38
|
+
opacity?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type LayoutGenerator = (images: ImageSourceType[], config: LayoutConfig) => ImageLayoutItem[];
|
|
42
|
+
|
|
43
|
+
const LAYOUT_GENERATORS: Record<CollageLayout, LayoutGenerator> = {
|
|
44
|
+
grid: generateGridLayout,
|
|
45
|
+
dense: generateDenseGridLayout,
|
|
46
|
+
masonry: generateMasonryLayout,
|
|
47
|
+
collage: generateCollageLayout,
|
|
48
|
+
scattered: generateScatteredLayout,
|
|
49
|
+
tiles: generateTilesLayout,
|
|
50
|
+
honeycomb: generateHoneycombLayout,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
54
|
+
images,
|
|
55
|
+
layout = "grid",
|
|
56
|
+
columns,
|
|
57
|
+
gap,
|
|
58
|
+
borderRadius,
|
|
59
|
+
opacity = 1,
|
|
60
|
+
}) => {
|
|
61
|
+
const insets = useSafeAreaInsets();
|
|
62
|
+
|
|
63
|
+
const imageLayouts = useMemo(() => {
|
|
64
|
+
if (!images || images.length === 0) return [];
|
|
65
|
+
|
|
66
|
+
const generator = LAYOUT_GENERATORS[layout] ?? generateGridLayout;
|
|
67
|
+
return generator(images, {
|
|
68
|
+
columns,
|
|
69
|
+
gap,
|
|
70
|
+
borderRadius,
|
|
71
|
+
safeAreaInsets: insets
|
|
72
|
+
});
|
|
73
|
+
}, [images, layout, columns, gap, borderRadius, insets]);
|
|
74
|
+
|
|
75
|
+
if (imageLayouts.length === 0) return null;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<View style={[StyleSheet.absoluteFill, { opacity }]} pointerEvents="none">
|
|
79
|
+
{imageLayouts.map((item, index) => (
|
|
80
|
+
<Image
|
|
81
|
+
key={index}
|
|
82
|
+
source={item.source}
|
|
83
|
+
style={item.style}
|
|
84
|
+
contentFit="cover"
|
|
85
|
+
transition={300}
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
4
|
+
|
|
5
|
+
interface BackgroundVideoProps {
|
|
6
|
+
source: any;
|
|
7
|
+
overlayOpacity?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const BackgroundVideo = ({ source, overlayOpacity = 0.5 }: BackgroundVideoProps) => {
|
|
11
|
+
const player = useVideoPlayer(source, (p: any) => {
|
|
12
|
+
p.loop = true;
|
|
13
|
+
p.play();
|
|
14
|
+
p.muted = true;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<View style={StyleSheet.absoluteFill}>
|
|
19
|
+
<VideoView player={player} style={StyleSheet.absoluteFill} contentFit="cover" nativeControls={false} />
|
|
20
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: `rgba(0,0,0,${overlayOpacity})` }]} />
|
|
21
|
+
</View>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseSlide Component
|
|
3
|
+
* Single Responsibility: Provide a base layout for all onboarding slides
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, ScrollView } from "react-native";
|
|
8
|
+
import { useAppDesignTokens, useResponsive } from "@umituz/react-native-design-system";
|
|
9
|
+
import type { ContentPosition } from "../../domain/entities/OnboardingSlide";
|
|
10
|
+
|
|
11
|
+
export interface BaseSlideProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
contentPosition?: ContentPosition;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const BaseSlide = ({
|
|
17
|
+
children,
|
|
18
|
+
contentPosition = "center",
|
|
19
|
+
}: BaseSlideProps) => {
|
|
20
|
+
const tokens = useAppDesignTokens();
|
|
21
|
+
const { verticalPadding, horizontalPadding } = useResponsive();
|
|
22
|
+
const isBottom = contentPosition === "bottom";
|
|
23
|
+
|
|
24
|
+
const contentContainerStyle = useMemo(() => ({
|
|
25
|
+
flexGrow: 1,
|
|
26
|
+
paddingVertical: verticalPadding,
|
|
27
|
+
justifyContent: isBottom ? "flex-end" as const : "center" as const,
|
|
28
|
+
paddingBottom: isBottom ? verticalPadding : undefined,
|
|
29
|
+
}), [verticalPadding, isBottom]);
|
|
30
|
+
|
|
31
|
+
const slideContainerStyle = useMemo(() => ({
|
|
32
|
+
width: "100%" as const,
|
|
33
|
+
paddingHorizontal: horizontalPadding,
|
|
34
|
+
alignItems: "center" as const,
|
|
35
|
+
}), [horizontalPadding]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ScrollView
|
|
39
|
+
style={{ flex: 1 }}
|
|
40
|
+
contentContainerStyle={contentContainerStyle}
|
|
41
|
+
showsVerticalScrollIndicator={false}
|
|
42
|
+
bounces={false}
|
|
43
|
+
>
|
|
44
|
+
<View style={slideContainerStyle}>{children}</View>
|
|
45
|
+
</ScrollView>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Background Component
|
|
3
|
+
* Handles rendering of video, image or solid backgrounds
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
|
|
9
|
+
import { Image } from "expo-image";
|
|
10
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
11
|
+
import { BackgroundVideo } from "./BackgroundVideo";
|
|
12
|
+
import { BackgroundImageCollage } from "./BackgroundImageCollage";
|
|
13
|
+
|
|
14
|
+
interface OnboardingBackgroundProps {
|
|
15
|
+
currentSlide: OnboardingSlide | undefined;
|
|
16
|
+
useCustomBackground: boolean;
|
|
17
|
+
showOverlay: boolean;
|
|
18
|
+
overlayOpacity: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const OnboardingBackground: React.FC<OnboardingBackgroundProps> = ({
|
|
22
|
+
currentSlide,
|
|
23
|
+
useCustomBackground,
|
|
24
|
+
showOverlay,
|
|
25
|
+
overlayOpacity,
|
|
26
|
+
}) => {
|
|
27
|
+
if (!currentSlide) return null;
|
|
28
|
+
|
|
29
|
+
const renderContent = () => {
|
|
30
|
+
if (currentSlide.backgroundVideo) {
|
|
31
|
+
return (
|
|
32
|
+
<BackgroundVideo
|
|
33
|
+
source={currentSlide.backgroundVideo}
|
|
34
|
+
overlayOpacity={overlayOpacity}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (currentSlide.backgroundImages && currentSlide.backgroundImages.length > 0) {
|
|
40
|
+
return (
|
|
41
|
+
<BackgroundImageCollage
|
|
42
|
+
images={currentSlide.backgroundImages}
|
|
43
|
+
layout={currentSlide.backgroundImagesLayout || "grid"}
|
|
44
|
+
columns={currentSlide.backgroundImagesColumns}
|
|
45
|
+
gap={currentSlide.backgroundImagesGap}
|
|
46
|
+
borderRadius={currentSlide.backgroundImagesBorderRadius}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (currentSlide.backgroundImage) {
|
|
52
|
+
return (
|
|
53
|
+
<Image
|
|
54
|
+
source={currentSlide.backgroundImage}
|
|
55
|
+
style={StyleSheet.absoluteFill}
|
|
56
|
+
contentFit="cover"
|
|
57
|
+
transition={500}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (useCustomBackground && currentSlide.backgroundColor) {
|
|
63
|
+
return (
|
|
64
|
+
<View
|
|
65
|
+
style={[
|
|
66
|
+
StyleSheet.absoluteFill,
|
|
67
|
+
{ backgroundColor: currentSlide.backgroundColor }
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
78
|
+
{renderContent()}
|
|
79
|
+
<View
|
|
80
|
+
style={[
|
|
81
|
+
StyleSheet.absoluteFill,
|
|
82
|
+
{
|
|
83
|
+
backgroundColor: showOverlay
|
|
84
|
+
? `rgba(0,0,0,${overlayOpacity})`
|
|
85
|
+
: "transparent",
|
|
86
|
+
},
|
|
87
|
+
]}
|
|
88
|
+
/>
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
5
|
+
import { AtomicText } from "@umituz/react-native-design-system";
|
|
6
|
+
import { useOnboardingProvider } from "../providers/OnboardingProvider";
|
|
7
|
+
|
|
8
|
+
export interface OnboardingFooterProps {
|
|
9
|
+
currentIndex: number;
|
|
10
|
+
totalSlides: number;
|
|
11
|
+
isLastSlide: boolean;
|
|
12
|
+
onNext: () => void;
|
|
13
|
+
showProgressBar?: boolean;
|
|
14
|
+
showDots?: boolean;
|
|
15
|
+
showProgressText?: boolean;
|
|
16
|
+
nextButtonText?: string;
|
|
17
|
+
getStartedButtonText?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const OnboardingFooter = ({
|
|
22
|
+
currentIndex,
|
|
23
|
+
totalSlides,
|
|
24
|
+
isLastSlide,
|
|
25
|
+
onNext,
|
|
26
|
+
showProgressBar = true,
|
|
27
|
+
showDots = true,
|
|
28
|
+
showProgressText = true,
|
|
29
|
+
nextButtonText,
|
|
30
|
+
getStartedButtonText,
|
|
31
|
+
disabled = false,
|
|
32
|
+
}: OnboardingFooterProps) => {
|
|
33
|
+
const insets = useSafeAreaInsets();
|
|
34
|
+
const { t } = useLocalization();
|
|
35
|
+
const {
|
|
36
|
+
theme: { colors },
|
|
37
|
+
} = useOnboardingProvider();
|
|
38
|
+
|
|
39
|
+
const buttonText = isLastSlide
|
|
40
|
+
? getStartedButtonText || t("onboarding.getStarted")
|
|
41
|
+
: nextButtonText || t("general.continue");
|
|
42
|
+
|
|
43
|
+
const progressPercent = ((currentIndex + 1) / totalSlides) * 100;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View style={[styles.footer, { paddingBottom: insets.bottom + 24 }]}>
|
|
47
|
+
{showProgressBar && (
|
|
48
|
+
<View style={styles.progressContainer}>
|
|
49
|
+
<View style={[styles.progressBar, { backgroundColor: colors.progressBarBg }]}>
|
|
50
|
+
<View style={[styles.progressFill, { width: `${progressPercent}%`, backgroundColor: colors.progressFillColor }]} />
|
|
51
|
+
</View>
|
|
52
|
+
</View>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{showDots && (
|
|
56
|
+
<View style={styles.dots}>
|
|
57
|
+
{Array.from({ length: totalSlides }).map((_, index) => (
|
|
58
|
+
<View
|
|
59
|
+
key={index}
|
|
60
|
+
style={[
|
|
61
|
+
styles.dot,
|
|
62
|
+
{ backgroundColor: colors.dotColor },
|
|
63
|
+
index === currentIndex && {
|
|
64
|
+
width: 12,
|
|
65
|
+
backgroundColor: colors.activeDotColor
|
|
66
|
+
}
|
|
67
|
+
]}
|
|
68
|
+
/>
|
|
69
|
+
))}
|
|
70
|
+
</View>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<TouchableOpacity
|
|
74
|
+
onPress={onNext}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
activeOpacity={0.7}
|
|
77
|
+
style={[
|
|
78
|
+
styles.button,
|
|
79
|
+
{
|
|
80
|
+
backgroundColor: colors.buttonBg,
|
|
81
|
+
opacity: disabled ? 0.5 : 1,
|
|
82
|
+
},
|
|
83
|
+
]}
|
|
84
|
+
>
|
|
85
|
+
<AtomicText
|
|
86
|
+
type="labelLarge"
|
|
87
|
+
style={[styles.buttonText, { color: colors.buttonTextColor }]}
|
|
88
|
+
>
|
|
89
|
+
{buttonText}
|
|
90
|
+
</AtomicText>
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
|
|
93
|
+
{showProgressText && (
|
|
94
|
+
<AtomicText
|
|
95
|
+
type="labelSmall"
|
|
96
|
+
style={[styles.progressText, { color: colors.progressTextColor }]}
|
|
97
|
+
>
|
|
98
|
+
{currentIndex + 1} {t("general.of")} {totalSlides}
|
|
99
|
+
</AtomicText>
|
|
100
|
+
)}
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const styles = StyleSheet.create({
|
|
106
|
+
footer: {
|
|
107
|
+
paddingHorizontal: 24,
|
|
108
|
+
paddingTop: 24,
|
|
109
|
+
},
|
|
110
|
+
progressContainer: {
|
|
111
|
+
marginBottom: 20,
|
|
112
|
+
},
|
|
113
|
+
progressBar: {
|
|
114
|
+
height: 4,
|
|
115
|
+
borderRadius: 2,
|
|
116
|
+
overflow: "hidden",
|
|
117
|
+
},
|
|
118
|
+
progressFill: {
|
|
119
|
+
height: "100%",
|
|
120
|
+
borderRadius: 2,
|
|
121
|
+
},
|
|
122
|
+
dots: {
|
|
123
|
+
flexDirection: "row",
|
|
124
|
+
justifyContent: "center",
|
|
125
|
+
marginBottom: 24,
|
|
126
|
+
gap: 8,
|
|
127
|
+
},
|
|
128
|
+
dot: {
|
|
129
|
+
width: 6,
|
|
130
|
+
height: 6,
|
|
131
|
+
borderRadius: 3,
|
|
132
|
+
},
|
|
133
|
+
progressText: {
|
|
134
|
+
marginTop: 12,
|
|
135
|
+
textAlign: "center",
|
|
136
|
+
},
|
|
137
|
+
button: {
|
|
138
|
+
width: "100%",
|
|
139
|
+
minHeight: 52,
|
|
140
|
+
borderRadius: 16,
|
|
141
|
+
alignItems: "center",
|
|
142
|
+
justifyContent: "center",
|
|
143
|
+
paddingVertical: 16,
|
|
144
|
+
paddingHorizontal: 24,
|
|
145
|
+
},
|
|
146
|
+
buttonText: {
|
|
147
|
+
fontWeight: "700",
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|