@umituz/react-native-design-system 2.6.128 → 2.8.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/molecules/navigation/utils/AppNavigation.ts +53 -28
- package/src/molecules/splash/hooks/useSplashFlow.ts +33 -7
- 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,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Helpers
|
|
3
|
+
* Single Responsibility: Common storage operations and error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
7
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_STORAGE_KEY = "@onboarding:completed";
|
|
10
|
+
export const USER_DATA_STORAGE_KEY = "@onboarding:user_data";
|
|
11
|
+
|
|
12
|
+
export async function loadCompletionStatus(storageKey: string): Promise<boolean> {
|
|
13
|
+
const result = await storageRepository.getString(storageKey, "false");
|
|
14
|
+
return unwrap(result, "false") === "true";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadUserData(
|
|
18
|
+
defaultData: OnboardingUserData
|
|
19
|
+
): Promise<OnboardingUserData> {
|
|
20
|
+
const result = await storageRepository.getItem<OnboardingUserData>(
|
|
21
|
+
USER_DATA_STORAGE_KEY,
|
|
22
|
+
defaultData
|
|
23
|
+
);
|
|
24
|
+
return unwrap(result, defaultData);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveCompletionStatus(storageKey: string): Promise<void> {
|
|
28
|
+
const result = await storageRepository.setString(storageKey, "true");
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
throw new Error("Failed to save completion status to storage");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function saveUserData(data: OnboardingUserData): Promise<void> {
|
|
35
|
+
const result = await storageRepository.setItem(USER_DATA_STORAGE_KEY, data);
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
throw new Error("Failed to save user data to storage");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function removeStorageKeys(storageKey: string): Promise<void> {
|
|
42
|
+
await storageRepository.removeItem(storageKey);
|
|
43
|
+
await storageRepository.removeItem(USER_DATA_STORAGE_KEY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function handleError(error: unknown, context: string): string {
|
|
47
|
+
const message = error instanceof Error ? error.message : `Failed to ${context}`;
|
|
48
|
+
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
console.error(`[OnboardingStore] ${context} error:`, error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return message;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function logSuccess(message: string): void {
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.log(`[OnboardingStore] ${message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for background-related operations
|
|
5
|
+
* Follows Single Responsibility Principle
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if slide should use custom background or overlay
|
|
12
|
+
* Indicates that we are not using the default theme background.
|
|
13
|
+
*
|
|
14
|
+
* @param slide - The slide to check
|
|
15
|
+
* @param globalUseCustomBackground - Global option from OnboardingOptions
|
|
16
|
+
* @returns true if custom background/media should be used, false otherwise
|
|
17
|
+
*/
|
|
18
|
+
export function shouldUseCustomBackground(
|
|
19
|
+
slide: OnboardingSlide | undefined,
|
|
20
|
+
globalUseCustomBackground?: boolean
|
|
21
|
+
): boolean {
|
|
22
|
+
if (!slide) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If there is background media, we always treat it as custom (needs overlay for readability)
|
|
27
|
+
if (slide.backgroundImage || slide.backgroundVideo || (slide.backgroundImages && slide.backgroundImages.length > 0)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If global custom background is enabled, use it if slide has color defined
|
|
32
|
+
if (globalUseCustomBackground === true) {
|
|
33
|
+
return !!slide.backgroundColor;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Otherwise, check slide's own useCustomBackground prop
|
|
37
|
+
return slide.useCustomBackground === true && !!slide.backgroundColor;
|
|
38
|
+
}
|
|
@@ -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, ImageSourceType } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateCollageLayout = (
|
|
10
|
+
images: ImageSourceType[],
|
|
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,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
|
+
};
|