@imksh/create 1.0.31 → 1.0.33

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 (158) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/commands/create.js +2 -0
  4. package/src/index.js +1 -1
  5. package/templates/layers/next/javascript/package.json +8 -2
  6. package/templates/layers/next/typescript/.eslintrc.json +12 -0
  7. package/templates/layers/next/typescript/package.json +14 -3
  8. package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -1
  9. package/templates/layers/next/typescript/src/components/common/LottieIcon.tsx +2 -2
  10. package/templates/layers/next/typescript/src/components/common/Table.tsx +4 -4
  11. package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +2 -1
  12. package/templates/layers/next/typescript/src/components/ui/Header.tsx +2 -2
  13. package/templates/layers/next/typescript/src/hooks/usePwaInstall.ts +10 -4
  14. package/templates/layers/next/typescript/src/services/authService.ts +1 -1
  15. package/templates/layers/next/typescript/src/store/useAuthStore.ts +24 -9
  16. package/templates/layers/node/javascript/.env.example +3 -1
  17. package/templates/layers/node/javascript/package.json +12 -3
  18. package/templates/layers/node/javascript/src/app.js +2 -1
  19. package/templates/layers/node/javascript/src/controllers/auth.controller.js +106 -168
  20. package/templates/layers/node/javascript/src/controllers/push.controller.js +51 -58
  21. package/templates/layers/node/javascript/src/controllers/upload.controller.js +12 -19
  22. package/templates/layers/node/javascript/src/middleware/auth.middleware.js +5 -11
  23. package/templates/layers/node/javascript/src/middleware/errorHandler.js +6 -4
  24. package/templates/layers/node/javascript/src/routes/auth.route.js +2 -0
  25. package/templates/layers/node/javascript/src/services/auth.service.js +159 -164
  26. package/templates/layers/node/javascript/src/utils/ApiError.js +20 -0
  27. package/templates/layers/node/javascript/src/utils/SuccessResponse.js +15 -0
  28. package/templates/layers/node/javascript/src/utils/apiResponse.js +4 -0
  29. package/templates/layers/node/javascript/src/utils/asyncHandler.js +5 -0
  30. package/templates/layers/node/typescript/.env.example +3 -1
  31. package/templates/layers/node/typescript/.eslintrc.cjs +28 -0
  32. package/templates/layers/node/typescript/package.json +16 -2
  33. package/templates/layers/node/typescript/src/app.ts +3 -2
  34. package/templates/layers/node/typescript/src/config/db.ts +3 -2
  35. package/templates/layers/node/typescript/src/config/socket.ts +5 -4
  36. package/templates/layers/node/typescript/src/controllers/auth.controller.ts +104 -132
  37. package/templates/layers/node/typescript/src/controllers/push.controller.ts +52 -59
  38. package/templates/layers/node/typescript/src/controllers/upload.controller.ts +12 -19
  39. package/templates/layers/node/typescript/src/middleware/auth.middleware.ts +13 -7
  40. package/templates/layers/node/typescript/src/middleware/errorHandler.ts +18 -5
  41. package/templates/layers/node/typescript/src/middleware/rateLimiter.ts +2 -2
  42. package/templates/layers/node/typescript/src/routes/auth.route.ts +2 -0
  43. package/templates/layers/node/typescript/src/services/auth.service.ts +86 -43
  44. package/templates/layers/node/typescript/src/types/express.d.ts +5 -3
  45. package/templates/layers/node/typescript/src/utils/ApiError.ts +25 -0
  46. package/templates/layers/node/typescript/src/utils/SuccessResponse.ts +17 -0
  47. package/templates/layers/node/typescript/src/utils/apiResponse.ts +4 -0
  48. package/templates/layers/node/typescript/src/utils/asyncHandler.ts +13 -0
  49. package/templates/layers/node/typescript/src/utils/cloudinaryUpload.ts +18 -7
  50. package/templates/layers/node/typescript/src/utils/expoNotification.ts +1 -1
  51. package/templates/layers/node/typescript/src/utils/genOtpToken.ts +7 -1
  52. package/templates/layers/node/typescript/src/utils/jwt.ts +8 -2
  53. package/templates/layers/node/typescript/src/utils/logger.ts +2 -1
  54. package/templates/layers/node/typescript/src/utils/pushHelper.ts +13 -9
  55. package/templates/layers/node-prisma/javascript/.env.example +3 -1
  56. package/templates/layers/node-prisma/javascript/package.json +11 -2
  57. package/templates/layers/node-prisma/javascript/src/app.js +2 -1
  58. package/templates/layers/node-prisma/javascript/src/controllers/auth.controller.js +90 -135
  59. package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +64 -76
  60. package/templates/layers/node-prisma/javascript/src/controllers/upload.controller.js +12 -19
  61. package/templates/layers/node-prisma/javascript/src/middleware/auth.middleware.js +5 -4
  62. package/templates/layers/node-prisma/javascript/src/middleware/errorHandler.js +6 -4
  63. package/templates/layers/node-prisma/javascript/src/routes/auth.route.js +2 -0
  64. package/templates/layers/node-prisma/javascript/src/services/auth.service.js +145 -122
  65. package/templates/layers/node-prisma/javascript/src/utils/ApiError.js +20 -0
  66. package/templates/layers/node-prisma/javascript/src/utils/SuccessResponse.js +15 -0
  67. package/templates/layers/node-prisma/javascript/src/utils/apiResponse.js +4 -0
  68. package/templates/layers/node-prisma/javascript/src/utils/asyncHandler.js +5 -0
  69. package/templates/layers/node-prisma/typescript/.env.example +3 -1
  70. package/templates/layers/node-prisma/typescript/.eslintrc.cjs +28 -0
  71. package/templates/layers/node-prisma/typescript/package.json +19 -5
  72. package/templates/layers/node-prisma/typescript/src/app.ts +3 -2
  73. package/templates/layers/node-prisma/typescript/src/config/prisma.ts +3 -2
  74. package/templates/layers/node-prisma/typescript/src/config/socket.ts +5 -4
  75. package/templates/layers/node-prisma/typescript/src/controllers/auth.controller.ts +89 -105
  76. package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -83
  77. package/templates/layers/node-prisma/typescript/src/controllers/upload.controller.ts +12 -19
  78. package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +9 -13
  79. package/templates/layers/node-prisma/typescript/src/middleware/errorHandler.ts +18 -5
  80. package/templates/layers/node-prisma/typescript/src/middleware/rateLimiter.ts +2 -2
  81. package/templates/layers/node-prisma/typescript/src/routes/auth.route.ts +2 -0
  82. package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +83 -31
  83. package/templates/layers/node-prisma/typescript/src/types/express.d.ts +5 -3
  84. package/templates/layers/node-prisma/typescript/src/utils/ApiError.ts +25 -0
  85. package/templates/layers/node-prisma/typescript/src/utils/SuccessResponse.ts +17 -0
  86. package/templates/layers/node-prisma/typescript/src/utils/apiResponse.ts +4 -0
  87. package/templates/layers/node-prisma/typescript/src/utils/asyncHandler.ts +13 -0
  88. package/templates/layers/node-prisma/typescript/src/utils/cloudinaryUpload.ts +18 -7
  89. package/templates/layers/node-prisma/typescript/src/utils/genOtpToken.ts +7 -1
  90. package/templates/layers/node-prisma/typescript/src/utils/jwt.ts +8 -2
  91. package/templates/layers/node-prisma/typescript/src/utils/logger.ts +2 -1
  92. package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +10 -6
  93. package/templates/layers/prisma/javascript/package.json +8 -0
  94. package/templates/layers/prisma/typescript/package.json +14 -1
  95. package/templates/layers/prisma/typescript/src/config/db.ts +3 -2
  96. package/templates/layers/react/javascript/package.json +6 -0
  97. package/templates/layers/react/typescript/.eslintrc.cjs +20 -0
  98. package/templates/layers/react/typescript/package.json +11 -2
  99. package/templates/layers/react/typescript/src/components/common/Table.tsx +4 -4
  100. package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +1 -1
  101. package/templates/layers/react/typescript/src/main.tsx +11 -11
  102. package/templates/layers/react/typescript/src/services/authService.ts +1 -1
  103. package/templates/layers/react/typescript/src/store/useAuthStore.ts +24 -9
  104. package/templates/layers/react-native/base/app.json +9 -22
  105. package/templates/layers/react-native/base/eslint.config.js +5 -1
  106. package/templates/layers/react-native/base/package.json +40 -33
  107. package/templates/layers/react-native/base/src/app/(auth)/edit-profile.tsx +7 -7
  108. package/templates/layers/react-native/base/src/app/(auth)/login.tsx +31 -7
  109. package/templates/layers/react-native/base/src/app/(tabs)/_layout.tsx +3 -3
  110. package/templates/layers/react-native/base/src/app/(tabs)/index.tsx +6 -5
  111. package/templates/layers/react-native/base/src/app/_layout.tsx +4 -8
  112. package/templates/layers/react-native/base/src/app/screens/doc-viewer.tsx +378 -0
  113. package/templates/layers/react-native/base/src/components/common/ConfirmationToast.tsx +58 -40
  114. package/templates/layers/react-native/base/src/components/common/GoogleAuthButton.tsx +123 -0
  115. package/templates/layers/react-native/base/src/components/common/LocationLoader.tsx +64 -9
  116. package/templates/layers/react-native/base/src/components/common/Typography.tsx +54 -19
  117. package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +3 -3
  118. package/templates/layers/react-native/base/src/components/ui/ClassicTabBar.tsx +15 -12
  119. package/templates/layers/react-native/base/src/components/ui/CustomTabBar.tsx +90 -91
  120. package/templates/layers/react-native/base/src/components/ui/Drawer.tsx +344 -0
  121. package/templates/layers/react-native/base/src/components/ui/Sidebar.tsx +4 -4
  122. package/templates/layers/react-native/base/src/components/ui/buttons/Button.tsx +29 -6
  123. package/templates/layers/react-native/base/src/components/ui/data-display/Accordion.tsx +2 -2
  124. package/templates/layers/react-native/base/src/components/ui/data-display/Badge.tsx +1 -2
  125. package/templates/layers/react-native/base/src/components/ui/data-display/Image.tsx +3 -3
  126. package/templates/layers/react-native/base/src/components/ui/data-display/SwipeableRow.tsx +28 -7
  127. package/templates/layers/react-native/base/src/components/ui/feedback/GestureWrapper.tsx +11 -4
  128. package/templates/layers/react-native/base/src/components/ui/inputs/DatePicker.tsx +3 -3
  129. package/templates/layers/react-native/base/src/components/ui/inputs/ImagePickerModal.tsx +45 -19
  130. package/templates/layers/react-native/base/src/components/ui/inputs/Input.tsx +28 -9
  131. package/templates/layers/react-native/base/src/components/ui/inputs/SearchablePicker.tsx +199 -0
  132. package/templates/layers/react-native/base/src/components/ui/inputs/Switch.tsx +7 -7
  133. package/templates/layers/react-native/base/src/components/ui/inputs/WebLocationPicker.tsx +146 -72
  134. package/templates/layers/react-native/base/src/config/api.ts +5 -5
  135. package/templates/layers/react-native/base/src/services/auth.service.ts +3 -1
  136. package/templates/layers/react-native/base/src/services/permission.service.ts +168 -44
  137. package/templates/layers/react-native/base/src/services/socket.service.ts +0 -7
  138. package/templates/layers/react-native/base/src/store/useAuthStore.ts +61 -16
  139. package/templates/layers/react-native/base/src/store/useNotificationStore.ts +36 -42
  140. package/templates/layers/react-native/base/src/types/images.d.ts +7 -5
  141. package/templates/layers/react-native/base/src/utils/env.ts +47 -0
  142. package/templates/layers/react-native/base/src/utils/fileDownload.ts +206 -0
  143. package/templates/layers/react-native/base/src/utils/storage.ts +1 -1
  144. package/templates/presets/production-fullstack/overrides/client/package.json +9 -0
  145. package/templates/presets/production-fullstack/overrides/mobile/app.json +2 -15
  146. package/templates/presets/production-fullstack/overrides/mobile/package.json +39 -29
  147. package/templates/presets/production-fullstack/overrides/mobile/src/app/_layout.tsx +3 -3
  148. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/Drawer.tsx +344 -0
  149. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/DatePicker.tsx +3 -3
  150. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/ImagePickerModal.tsx +3 -3
  151. package/templates/presets/production-fullstack/overrides/mobile/src/services/permission.service.ts +168 -44
  152. package/templates/presets/production-fullstack/overrides/mobile/src/store/useNotificationStore.ts +16 -0
  153. package/templates/presets/production-fullstack/overrides/mobile/src/utils/env.ts +47 -0
  154. package/templates/presets/production-fullstack/overrides/server/package.json +11 -2
  155. package/templates/layers/react-native/base/src/app/doc-viewer.tsx +0 -193
  156. package/templates/layers/react-native/base/src/components/ui/BottomModal.tsx +0 -176
  157. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/BottomModal.tsx +0 -159
  158. /package/templates/layers/react-native/base/assets/images/logo/{logoWithBg.png → icon.png} +0 -0
