nx-react-native-cli 2.7.1 → 3.0.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.
Files changed (56) hide show
  1. package/lib/index.cjs +27 -27
  2. package/package.json +1 -1
  3. package/templates/shared/apps/mobile/src/app/index.tsx +13 -8
  4. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.component.tsx +134 -0
  5. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.types.ts +18 -0
  6. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert.service.ts +27 -0
  7. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/index.ts +3 -0
  8. package/templates/shared/apps/mobile/src/components/atoms/BottomSheet/bottom-sheet.component.tsx +14 -8
  9. package/templates/shared/apps/mobile/src/components/atoms/Button/button.component.tsx +1 -1
  10. package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/date-modal-input.component.tsx +69 -0
  11. package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/index.ts +1 -0
  12. package/templates/shared/apps/mobile/src/components/atoms/DatePicker/date-picker.component.tsx +44 -0
  13. package/templates/shared/apps/mobile/src/components/atoms/DatePicker/index.ts +1 -0
  14. package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/date-text-input.component.tsx +218 -0
  15. package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/index.ts +1 -0
  16. package/templates/shared/apps/mobile/src/components/atoms/Divider/divider-component.tsx +1 -1
  17. package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/gradient-background.component.tsx +45 -0
  18. package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/index.ts +1 -0
  19. package/templates/shared/apps/mobile/src/components/atoms/InputLayout/input-layout.component.tsx +12 -4
  20. package/templates/shared/apps/mobile/src/components/atoms/KeyboardAccessory/keyboard-accessory.component.tsx +6 -3
  21. package/templates/shared/apps/mobile/src/components/atoms/Modal/modal.component.tsx +2 -0
  22. package/templates/shared/apps/mobile/src/components/atoms/ScreenLoader/screen-loader.component.tsx +6 -1
  23. package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/index.ts +1 -0
  24. package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/select-dropdown.component.tsx +223 -0
  25. package/templates/shared/apps/mobile/src/components/atoms/Skeleton/skeleton.component.tsx +1 -1
  26. package/templates/shared/apps/mobile/src/components/atoms/TextInput/bottom-sheet-text-input.component.tsx +4 -3
  27. package/templates/shared/apps/mobile/src/components/atoms/TextInput/text-input.component.tsx +8 -4
  28. package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/index.ts +1 -0
  29. package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/theme-manager.component.tsx +27 -0
  30. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/index.ts +3 -0
  31. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.component.tsx +109 -0
  32. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.types.ts +10 -0
  33. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast.service.ts +27 -0
  34. package/templates/shared/apps/mobile/src/components/atoms/Typography/typography.component.tsx +1 -1
  35. package/templates/shared/apps/mobile/src/components/atoms/index.ts +8 -0
  36. package/templates/shared/apps/mobile/src/components/molecules/BackButton/back-button.component.tsx +1 -1
  37. package/templates/shared/apps/mobile/src/components/molecules/ScreenContainer/screen-container.component.tsx +2 -22
  38. package/templates/shared/apps/mobile/src/components/molecules/ScreenHeader/screen-header.component.tsx +2 -2
  39. package/templates/shared/apps/mobile/src/hooks/index.ts +1 -0
  40. package/templates/shared/apps/mobile/src/hooks/usePushNotifications.hook.ts +104 -0
  41. package/templates/shared/apps/mobile/src/hooks/useToggleDarkMode.hook.tsx +24 -2
  42. package/templates/shared/apps/mobile/src/icons/alert-triangle.svg +5 -0
  43. package/templates/shared/apps/mobile/src/icons/check-circle.svg +4 -0
  44. package/templates/shared/apps/mobile/src/icons/chevron-down.svg +1 -0
  45. package/templates/shared/apps/mobile/src/icons/chevron-right.svg +1 -0
  46. package/templates/shared/apps/mobile/src/icons/index.ts +18 -1
  47. package/templates/shared/apps/mobile/src/icons/info.svg +5 -0
  48. package/templates/shared/apps/mobile/src/icons/x-circle.svg +5 -0
  49. package/templates/shared/apps/mobile/src/routes/index.tsx +19 -14
  50. package/templates/shared/apps/mobile/src/screens/LandingScreen/landing.screen.tsx +232 -8
  51. package/templates/shared/apps/mobile/src/stores/local-storage.store.ts +9 -5
  52. package/templates/shared/apps/mobile/src/stores/theme.slice.ts +15 -0
  53. package/templates/shared/apps/mobile/src/stores/user.slice.ts +5 -1
  54. package/templates/shared/apps/mobile/src/tailwind/index.ts +3 -3
  55. package/templates/shared/apps/mobile/tailwind.config.js +14 -0
  56. package/templates/shared/patches/react-native-animatable+1.4.0.patch +71 -0