@@ -2,9 +2,10 @@ import React from "react";
2
2
  import { View } from "react-native";
3
3
  import * as ImagePicker from "expo-image-picker";
4
4
  import { Txt } from "@/components/common/Typography";
5
- import BottomModal from "@/components/ui/BottomModal";
5
+ import Drawer from "@/components/ui/Drawer";
6
6
  import Button from "@/components/ui/buttons/Button";
7
7
 
8
+
8
9
  export type ImagePickerSource = "camera" | "gallery" | "both";
9
10
 
10
11
  export interface ImagePickerResult {
@@ -23,6 +24,7 @@ export interface ImagePickerModalProps {
23
24
  subtitle?: string;
24
25
  allowsEditing?: boolean;
25
26
  allowsMultipleSelection?: boolean;
27
+ aspectRatio?: [number, number];
26
28
  }
27
29
 
28
30
  export default function ImagePickerModal({
@@ -34,9 +36,9 @@ export default function ImagePickerModal({
34
36
  title = "Select Image Source",
35
37
  subtitle = "Choose where to pick your image from",
36
38
  allowsEditing = false,
39
+ aspectRatio = [1, 1],
37
40
  allowsMultipleSelection = false,
38
41
  }: ImagePickerModalProps) {
39
-
40
42
  const launchPicker = async (type: "gallery" | "camera") => {
41
43
  onClose();
42
44
 
@@ -51,10 +53,10 @@ export default function ImagePickerModal({
51
53
 
52
54
  result = await ImagePicker.launchImageLibraryAsync({
53
55
  mediaTypes: ["images"],
54
- quality: 0.8,
56
+ quality: 0.6,
55
57
  allowsEditing: !allowsMultipleSelection && allowsEditing, // multi-select cannot have editing on some OS
56
58
  allowsMultipleSelection,
57
- aspect: allowsEditing ? [16, 9] : undefined, // Assuming a common aspect ratio if cropped
59
+ aspect: allowsEditing ? aspectRatio : undefined, // Assuming a common aspect ratio if cropped
58
60
  });
59
61
  } else {
60
62
  const perm = await ImagePicker.requestCameraPermissionsAsync();
@@ -62,23 +64,47 @@ export default function ImagePickerModal({
62
64
 
63
65
  result = await ImagePicker.launchCameraAsync({
64
66
  mediaTypes: ["images"],
65
- quality: 0.8,
67
+ quality: 0.6,
66
68
  allowsEditing,
67
- aspect: allowsEditing ? [16, 9] : undefined,
69
+ aspect: allowsEditing ? aspectRatio : undefined,
68
70
  });
69
71
  }
70
72
 
71
- if (result && !result.canceled && result.assets && result.assets.length > 0) {
72
- const mappedAssets = result.assets.map(asset => {
73
- const filename = asset.uri.split("/").pop() || "image.jpg";
74
- const match = /\.(\w+)$/.exec(filename);
75
- const mimeType = match ? `image/${match[1]}` : "image/jpeg";
76
- return {
77
- uri: asset.uri,
78
- name: filename,
79
- type: mimeType,
80
- };
81
- });
73
+ if (
74
+ result &&
75
+ !result.canceled &&
76
+ result.assets &&
77
+ result.assets.length > 0
78
+ ) {
79
+ const mappedAssets = await Promise.all(
80
+ result.assets.map(async (asset) => {
81
+ let finalUri = asset.uri;
82
+
83
+ // Resize image if it's too large to save bandwidth
84
+ if (asset.width > 1024 || asset.height > 1024) {
85
+ const resizeAction =
86
+ asset.width > asset.height
87
+ ? { resize: { width: 1024 } }
88
+ : { resize: { height: 1024 } };
89
+
90
+ const manipResult = await ImageManipulator.manipulateAsync(
91
+ asset.uri,
92
+ [resizeAction],
93
+ { compress: 0.7, format: ImageManipulator.SaveFormat.JPEG },
94
+ );
95
+ finalUri = manipResult.uri;
96
+ }
97
+
98
+ const filename = finalUri.split("/").pop() || "image.jpg";
99
+ const match = /\.(\w+)$/.exec(filename);
100
+ const mimeType = match ? `image/${match[1]}` : "image/jpeg";
101
+ return {
102
+ uri: finalUri,
103
+ name: filename,
104
+ type: mimeType,
105
+ };
106
+ }),
107
+ );
82
108
 
83
109
  if (allowsMultipleSelection && onImagesSelected) {
84
110
  onImagesSelected(mappedAssets);
@@ -93,7 +119,7 @@ export default function ImagePickerModal({
93
119
  };
94
120
 
95
121
  return (
96
- <BottomModal isOpen={isOpen} onClose={onClose}>
122
+ <Drawer isOpen={isOpen} onClose={onClose} position="bottom">
97
123
  <View className="p-6">
98
124
  <Txt variant="lg" className="font-bold mb-1 text-center">
99
125
  {title}
@@ -124,6 +150,6 @@ export default function ImagePickerModal({
124
150
  )}
125
151
  </View>
126
152
  </View>
127
- </BottomModal>
153
+ </Drawer>
128
154
  );
129
155
  }
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import { useState } from "react";
2
2
  import {
3
3
  View,
4
4
  TextInput,
@@ -16,11 +16,16 @@ export interface InputProps extends TextInputProps {
16
16
  leftIcon?: keyof typeof Ionicons.glyphMap;
17
17
  rightIcon?: keyof typeof Ionicons.glyphMap;
18
18
  onRightIconPress?: () => void;
19
+ placeHolder?: string;
20
+ style?: import("react-native").StyleProp<import("react-native").ViewStyle>;
19
21
  helperText?: string;
20
22
  containerStyle?: import("react-native").StyleProp<
21
23
  import("react-native").ViewStyle
22
24
  >;
23
25
  labelOnBorder?: boolean;
26
+ className?: string;
27
+ weight?: "normal" | "medium" | "bold" | "semibold" | "black";
28
+ size?: "sm" | "md" | "lg";
24
29
  }
25
30
 
26
31
  export default function Input({
@@ -31,8 +36,12 @@ export default function Input({
31
36
  onRightIconPress,
32
37
  helperText,
33
38
  style,
39
+ placeHolder = "",
34
40
  containerStyle,
35
41
  labelOnBorder = false,
42
+ className = "mb-2",
43
+ weight = "normal",
44
+ size = "md",
36
45
  ...props
37
46
  }: InputProps) {
38
47
  const { colorScheme } = useColorScheme();
@@ -45,13 +54,22 @@ export default function Input({
45
54
  ? theme.primary
46
55
  : theme.base300;
47
56
 
57
+ const inputHeight = size === "sm" ? 40 : size === "lg" ? 64 : 52;
58
+ const inputFontSize = size === "sm" ? 14 : size === "lg" ? 18 : 16;
59
+ const iconSize = size === "sm" ? 16 : size === "lg" ? 24 : 20;
60
+ const labelSize = size === "sm" ? "xs" : size === "lg" ? "lg" : "base";
48
61
  return (
49
- <View className="mb-4 relative">
62
+ <View
63
+ className={`${className} relative ${props.editable === false ? "opacity-50" : ""}`}
64
+ >
50
65
  {!labelOnBorder && label && (
51
66
  <Txt
52
67
  color={theme.baseContent}
53
- weight="semibold"
54
68
  style={{ marginBottom: 6 }}
69
+ weight={weight}
70
+ variant={labelSize}
71
+ adjustsFontSizeToFit
72
+ numberOfLines={1}
55
73
  >
56
74
  {label} {error ? "*" : ""}{" "}
57
75
  </Txt>
@@ -63,7 +81,7 @@ export default function Input({
63
81
  position: "absolute",
64
82
  top: -6,
65
83
  left: 12,
66
- backgroundColor: theme.base100,
84
+ backgroundColor: theme.base200,
67
85
  paddingHorizontal: 6,
68
86
  zIndex: 10,
69
87
  }}
@@ -85,12 +103,12 @@ export default function Input({
85
103
  {
86
104
  flexDirection: "row",
87
105
  alignItems: "center",
88
- backgroundColor: theme.base100,
106
+ backgroundColor: theme.base200,
89
107
  borderWidth: 1,
90
108
  borderColor,
91
109
  borderRadius: 12,
92
110
  paddingHorizontal: 12,
93
- height: 52,
111
+ height: inputHeight,
94
112
  },
95
113
  containerStyle,
96
114
  ]}
@@ -98,7 +116,7 @@ export default function Input({
98
116
  {leftIcon && (
99
117
  <Ionicons
100
118
  name={leftIcon}
101
- size={20}
119
+ size={iconSize}
102
120
  color={isFocused ? theme.primary : theme.secondary}
103
121
  style={{ marginRight: 8 }}
104
122
  />
@@ -109,12 +127,13 @@ export default function Input({
109
127
  {
110
128
  flex: 1,
111
129
  color: theme.baseContent,
112
- fontSize: 16,
130
+ fontSize: inputFontSize,
113
131
  height: "100%",
114
132
  },
115
133
  style,
116
134
  ]}
117
135
  placeholderTextColor={theme.secondary}
136
+ placeholder={placeHolder}
118
137
  onFocus={(e) => {
119
138
  setIsFocused(true);
120
139
  props.onFocus?.(e);
@@ -133,7 +152,7 @@ export default function Input({
133
152
  >
134
153
  <Ionicons
135
154
  name={rightIcon}
136
- size={20}
155
+ size={iconSize}
137
156
  color={theme.secondary}
138
157
  style={{ marginLeft: 8 }}
139
158
  />
@@ -0,0 +1,199 @@
1
+ import React, { useState, useMemo } from "react";
2
+ import {
3
+ View,
4
+ TouchableOpacity,
5
+ Modal,
6
+ FlatList,
7
+ ActivityIndicator,
8
+ } from "react-native";
9
+ import { Txt } from "../../common/Typography";
10
+ import { Ionicons } from "@expo/vector-icons";
11
+ import { useColorScheme } from "nativewind";
12
+ import { Colors } from "../../../constants/Colors";
13
+ import ScreenHeader from "@/components/common/ScreenHeader";
14
+ import SearchBar from "./SearchBar";
15
+
16
+ interface SearchablePickerProps<T> {
17
+ label?: string;
18
+ data: T[];
19
+ value: string;
20
+ onSelect: (item: T) => void;
21
+ labelExtractor: (item: T) => string;
22
+ valueExtractor: (item: T) => string;
23
+ placeholder?: string;
24
+ isLoading?: boolean;
25
+ size?: "xs" | "sm" | "md" | "lg";
26
+ className?: string;
27
+ weight?: "normal" | "medium" | "bold" | "semibold" | "black";
28
+ renderTrigger?: (
29
+ onPress: () => void,
30
+ selectedItem: T | undefined,
31
+ ) => React.ReactNode;
32
+ onSearchChange?: (text: string) => void;
33
+ onEndReached?: () => void;
34
+ disableLocalFilter?: boolean;
35
+ renderOption?: (item: T, isSelected: boolean) => React.ReactNode;
36
+ }
37
+ export default function SearchablePicker<T>({
38
+ label,
39
+ data,
40
+ value,
41
+ onSelect,
42
+ labelExtractor,
43
+ valueExtractor,
44
+ placeholder = "Select an option",
45
+ isLoading = false,
46
+ size = "md",
47
+ className = "mb-4",
48
+ weight = "normal",
49
+ renderTrigger,
50
+ onSearchChange,
51
+ onEndReached,
52
+ disableLocalFilter = false,
53
+ renderOption,
54
+ }: SearchablePickerProps<T>) {
55
+ const { colorScheme } = useColorScheme();
56
+ const theme = Colors[colorScheme ?? "light"];
57
+ const [modalVisible, setModalVisible] = useState(false);
58
+ const [searchQuery, setSearchQuery] = useState("");
59
+
60
+ const selectedItem = useMemo(
61
+ () => data.find((item) => valueExtractor(item) === value),
62
+ [data, value, valueExtractor],
63
+ );
64
+
65
+ const filteredData = useMemo(() => {
66
+ if (disableLocalFilter) return data;
67
+ if (!searchQuery) return data;
68
+ return data.filter((item) =>
69
+ labelExtractor(item).toLowerCase().includes(searchQuery.toLowerCase()),
70
+ );
71
+ }, [data, searchQuery, labelExtractor, disableLocalFilter]);
72
+
73
+ return (
74
+ <View className={className}>
75
+ {label && (
76
+ <Txt
77
+ color={theme.baseContent}
78
+ weight={weight}
79
+ style={{ marginBottom: 6 }}
80
+ >
81
+ {label}
82
+ </Txt>
83
+ )}
84
+ {renderTrigger ? (
85
+ renderTrigger(() => setModalVisible(true), selectedItem)
86
+ ) : (
87
+ <TouchableOpacity
88
+ onPress={() => setModalVisible(true)}
89
+ className="bg-base-100 border border-base-300 rounded-xl px-4 flex-row items-center justify-between"
90
+ style={{
91
+ height:
92
+ size === "xs" ? 30 : size === "sm" ? 40 : size === "lg" ? 64 : 52,
93
+ }}
94
+ >
95
+ <Txt
96
+ className={
97
+ selectedItem ? "text-base-content" : "text-base-content/50"
98
+ }
99
+ style={{
100
+ fontSize:
101
+ size === "xs"
102
+ ? 12
103
+ : size === "sm"
104
+ ? 14
105
+ : size === "lg"
106
+ ? 18
107
+ : 16,
108
+ }}
109
+ >
110
+ {selectedItem ? labelExtractor(selectedItem) : placeholder}
111
+ </Txt>
112
+ <Ionicons
113
+ name="chevron-down"
114
+ size={size === "xs" ? 14 : size === "sm" ? 16 : 20}
115
+ color={theme.secondary}
116
+ />
117
+ </TouchableOpacity>
118
+ )}
119
+
120
+ <Modal
121
+ visible={modalVisible}
122
+ animationType="slide"
123
+ presentationStyle="pageSheet"
124
+ onRequestClose={() => setModalVisible(false)}
125
+ >
126
+ <View className="flex-1 bg-base-100">
127
+ <ScreenHeader
128
+ title={label || "Select"}
129
+ icon="close"
130
+ onBack={() => setModalVisible(false)}
131
+ />
132
+
133
+ <View className="px-4 pt-4">
134
+ <SearchBar
135
+ value={searchQuery}
136
+ onChangeText={(text) => {
137
+ setSearchQuery(text);
138
+ if (onSearchChange) onSearchChange(text);
139
+ }}
140
+ debounceMs={0}
141
+ />
142
+ </View>
143
+
144
+ {isLoading ? (
145
+ <ActivityIndicator
146
+ style={{ marginTop: 20 }}
147
+ color={theme.primary}
148
+ />
149
+ ) : (
150
+ <FlatList
151
+ data={filteredData}
152
+ keyExtractor={(item, idx) =>
153
+ valueExtractor(item) || idx.toString()
154
+ }
155
+ contentContainerStyle={{
156
+ paddingHorizontal: 16,
157
+ paddingBottom: 40,
158
+ }}
159
+ renderItem={({ item }) => (
160
+ <TouchableOpacity
161
+ onPress={() => {
162
+ onSelect(item);
163
+ setModalVisible(false);
164
+ setSearchQuery("");
165
+ }}
166
+ className="py-4 border-b border-base-200 flex-row justify-between items-center"
167
+ >
168
+ {renderOption ? (
169
+ renderOption(item, valueExtractor(item) === value)
170
+ ) : (
171
+ <>
172
+ <Txt>{labelExtractor(item)}</Txt>
173
+ {valueExtractor(item) === value && (
174
+ <Ionicons
175
+ name="checkmark"
176
+ size={20}
177
+ color={theme.primary}
178
+ />
179
+ )}
180
+ </>
181
+ )}
182
+ </TouchableOpacity>
183
+ )}
184
+ ListEmptyComponent={() => (
185
+ <View className="items-center py-8">
186
+ <Txt className="text-base-content/50">
187
+ No data found
188
+ </Txt>
189
+ </View>
190
+ )}
191
+ onEndReached={onEndReached}
192
+ onEndReachedThreshold={0.5}
193
+ />
194
+ )}
195
+ </View>
196
+ </Modal>
197
+ </View>
198
+ );
199
+ }
@@ -11,15 +11,15 @@ import { Colors } from "../../../constants/Colors";
11
11
  import { Txt } from "../../common/Typography";
12
12
 
13
13
  export interface SwitchProps {
14
- checked: boolean;
15
- onChange?: (checked: boolean) => void;
14
+ value: boolean;
15
+ onChange?: (value: boolean) => void;
16
16
  label?: string;
17
17
  disabled?: boolean;
18
18
  style?: StyleProp<ViewStyle>;
19
19
  }
20
20
 
21
21
  export default function Switch({
22
- checked,
22
+ value,
23
23
  onChange,
24
24
  label,
25
25
  disabled,
@@ -28,11 +28,11 @@ export default function Switch({
28
28
  const { colorScheme } = useColorScheme();
29
29
  const theme = Colors[colorScheme ?? "light"];
30
30
 
31
- const progress = useSharedValue(checked ? 1 : 0);
31
+ const progress = useSharedValue(value ? 1 : 0);
32
32
 
33
33
  useEffect(() => {
34
- progress.value = withTiming(checked ? 1 : 0, { duration: 250 });
35
- }, [checked, progress]);
34
+ progress.value = withTiming(value ? 1 : 0, { duration: 250 });
35
+ }, [value, progress]);
36
36
 
37
37
  const trackStyle = useAnimatedStyle(() => {
38
38
  const backgroundColor = interpolateColor(
@@ -53,7 +53,7 @@ export default function Switch({
53
53
  <TouchableOpacity
54
54
  activeOpacity={0.8}
55
55
  disabled={disabled}
56
- onPress={() => onChange && onChange(!checked)}
56
+ onPress={() => onChange && onChange(!value)}
57
57
  style={[{ flexDirection: "row", alignItems: "center", opacity: disabled ? 0.5 : 1 }, style]}
58
58
  >
59
59
  <Animated.View