@@ -1,12 +1,9 @@
1
- import { useFocusEffect } from '@react-navigation/native';
2
1
  import React, { ReactElement } from 'react';
3
2
  import {
4
3
  NativeScrollEvent,
5
4
  NativeSyntheticEvent,
6
5
  Platform,
7
6
  RefreshControlProps,
8
- StatusBar,
9
- StatusBarStyle,
10
7
  StyleProp,
11
8
  View,
12
9
  ViewStyle,
@@ -15,20 +12,15 @@ import { ScrollView as RNScrollView } from 'react-native-gesture-handler';
15
12
  import { Edge, SafeAreaProviderProps, SafeAreaView } from 'react-native-safe-area-context';
16
13
 
17
14
  import { KeyboardAwareScrollView } from '@/components';
18
- import CONFIG from '@/config';
19
15
  import { tw } from '@/tailwind';
20
16
 
21
17
  type Props = SafeAreaProviderProps & {
22
- barStyle?: StatusBarStyle;
23
18
  scrollViewRef?: React.RefObject<RNScrollView | null>;
24
19
  containerStyle?: StyleProp<ViewStyle>;
25
20
  excludedEdges?: Edge[];
26
21
  extraBottomPadding?: number;
27
22
  hasScroll?: boolean;
28
23
  refreshControl?: ReactElement;
29
- shouldShowStatusBar?: boolean;
30
- shouldBeTranslucent?: boolean;
31
- statusBarColor?: string;
32
24
  useSafeAreaView?: boolean;
33
25
  onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
34
26
  };
@@ -43,7 +35,6 @@ const safeAreaViewEdges: Edge[] = Platform.select({
43
35
 
44
36
  export function ScreenContainer(props: Props) {
45
37
  const {
46
- barStyle = 'dark-content',
47
38
  children,
48
39
  containerStyle,
49
40
  excludedEdges = [],
@@ -52,20 +43,9 @@ export function ScreenContainer(props: Props) {
52
43
  onScroll,
53
44
  refreshControl,
54
45
  scrollViewRef,
55
- shouldBeTranslucent = false,
56
- shouldShowStatusBar = true,
57
- statusBarColor = 'transparent',
58
46
  style,
59
47
  useSafeAreaView = true,
60
48
  } = props;
61
- useFocusEffect(() => {
62
- StatusBar.setHidden(!shouldShowStatusBar);
63
- if (CONFIG.IS_ANDROID) {
64
- StatusBar.setBackgroundColor(statusBarColor);
65
- StatusBar.setTranslucent(!shouldBeTranslucent);
66
- StatusBar.setBarStyle(barStyle);
67
- }
68
- });
69
49
 
70
50
  const defaultContainerStyle = [
71
51
  defaultStyle,
@@ -76,7 +56,7 @@ export function ScreenContainer(props: Props) {
76
56
 
77
57
  if (!useSafeAreaView) {
78
58
  return (
79
- <View style={[tw`flex-1 bg-white`, style]}>
59
+ <View style={[tw`dark:bg-background flex-1 bg-white`, style]}>
80
60
  {hasScroll ? (
81
61
  <KeyboardAwareScrollView
82
62
  containerStyle={defaultContainerStyle}
@@ -99,7 +79,7 @@ export function ScreenContainer(props: Props) {
99
79
  : safeAreaViewEdges;
100
80
 
101
81
  return (
102
- <SafeAreaView edges={edges} style={[tw`flex-1 bg-white`, style]}>
82
+ <SafeAreaView edges={edges} style={[tw`dark:bg-background flex-1 bg-white`, style]}>
103
83
  {hasScroll ? (
104
84
  <KeyboardAwareScrollView
105
85
  containerStyle={defaultContainerStyle}
@@ -39,7 +39,7 @@ export function ScreenHeader(props: Props) {
39
39
  {extraActionComponent ? (
40
40
  extraActionComponent
41
41
  ) : (
42
- <GearIcon height={25} style={tw`text-black`} width={25} />
42
+ <GearIcon height={25} style={tw`dark:text-foreground text-gray-900`} width={25} />
43
43
  )}
44
44
  </View>
45
45
  </TouchableOpacity>
@@ -51,7 +51,7 @@ export function ScreenHeader(props: Props) {
51
51
  {hasBackButton && <BackButton style={tw`z-10`} onPress={onBackPress} />}
52
52
  <Typography
53
53
  style={[
54
- tw`text-primary-700 absolute inset-x-0 top-4 text-center text-xl font-medium`,
54
+ tw`dark:text-foreground absolute inset-x-0 top-4 text-center text-xl font-medium text-gray-900`,
55
55
  titleStyle,
56
56
  ]}
57
57
  >
@@ -4,6 +4,7 @@ export * from './useDebounce.hook';
4
4
  export * from './useGetLayoutHeight.hook';
5
5
  export * from './useGetLayoutWidth.hook';
6
6
  export * from './useNavigation.hook';
7
+ export * from './usePushNotifications.hook';
7
8
  export * from './useShakeAnimation.hook';
8
9
  export * from './useTextInputChangeFocus.hook';
9
10
  export * from './useThrottle.hook';
@@ -0,0 +1,104 @@
1
+ import { useEffect } from 'react';
2
+ import { AppState, AppStateStatus, Linking } from 'react-native';
3
+ import { checkNotifications, requestNotifications, RESULTS } from 'react-native-permissions';
4
+
5
+ import { Alert } from '@/components/atoms';
6
+ import { useLocalStorageStore } from '@/stores/local-storage.store';
7
+ import { devLog } from '@/utils/log.util';
8
+
9
+ function showEnableNotificationsAlert() {
10
+ Alert.info({
11
+ actions: [
12
+ { label: 'Later', variant: 'cancel' },
13
+ { label: 'Open Settings', onPress: () => Linking.openSettings() },
14
+ ],
15
+ message: 'Enable notifications to stay updated with important alerts and messages.',
16
+ title: 'Notifications Disabled',
17
+ });
18
+ }
19
+
20
+ function isGranted(status: string): boolean {
21
+ return status === RESULTS.GRANTED || status === RESULTS.LIMITED;
22
+ }
23
+
24
+ // TODO: Implement once @react-native-firebase/messaging is installed
25
+ async function registerFcmToken() {
26
+ devLog('registerFcmToken: Firebase messaging not configured yet');
27
+ }
28
+
29
+ // TODO: Implement once @react-native-firebase/messaging is installed
30
+ async function removeFcmToken() {
31
+ devLog('removeFcmToken: Firebase messaging not configured yet');
32
+ }
33
+
34
+ function syncPushNotificationState(status: string) {
35
+ useLocalStorageStore.getState().setPushNotificationsEnabled(isGranted(status));
36
+ }
37
+
38
+ export async function requestPushNotification() {
39
+ const { status } = await checkNotifications();
40
+ syncPushNotificationState(status);
41
+
42
+ if (isGranted(status)) {
43
+ await registerFcmToken();
44
+
45
+ return;
46
+ }
47
+
48
+ const { status: requestStatus } = await requestNotifications([]);
49
+
50
+ if (isGranted(requestStatus)) {
51
+ syncPushNotificationState(requestStatus);
52
+ await registerFcmToken();
53
+
54
+ return;
55
+ }
56
+
57
+ const isBlocked = status === RESULTS.BLOCKED || requestStatus === RESULTS.BLOCKED;
58
+ if (isBlocked) {
59
+ showEnableNotificationsAlert();
60
+ }
61
+ }
62
+
63
+ async function handleAppForeground() {
64
+ const { status } = await checkNotifications();
65
+ syncPushNotificationState(status);
66
+
67
+ if (!isGranted(status)) {
68
+ return;
69
+ }
70
+
71
+ await registerFcmToken();
72
+ }
73
+
74
+ export async function removeCurrentFcmToken(): Promise<void> {
75
+ try {
76
+ await removeFcmToken();
77
+ } catch (error) {
78
+ devLog('Failed to remove FCM token:', error);
79
+ }
80
+ }
81
+
82
+ export function usePushNotifications(isAuthenticated: boolean) {
83
+ useEffect(() => {
84
+ if (!isAuthenticated) {
85
+ return;
86
+ }
87
+
88
+ const handleAppStateChange = (nextState: AppStateStatus) => {
89
+ if (nextState !== 'active') {
90
+ return;
91
+ }
92
+
93
+ handleAppForeground().catch((error) => {
94
+ devLog('Failed to check notification permission on foreground:', error);
95
+ });
96
+ };
97
+
98
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
99
+
100
+ return () => {
101
+ subscription.remove();
102
+ };
103
+ }, [isAuthenticated]);
104
+ }
@@ -1,9 +1,31 @@
1
+ import { useCallback } from 'react';
1
2
  import { useAppColorScheme } from 'twrnc';
2
3
 
4
+ import { useLocalStorageStore } from '@/stores';
5
+ import { ColorScheme } from '@/stores/theme.slice';
3
6
  import { tw } from '@/tailwind';
4
7
 
5
8
  export function useToggleDarkMode() {
6
- const [, toggleColorScheme] = useAppColorScheme(tw);
9
+ const [colorScheme, , setTwColorScheme] = useAppColorScheme(tw);
10
+ const setStoredColorScheme = useLocalStorageStore((s) => s.setColorScheme);
7
11
 
8
- return toggleColorScheme;
12
+ const toggleColorScheme = useCallback(() => {
13
+ const next: ColorScheme = colorScheme === 'dark' ? 'light' : 'dark';
14
+ setTwColorScheme(next);
15
+ setStoredColorScheme(next);
16
+ }, [colorScheme, setTwColorScheme, setStoredColorScheme]);
17
+
18
+ const setColorScheme = useCallback(
19
+ (scheme: ColorScheme) => {
20
+ setTwColorScheme(scheme);
21
+ setStoredColorScheme(scheme);
22
+ },
23
+ [setTwColorScheme, setStoredColorScheme],
24
+ );
25
+
26
+ return {
27
+ colorScheme: (colorScheme ?? 'light') as ColorScheme,
28
+ setColorScheme,
29
+ toggleColorScheme,
30
+ };
9
31
  }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/>
3
+ <path d="M12 9v4"/>
4
+ <path d="M12 17h.01"/>
5
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
3
+ <path d="m9 11 3 3L22 4"/>
4
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
@@ -1,6 +1,23 @@
1
+ import AlertTriangleIcon from './alert-triangle.svg';
1
2
  import ArrowLeftIcon from './arrow-left.svg';
3
+ import CheckCircleIcon from './check-circle.svg';
4
+ import ChevronDownIcon from './chevron-down.svg';
5
+ import ChevronRightIcon from './chevron-right.svg';
2
6
  import CrossIcon from './cross.svg';
3
7
  import GearIcon from './gear.svg';
8
+ import InfoIcon from './info.svg';
4
9
  import KeyboardHideIcon from './keyboard-hide.svg';
10
+ import XCircleIcon from './x-circle.svg';
5
11
 
6
- export { ArrowLeftIcon, CrossIcon, GearIcon, KeyboardHideIcon };
12
+ export {
13
+ AlertTriangleIcon,
14
+ ArrowLeftIcon,
15
+ CheckCircleIcon,
16
+ ChevronDownIcon,
17
+ ChevronRightIcon,
18
+ CrossIcon,
19
+ GearIcon,
20
+ InfoIcon,
21
+ KeyboardHideIcon,
22
+ XCircleIcon,
23
+ };
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <circle cx="12" cy="12" r="10"/>
3
+ <path d="M12 16v-4"/>
4
+ <path d="M12 8h.01"/>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <circle cx="12" cy="12" r="10"/>
3
+ <path d="m15 9-6 6"/>
4
+ <path d="m9 9 6 6"/>
5
+ </svg>
@@ -1,14 +1,13 @@
1
- import { DefaultTheme, NavigationContainer, Theme } from '@react-navigation/native';
1
+ import { DarkTheme, DefaultTheme, NavigationContainer, Theme } from '@react-navigation/native';
2
2
  import { createNativeStackNavigator } from '@react-navigation/native-stack';
3
- import React, { useEffect } from 'react';
4
- import { StatusBar } from 'react-native';
3
+ import React, { useMemo } from 'react';
5
4
  import BootSplash from 'react-native-bootsplash';
6
5
 
7
- import CONFIG from '@/config';
8
6
  import { Routes } from '@/routes';
9
7
  import PrivateRoutes from '@/routes/privateRoutes';
10
8
  import PublicRoutes from '@/routes/publicRoutes';
11
9
  import { screenOptions } from '@/routes/screen-options';
10
+ import { useLocalStorageStore } from '@/stores';
12
11
  import { colors } from '@/tailwind';
13
12
 
14
13
  const RootStack = createNativeStackNavigator();
@@ -17,11 +16,19 @@ export const noAnimation = {
17
16
  animationEnabled: false,
18
17
  };
19
18
 
20
- const navigationTheme: Theme = {
19
+ const lightNavigationTheme: Theme = {
21
20
  ...DefaultTheme,
22
21
  colors: {
23
22
  ...DefaultTheme.colors,
24
- background: colors.gray[50],
23
+ background: colors.white,
24
+ },
25
+ };
26
+
27
+ const darkNavigationTheme: Theme = {
28
+ ...DarkTheme,
29
+ colors: {
30
+ ...DarkTheme.colors,
31
+ background: colors.background,
25
32
  },
26
33
  };
27
34
 
@@ -30,15 +37,13 @@ export default function ApplicationRoutes() {
30
37
  // const isUserAuthenticated = useLocalStorageState((state) => !!state.accessToken);
31
38
  const isUserAuthenticated = false;
32
39
  const initialRouteName = isUserAuthenticated ? Routes.PRIVATE : Routes.PUBLIC;
40
+ const colorScheme = useLocalStorageStore((s) => s.colorScheme);
41
+ const isDark = colorScheme === 'dark';
33
42
 
34
- useEffect(() => {
35
- StatusBar.setHidden(false);
36
- if (CONFIG.IS_ANDROID) {
37
- StatusBar.setBackgroundColor('transparent');
38
- StatusBar.setTranslucent(true);
39
- StatusBar.setBarStyle('dark-content');
40
- }
41
- }, []);
43
+ const navigationTheme = useMemo(
44
+ () => (isDark ? darkNavigationTheme : lightNavigationTheme),
45
+ [isDark],
46
+ );
42
47
 
43
48
  // if (isLoading) {
44
49
  // return <ScreenLoader />;
@@ -1,17 +1,241 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
+ import { Image, View } from 'react-native';
2
3
 
3
- import { ScreenContainer, ScreenHeader } from '@/components';
4
+ import {
5
+ Alert,
6
+ BottomSheet,
7
+ Button,
8
+ DateModalInput,
9
+ DateTextInput,
10
+ Divider,
11
+ InputLayout,
12
+ Modal,
13
+ OutlinedButton,
14
+ ScreenContainer,
15
+ ScreenHeader,
16
+ SelectDropdown,
17
+ Skeleton,
18
+ TextInput,
19
+ Toast,
20
+ Typography,
21
+ useBottomSheet,
22
+ useModal,
23
+ } from '@/components';
24
+ import { requestPushNotification, useToggleDarkMode } from '@/hooks';
4
25
  import { PublicScreenProps, Screens } from '@/routes';
5
- import { toast } from '@/utils';
26
+ import { tw } from '@/tailwind';
6
27
 
7
- export function LandingScreen(props: PublicScreenProps<Screens.LANDING>) {
8
- function toastHi() {
9
- toast('Hi!');
10
- }
28
+ const DROPDOWN_OPTIONS = [
29
+ { id: '1', label: 'React Native' },
30
+ { id: '2', label: 'Flutter' },
31
+ { id: '3', label: 'SwiftUI' },
32
+ { id: '4', label: 'Jetpack Compose' },
33
+ ];
34
+
35
+ function SectionTitle({ title }: { title: string }) {
36
+ return <Typography style={tw`mb-2 mt-6 text-lg font-semibold`}>{title}</Typography>;
37
+ }
38
+
39
+ export function LandingScreen(_props: PublicScreenProps<Screens.LANDING>) {
40
+ const [textValue, setTextValue] = useState('');
41
+ const [dateModalValue, setDateModalValue] = useState<Date | undefined>();
42
+ const [dateTextValue, setDateTextValue] = useState<Date | undefined>();
43
+ const [selectedDropdown, setSelectedDropdown] = useState<string | undefined>();
44
+ const { hideModal, isVisible: isModalVisible, showModal } = useModal();
45
+ const { closeSheet, expandSheet, sheetRef } = useBottomSheet();
46
+ const { colorScheme, toggleColorScheme } = useToggleDarkMode();
47
+ const isDark = colorScheme === 'dark';
11
48
 
12
49
  return (
13
50
  <ScreenContainer>
14
- <ScreenHeader title="Landing" onExtraActionPress={toastHi} />
51
+ <ScreenHeader hasBackButton={false} title="Component Showcase" />
52
+ <View style={tw`px-4 pb-8`}>
53
+ {/* Logo */}
54
+ <View style={tw`mt-4 items-center`}>
55
+ <Image source={require('@/assets/images/logo.png')} style={{ height: 80, width: 80 }} />
56
+ </View>
57
+
58
+ {/* Theme Toggle */}
59
+ <SectionTitle title="Theme" />
60
+ <Button
61
+ title={isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
62
+ onPress={toggleColorScheme}
63
+ />
64
+
65
+ <Button
66
+ style={tw`mt-3`}
67
+ title="Request Push Notifications"
68
+ onPress={() => requestPushNotification()}
69
+ />
70
+
71
+ <Divider style={tw`mt-4`} />
72
+
73
+ {/* Typography */}
74
+ <SectionTitle title="Typography" />
75
+ <Typography style={tw`text-2xl font-bold`}>Heading Bold</Typography>
76
+ <Typography style={tw`text-lg font-semibold`}>Subheading Semibold</Typography>
77
+ <Typography style={tw`text-base`}>Body text regular</Typography>
78
+ <Typography style={tw`text-sm text-gray-500`}>Caption text</Typography>
79
+
80
+ <Divider style={tw`mt-4`} />
81
+
82
+ {/* Buttons */}
83
+ <SectionTitle title="Buttons" />
84
+ <View style={tw`gap-3`}>
85
+ <Button title="Primary Button" onPress={() => {}} />
86
+ <Button isLoading title="Loading Button" onPress={() => {}} />
87
+ <Button isDisabled title="Disabled Button" onPress={() => {}} />
88
+ <OutlinedButton title="Outlined Button" onPress={() => {}} />
89
+ </View>
90
+
91
+ <Divider style={tw`mt-4`} />
92
+
93
+ {/* TextInput */}
94
+ <SectionTitle title="Text Input" />
95
+ <View style={tw`gap-3`}>
96
+ <InputLayout isRequired label="Name">
97
+ <TextInput
98
+ placeholder="Enter your name"
99
+ value={textValue}
100
+ onChangeText={setTextValue}
101
+ />
102
+ </InputLayout>
103
+ <InputLayout error="This field is required" label="With Error">
104
+ <TextInput placeholder="Something went wrong" value="" onChangeText={() => {}} />
105
+ </InputLayout>
106
+ </View>
107
+
108
+ <Divider style={tw`mt-4`} />
109
+
110
+ {/* Select Dropdown */}
111
+ <SectionTitle title="Select Dropdown" />
112
+ <InputLayout label="Framework">
113
+ <SelectDropdown
114
+ options={DROPDOWN_OPTIONS}
115
+ placeholder="Choose a framework"
116
+ selectedId={selectedDropdown}
117
+ onSelect={setSelectedDropdown}
118
+ />
119
+ </InputLayout>
120
+
121
+ <Divider style={tw`mt-4`} />
122
+
123
+ {/* Date Inputs */}
124
+ <SectionTitle title="Date Modal Input" />
125
+ <InputLayout label="Birthday">
126
+ <DateModalInput
127
+ maximumDate={new Date()}
128
+ placeholder="Pick a date"
129
+ value={dateModalValue}
130
+ onChange={setDateModalValue}
131
+ />
132
+ </InputLayout>
133
+
134
+ <SectionTitle title="Date Text Input" />
135
+ <InputLayout label="Date of Birth">
136
+ <DateTextInput value={dateTextValue} onChange={setDateTextValue} />
137
+ </InputLayout>
138
+
139
+ <Divider style={tw`mt-4`} />
140
+
141
+ {/* Skeleton */}
142
+ <SectionTitle title="Skeleton" />
143
+ <View style={tw`gap-2`}>
144
+ <Skeleton isLoading>
145
+ <View style={tw`h-5 w-3/4 rounded`} />
146
+ </Skeleton>
147
+ <Skeleton isLoading>
148
+ <View style={tw`h-5 w-1/2 rounded`} />
149
+ </Skeleton>
150
+ <Skeleton isLoading>
151
+ <View style={tw`h-10 w-full rounded-xl`} />
152
+ </Skeleton>
153
+ </View>
154
+
155
+ <Divider style={tw`mt-4`} />
156
+
157
+ {/* Alert */}
158
+ <SectionTitle title="Alerts" />
159
+ <View style={tw`gap-3`}>
160
+ <Button
161
+ title="Show Error Alert"
162
+ onPress={() =>
163
+ Alert.error({
164
+ message: 'Something went wrong. Please try again.',
165
+ title: 'Request Failed',
166
+ })
167
+ }
168
+ />
169
+ <Button
170
+ title="Show Success Alert"
171
+ onPress={() =>
172
+ Alert.success({
173
+ title: 'Profile Updated',
174
+ })
175
+ }
176
+ />
177
+ <Button
178
+ title="Show Warning Alert"
179
+ onPress={() =>
180
+ Alert.warning({
181
+ actions: [
182
+ { label: 'Cancel', variant: 'cancel' },
183
+ { label: 'Delete', onPress: () => {} },
184
+ ],
185
+ message: 'This action cannot be undone.',
186
+ title: 'Delete Account',
187
+ })
188
+ }
189
+ />
190
+ </View>
191
+
192
+ <Divider style={tw`mt-4`} />
193
+
194
+ {/* Toast */}
195
+ <SectionTitle title="Toasts" />
196
+ <View style={tw`gap-3`}>
197
+ <Button title="Success Toast" onPress={() => Toast.success('Profile saved')} />
198
+ <Button title="Error Toast" onPress={() => Toast.error('Something went wrong')} />
199
+ <Button title="Info Toast" onPress={() => Toast.info('New version available')} />
200
+ <Button title="Warning Toast" onPress={() => Toast.warning('Check your connection')} />
201
+ </View>
202
+
203
+ <Divider style={tw`mt-4`} />
204
+
205
+ {/* Modal */}
206
+ <SectionTitle title="Modal" />
207
+ <Button title="Open Modal" onPress={showModal} />
208
+ <Modal isVisible={isModalVisible} onBackButtonPress={hideModal} onBackdropPress={hideModal}>
209
+ <View pointerEvents="box-none" style={tw`flex-1 items-center justify-center`}>
210
+ <View style={tw`dark:bg-sheet mx-8 w-[85%] rounded-2xl bg-white p-6`}>
211
+ <Typography style={tw`text-lg font-semibold`}>Modal Title</Typography>
212
+ <Typography style={tw`dark:text-subtitle mt-2 text-sm text-gray-500`}>
213
+ This is a custom modal with any content you want.
214
+ </Typography>
215
+ <Button style={tw`mt-4`} title="Close" onPress={hideModal} />
216
+ </View>
217
+ </View>
218
+ </Modal>
219
+
220
+ <Divider style={tw`mt-4`} />
221
+
222
+ {/* Bottom Sheet */}
223
+ <SectionTitle title="Bottom Sheet" />
224
+ <Button title="Open Bottom Sheet" onPress={expandSheet} />
225
+ <BottomSheet
226
+ backgroundStyle={tw`dark:bg-sheet bg-white`}
227
+ sheetRef={sheetRef}
228
+ snapPoints={['40%']}
229
+ >
230
+ <View style={tw`p-6`}>
231
+ <Typography style={tw`text-lg font-semibold`}>Bottom Sheet</Typography>
232
+ <Typography style={tw`dark:text-subtitle mt-2 text-sm text-gray-500`}>
233
+ Swipe down or tap the backdrop to dismiss.
234
+ </Typography>
235
+ <Button style={tw`mt-4`} title="Close" onPress={closeSheet} />
236
+ </View>
237
+ </BottomSheet>
238
+ </View>
15
239
  </ScreenContainer>
16
240
  );
17
241
  }
@@ -1,15 +1,17 @@
1
1
  import { StateCreator, create } from 'zustand';
2
2
  import { PersistOptions, createJSONStorage, persist } from 'zustand/middleware';
3
3
 
4
+ import { createThemeSlice, ThemeSlice } from './theme.slice';
4
5
  import { createUserSlice, UserSlice } from './user.slice';
5
6
 
6
7
  import { MmkvStorage } from '@/stores/mmkvStorage';
7
8
 
8
- export type LocalStorageStore = UserSlice & {
9
- _hasHydrated: boolean;
10
- setHasHydrated: (hasHydrated: boolean) => void;
11
- clear: () => void;
12
- };
9
+ export type LocalStorageStore = UserSlice &
10
+ ThemeSlice & {
11
+ _hasHydrated: boolean;
12
+ setHasHydrated: (hasHydrated: boolean) => void;
13
+ clear: () => void;
14
+ };
13
15
 
14
16
  type MyPersist = (
15
17
  config: StateCreator<LocalStorageStore>,
@@ -21,9 +23,11 @@ export const useLocalStorageStore = create<LocalStorageStore, []>(
21
23
  (set, get, store) =>
22
24
  (() => {
23
25
  const userSlice = createUserSlice(set, get, store);
26
+ const themeSlice = createThemeSlice(set, get, store);
24
27
 
25
28
  return {
26
29
  ...userSlice,
30
+ ...themeSlice,
27
31
  _hasHydrated: false,
28
32
  clear: () => {
29
33
  userSlice.signOut();
@@ -0,0 +1,15 @@
1
+ import { StateCreator } from 'zustand';
2
+
3
+ export type ColorScheme = 'light' | 'dark';
4
+
5
+ export type ThemeSlice = {
6
+ colorScheme: ColorScheme;
7
+ setColorScheme: (scheme: ColorScheme) => void;
8
+ toggleColorScheme: () => void;
9
+ };
10
+
11
+ export const createThemeSlice: StateCreator<ThemeSlice> = (set, get) => ({
12
+ colorScheme: 'light',
13
+ setColorScheme: (scheme: ColorScheme) => set({ colorScheme: scheme }),
14
+ toggleColorScheme: () => set({ colorScheme: get().colorScheme === 'light' ? 'dark' : 'light' }),
15
+ });