diff --git a/MODAL_USAGE.md b/MODAL_USAGE.md
new file mode 100644
index 0000000..071d9b4
--- /dev/null
+++ b/MODAL_USAGE.md
@@ -0,0 +1,409 @@
+# Modal Component
+
+Modal component tương tự như Modal của Ant Design, được tạo cho React Native/Expo.
+
+## Cài đặt
+
+Component này sử dụng `@expo/vector-icons` cho các icon. Đảm bảo bạn đã cài đặt:
+
+```bash
+npx expo install @expo/vector-icons
+```
+
+## Import
+
+```tsx
+import Modal from "@/components/ui/modal";
+```
+
+## Các tính năng chính
+
+### 1. Basic Modal
+
+```tsx
+import React, { useState } from "react";
+import { View, Text, Button } from "react-native";
+import Modal from "@/components/ui/modal";
+
+export default function BasicExample() {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+ );
+}
+```
+
+### 2. Async Close (với confirmLoading)
+
+```tsx
+import React, { useState } from "react";
+import { View, Text, Button } from "react-native";
+import Modal from "@/components/ui/modal";
+
+export default function AsyncExample() {
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const handleOk = async () => {
+ setLoading(true);
+ // Giả lập API call
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ setLoading(false);
+ setOpen(false);
+ };
+
+ return (
+
+
+ );
+}
+```
+
+### 3. Customized Footer
+
+```tsx
+import React, { useState } from "react";
+import { View, Text, Button, TouchableOpacity, StyleSheet } from "react-native";
+import Modal from "@/components/ui/modal";
+
+export default function CustomFooterExample() {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ customFooter: {
+ flexDirection: "row",
+ gap: 8,
+ },
+ customButton: {
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 6,
+ borderWidth: 1,
+ borderColor: "#d9d9d9",
+ },
+ submitButton: {
+ backgroundColor: "#1890ff",
+ borderColor: "#1890ff",
+ },
+ buttonText: {
+ fontSize: 14,
+ color: "#000",
+ },
+ submitText: {
+ color: "#fff",
+ },
+});
+```
+
+### 4. No Footer
+
+```tsx
+ setOpen(false)}
+>
+ Modal content without footer buttons
+
+```
+
+### 5. Centered Modal
+
+```tsx
+ setOpen(false)}
+ onCancel={() => setOpen(false)}
+>
+ This modal is centered on the screen
+
+```
+
+### 6. Custom Width
+
+```tsx
+ setOpen(false)}
+ onCancel={() => setOpen(false)}
+>
+ This modal has custom width
+
+```
+
+### 7. Confirm Modal với useModal Hook
+
+**Đây là cách khuyến nghị sử dụng trong React Native để có context đầy đủ:**
+
+```tsx
+import React from "react";
+import { View, Button } from "react-native";
+import Modal from "@/components/ui/modal";
+
+export default function HookExample() {
+ const [modal, contextHolder] = Modal.useModal();
+
+ const showConfirm = () => {
+ modal.confirm({
+ title: "Do you want to delete these items?",
+ content:
+ "When clicked the OK button, this dialog will be closed after 1 second",
+ onOk: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ console.log("OK");
+ },
+ onCancel: () => {
+ console.log("Cancel");
+ },
+ });
+ };
+
+ const showInfo = () => {
+ modal.info({
+ title: "This is a notification message",
+ content: "Some additional information...",
+ });
+ };
+
+ const showSuccess = () => {
+ modal.success({
+ title: "Success",
+ content: "Operation completed successfully!",
+ });
+ };
+
+ const showError = () => {
+ modal.error({
+ title: "Error",
+ content: "Something went wrong!",
+ });
+ };
+
+ const showWarning = () => {
+ modal.warning({
+ title: "Warning",
+ content: "This is a warning message!",
+ });
+ };
+
+ return (
+
+ {/* contextHolder phải được đặt trong component */}
+ {contextHolder}
+
+
+
+
+
+
+
+ );
+}
+```
+
+### 8. Update Modal Instance
+
+```tsx
+import React from "react";
+import { View, Button } from "react-native";
+import Modal from "@/components/ui/modal";
+
+export default function UpdateExample() {
+ const [modal, contextHolder] = Modal.useModal();
+
+ const showModal = () => {
+ const instance = modal.success({
+ title: "Loading...",
+ content: "Please wait...",
+ });
+
+ // Update after 2 seconds
+ setTimeout(() => {
+ instance.update({
+ title: "Success!",
+ content: "Operation completed successfully!",
+ });
+ }, 2000);
+
+ // Close after 4 seconds
+ setTimeout(() => {
+ instance.destroy();
+ }, 4000);
+ };
+
+ return (
+
+ {contextHolder}
+
+
+ );
+}
+```
+
+## API
+
+### Modal Props
+
+| Prop | Type | Default | Description |
+| --------------- | ----------------------------- | --------- | ------------------------------------------------------------ |
+| open | boolean | false | Whether the modal dialog is visible or not |
+| title | ReactNode | - | The modal dialog's title |
+| closable | boolean | true | Whether a close (x) button is visible on top right or not |
+| closeIcon | ReactNode | - | Custom close icon |
+| maskClosable | boolean | true | Whether to close the modal dialog when the mask is clicked |
+| centered | boolean | false | Centered Modal |
+| width | number \| string | 520 | Width of the modal dialog |
+| confirmLoading | boolean | false | Whether to apply loading visual effect for OK button |
+| okText | string | 'OK' | Text of the OK button |
+| cancelText | string | 'Cancel' | Text of the Cancel button |
+| okType | 'primary' \| 'default' | 'primary' | Button type of the OK button |
+| footer | ReactNode \| null | - | Footer content, set as footer={null} to hide default buttons |
+| mask | boolean | true | Whether show mask or not |
+| zIndex | number | 1000 | The z-index of the Modal |
+| onOk | (e?) => void \| Promise | - | Callback when clicking OK button |
+| onCancel | (e?) => void | - | Callback when clicking cancel button or close icon |
+| afterOpenChange | (open: boolean) => void | - | Callback when animation ends |
+| afterClose | () => void | - | Callback when modal is closed completely |
+| destroyOnClose | boolean | false | Whether to unmount child components on close |
+| keyboard | boolean | true | Whether support press back button to close (Android) |
+
+### Modal.useModal()
+
+Khi bạn cần sử dụng Context, bạn có thể dùng `Modal.useModal()` để tạo `contextHolder` và chèn vào children. Modal được tạo bởi hooks sẽ có tất cả context nơi `contextHolder` được đặt.
+
+**Returns:** `[modalMethods, contextHolder]`
+
+- `modalMethods`: Object chứa các methods
+
+ - `info(config)`: Show info modal
+ - `success(config)`: Show success modal
+ - `error(config)`: Show error modal
+ - `warning(config)`: Show warning modal
+ - `confirm(config)`: Show confirm modal
+
+- `contextHolder`: React element cần được render trong component tree
+
+### Modal Methods Config
+
+| Prop | Type | Default | Description |
+| ---------- | -------------------------------------------------------- | --------- | ----------------------------- |
+| type | 'info' \| 'success' \| 'error' \| 'warning' \| 'confirm' | 'confirm' | Type of the modal |
+| title | ReactNode | - | Title |
+| content | ReactNode | - | Content |
+| icon | ReactNode | - | Custom icon |
+| okText | string | 'OK' | Text of the OK button |
+| cancelText | string | 'Cancel' | Text of the Cancel button |
+| onOk | (e?) => void \| Promise | - | Callback when clicking OK |
+| onCancel | (e?) => void | - | Callback when clicking Cancel |
+
+### Modal Instance
+
+Modal instance được trả về bởi `Modal.useModal()`:
+
+```tsx
+interface ModalInstance {
+ destroy: () => void;
+ update: (config: ConfirmModalProps) => void;
+}
+```
+
+## Lưu ý
+
+1. **React Native Limitations**: Các static methods như `Modal.info()`, `Modal.confirm()` gọi trực tiếp (không qua hook) không được hỗ trợ đầy đủ trong React Native do không thể render imperatively. Hãy sử dụng `Modal.useModal()` hook thay thế.
+
+2. **Context Support**: Khi cần sử dụng Context (như Redux, Theme Context), bắt buộc phải dùng `Modal.useModal()` hook và đặt `contextHolder` trong component tree.
+
+3. **Animation**: Modal sử dụng React Native's built-in Modal với `animationType="fade"`.
+
+4. **Icons**: Component sử dụng `@expo/vector-icons` (Ionicons). Đảm bảo đã cài đặt package này.
+
+5. **Keyboard**: Prop `keyboard` trong React Native chỉ hoạt động với nút back của Android (không có ESC key như web).
+
+## So sánh với Ant Design Modal
+
+| Feature | Ant Design (Web) | This Component (RN) |
+| --------------------------- | ---------------- | --------------------------- |
+| Basic Modal | ✅ | ✅ |
+| Centered | ✅ | ✅ |
+| Custom Footer | ✅ | ✅ |
+| Confirm Dialog | ✅ | ✅ (via useModal) |
+| Info/Success/Error/Warning | ✅ | ✅ (via useModal) |
+| Async close | ✅ | ✅ |
+| Custom width | ✅ | ✅ |
+| Mask closable | ✅ | ✅ |
+| Keyboard close | ✅ (ESC) | ✅ (Back button on Android) |
+| Static methods without hook | ✅ | ⚠️ (Limited support) |
+| useModal hook | ✅ | ✅ (Recommended) |
+| Draggable | ✅ | ❌ (Not applicable) |
+| destroyAll() | ✅ | ❌ |
+
+## License
+
+MIT
diff --git a/README.md b/README.md
index 48dd63f..dbb89ee 100644
--- a/README.md
+++ b/README.md
@@ -48,3 +48,35 @@ Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
+
+## Build app
+
+- Add eas.json file to root folder and add this:
+
+```
+{
+ "cli": {
+ "version": ">= 16.27.0",
+ "appVersionSource": "remote"
+ },
+ "build": {
+ "development": {
+ "developmentClient": true,
+ "distribution": "internal"
+ },
+ "preview": {
+ "android": {
+ "buildType": "apk"
+ },
+ "distribution": "internal"
+ },
+ "production": {
+ "autoIncrement": true
+ }
+ },
+ "submit": {
+ "production": {}
+ }
+}
+
+```
diff --git a/app.json b/app.json
index 595c6f2..a9b5a21 100644
--- a/app.json
+++ b/app.json
@@ -12,8 +12,6 @@
"supportsTablet": true,
"infoPlist": {
"CFBundleLocalizations": [
- "en",
- "vi",
"en",
"vi"
]
@@ -80,6 +78,12 @@
"experiments": {
"typedRoutes": true,
"reactCompiler": true
+ },
+ "extra": {
+ "router": {},
+ "eas": {
+ "projectId": "d4ef1318-5427-4c96-ad88-97ec117829cc"
+ }
}
}
}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 57769dd..384d14b 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,7 +1,7 @@
import {
DarkTheme,
DefaultTheme,
- ThemeProvider as NavigationThemeProvider,
+ ThemeProvider,
} from "@react-navigation/native";
import { Stack, useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
@@ -9,65 +9,64 @@ import { useEffect } from "react";
import "react-native-reanimated";
// import Toast from "react-native-toast-message";
-import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider/gluestack-ui-provider";
+// import { toastConfig } from "@/config";
import { toastConfig } from "@/config";
import { setRouterInstance } from "@/config/auth";
import "@/global.css";
-import { ThemeProvider, useThemeContext } from "@/hooks/use-theme-context";
import { I18nProvider } from "@/hooks/use-i18n";
+import { ThemeProvider as AppThemeProvider } from "@/hooks/use-theme-context";
+import { useColorScheme } from "react-native";
import Toast from "react-native-toast-message";
import "../global.css";
function AppContent() {
- const { colorScheme } = useThemeContext();
+ // const { colorScheme } = useThemeContext();
const router = useRouter();
+ const colorScheme = useColorScheme();
+ console.log("Color Scheme: ", colorScheme);
useEffect(() => {
setRouterInstance(router);
}, [router]);
return (
-
-
+
-
-
+
-
+ {/*
-
-
-
-
-
-
+ */}
+
+
+
+
);
}
export default function RootLayout() {
return (
-
+
-
+
);
}
diff --git a/app/auth/login.tsx b/app/auth/login.tsx
index fde03fb..72141a2 100644
--- a/app/auth/login.tsx
+++ b/app/auth/login.tsx
@@ -6,8 +6,13 @@ import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import SliceSwitch from "@/components/ui/slice-switch";
import { DOMAIN, TOKEN } from "@/constants";
+import { Colors } from "@/constants/theme";
import { login } from "@/controller/AuthController";
import { useI18n } from "@/hooks/use-i18n";
+import {
+ ColorScheme as ThemeColorScheme,
+ useTheme,
+} from "@/hooks/use-theme-context";
import { showErrorToast, showWarningToast } from "@/services/toast_service";
import {
getStorageItem,
@@ -17,7 +22,7 @@ import {
import { parseJwtToken } from "@/utils/token";
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Image,
@@ -38,6 +43,13 @@ export default function LoginScreen() {
const [showPassword, setShowPassword] = useState(false);
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
const { t, setLocale, locale } = useI18n();
+ const { colors, colorScheme } = useTheme();
+ const styles = useMemo(
+ () => createStyles(colors, colorScheme),
+ [colors, colorScheme]
+ );
+ const placeholderColor = colors.textSecondary;
+ const buttonTextColor = colorScheme === "dark" ? colors.text : colors.surface;
const [isVNLang, setIsVNLang] = useState(false);
const checkLogin = useCallback(async () => {
@@ -154,7 +166,10 @@ export default function LoginScreen() {
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
-
+
{/* Header */}
@@ -177,7 +192,7 @@ export default function LoginScreen() {
@@ -241,9 +256,13 @@ export default function LoginScreen() {
disabled={loading}
>
{loading ? (
-
+
) : (
- {t("auth.login")}
+
+ {t("auth.login")}
+
)}
@@ -252,10 +271,10 @@ export default function LoginScreen() {
flex: 1,
paddingVertical: 10,
marginTop: 0,
- borderColor: "#ddd",
+ borderColor: colors.border,
borderWidth: 1,
borderRadius: 8,
- backgroundColor: "transparent",
+ backgroundColor: colors.surface,
justifyContent: "center",
alignItems: "center",
}}
@@ -265,7 +284,7 @@ export default function LoginScreen() {
@@ -282,13 +301,17 @@ export default function LoginScreen() {
@@ -310,129 +333,130 @@ export default function LoginScreen() {
);
}
-const styles = StyleSheet.create({
- scrollContainer: {
- flexGrow: 1,
- justifyContent: "center",
- },
- container: {
- flex: 1,
- paddingHorizontal: 20,
- justifyContent: "center",
- },
- headerContainer: {
- marginBottom: 40,
- alignItems: "center",
- },
- logo: {
- width: 120,
- height: 120,
- marginBottom: 20,
- },
- title: {
- fontSize: 28,
- fontWeight: "bold",
- marginBottom: 8,
- },
- subtitle: {
- fontSize: 16,
- opacity: 0.7,
- },
- formContainer: {
- gap: 16,
- },
- inputGroup: {
- gap: 8,
- },
- label: {
- fontSize: 14,
- fontWeight: "600",
- },
- input: {
- borderWidth: 1,
- borderColor: "#ddd",
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 12,
- fontSize: 16,
- backgroundColor: "#f5f5f5",
- color: "#000",
- },
- loginButton: {
- backgroundColor: "#007AFF",
- paddingVertical: 14,
- borderRadius: 8,
- alignItems: "center",
- marginTop: 16,
- },
- loginButtonDisabled: {
- opacity: 0.6,
- },
- loginButtonText: {
- color: "#fff",
- fontSize: 16,
- fontWeight: "600",
- },
- footerContainer: {
- marginTop: 16,
- alignItems: "center",
- },
- footerText: {
- fontSize: 14,
- },
- linkText: {
- color: "#007AFF",
- fontWeight: "600",
- },
- copyrightContainer: {
- marginTop: 20,
- alignItems: "center",
- },
- copyrightText: {
- fontSize: 12,
- opacity: 0.6,
- textAlign: "center",
- },
- languageSwitcherContainer: {
- display: "flex",
- flexDirection: "row",
- justifyContent: "center",
-
- alignItems: "center",
- marginTop: 24,
- gap: 20,
- },
- languageSwitcherLabel: {
- fontSize: 12,
- fontWeight: "600",
- textAlign: "center",
- opacity: 0.8,
- },
- languageButtonsContainer: {
- flexDirection: "row",
- gap: 10,
- },
- languageButton: {
- flex: 1,
- paddingVertical: 10,
- paddingHorizontal: 12,
- borderWidth: 1,
- borderColor: "#ddd",
- borderRadius: 8,
- alignItems: "center",
- backgroundColor: "transparent",
- },
- languageButtonActive: {
- backgroundColor: "#007AFF",
- borderColor: "#007AFF",
- },
- languageButtonText: {
- fontSize: 14,
- fontWeight: "500",
- color: "#666",
- },
- languageButtonTextActive: {
- color: "#fff",
- fontWeight: "600",
- },
-});
+const createStyles = (colors: typeof Colors.light, scheme: ThemeColorScheme) =>
+ StyleSheet.create({
+ scrollContainer: {
+ flexGrow: 1,
+ justifyContent: "center",
+ backgroundColor: colors.background,
+ },
+ container: {
+ flex: 1,
+ paddingHorizontal: 20,
+ justifyContent: "center",
+ },
+ headerContainer: {
+ marginBottom: 40,
+ alignItems: "center",
+ },
+ logo: {
+ width: 120,
+ height: 120,
+ marginBottom: 20,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: "bold",
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 16,
+ opacity: 0.7,
+ },
+ formContainer: {
+ gap: 16,
+ },
+ inputGroup: {
+ gap: 8,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: "600",
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: colors.border,
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ fontSize: 16,
+ backgroundColor: colors.surface,
+ color: colors.text,
+ },
+ loginButton: {
+ backgroundColor: colors.primary,
+ paddingVertical: 14,
+ borderRadius: 8,
+ alignItems: "center",
+ marginTop: 16,
+ },
+ loginButtonDisabled: {
+ opacity: 0.6,
+ },
+ loginButtonText: {
+ fontSize: 16,
+ fontWeight: "600",
+ },
+ footerContainer: {
+ marginTop: 16,
+ alignItems: "center",
+ },
+ footerText: {
+ fontSize: 14,
+ },
+ linkText: {
+ color: colors.primary,
+ fontWeight: "600",
+ },
+ copyrightContainer: {
+ marginTop: 20,
+ alignItems: "center",
+ },
+ copyrightText: {
+ fontSize: 12,
+ opacity: 0.6,
+ textAlign: "center",
+ color: colors.textSecondary,
+ },
+ languageSwitcherContainer: {
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "center",
+ alignItems: "center",
+ marginTop: 24,
+ gap: 20,
+ },
+ languageSwitcherLabel: {
+ fontSize: 12,
+ fontWeight: "600",
+ textAlign: "center",
+ opacity: 0.8,
+ },
+ languageButtonsContainer: {
+ flexDirection: "row",
+ gap: 10,
+ },
+ languageButton: {
+ flex: 1,
+ paddingVertical: 10,
+ paddingHorizontal: 12,
+ borderWidth: 1,
+ borderColor: colors.border,
+ borderRadius: 8,
+ alignItems: "center",
+ backgroundColor: colors.surface,
+ },
+ languageButtonActive: {
+ backgroundColor: colors.primary,
+ borderColor: colors.primary,
+ },
+ languageButtonText: {
+ fontSize: 14,
+ fontWeight: "500",
+ color: colors.textSecondary,
+ },
+ languageButtonTextActive: {
+ color: scheme === "dark" ? colors.text : colors.surface,
+ fontWeight: "600",
+ },
+ });
diff --git a/components/map/SosButton.tsx b/components/map/SosButton.tsx
index e54c66b..6c00299 100644
--- a/components/map/SosButton.tsx
+++ b/components/map/SosButton.tsx
@@ -8,24 +8,10 @@ import { showErrorToast } from "@/services/toast_service";
import { sosMessage } from "@/utils/sosUtils";
import { MaterialIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
-import {
- FlatList,
- ScrollView,
- StyleSheet,
- Text,
- TextInput,
- TouchableOpacity,
- View,
-} from "react-native";
-import { Button, ButtonText } from "../ui/gluestack-ui-provider/button";
-import {
- Modal,
- ModalBackdrop,
- ModalBody,
- ModalContent,
- ModalFooter,
- ModalHeader,
-} from "../ui/gluestack-ui-provider/modal";
+import { StyleSheet, Text, TextInput, View } from "react-native";
+import IconButton from "../IconButton";
+import Select from "../Select";
+import Modal from "../ui/modal";
const SosButton = () => {
const [sosData, setSosData] = useState();
@@ -34,17 +20,23 @@ const SosButton = () => {
null
);
const [customMessage, setCustomMessage] = useState("");
- const [showDropdown, setShowDropdown] = useState(false);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const { t } = useI18n();
const sosOptions = [
- ...sosMessage.map((msg) => ({ ma: msg.ma, moTa: msg.moTa })),
- { ma: 999, moTa: "Khác" },
+ ...sosMessage.map((msg) => ({
+ ma: msg.ma,
+ moTa: msg.moTa,
+ label: msg.moTa,
+ value: msg.ma,
+ })),
+ { ma: 999, moTa: "Khác", label: "Khác", value: 999 },
];
const getSosData = async () => {
try {
const response = await queryGetSos();
+ // console.log("SoS ResponseL: ", response);
+
setSosData(response.data);
} catch (error) {
console.error("Failed to fetch SOS data:", error);
@@ -58,8 +50,6 @@ const SosButton = () => {
const validateForm = () => {
const newErrors: { [key: string]: string } = {};
- // Không cần validate sosMessage vì luôn có default value (11)
-
if (selectedSosMessage === 999 && customMessage.trim() === "") {
newErrors.customMessage = t("home.sos.statusRequired");
}
@@ -69,27 +59,34 @@ const SosButton = () => {
};
const handleConfirmSos = async () => {
- if (validateForm()) {
- let messageToSend = "";
- if (selectedSosMessage === 999) {
- messageToSend = customMessage.trim();
- } else {
- const selectedOption = sosOptions.find(
- (opt) => opt.ma === selectedSosMessage
- );
- messageToSend = selectedOption ? selectedOption.moTa : "";
- }
- // Gửi dữ liệu đi
- setShowConfirmSosDialog(false);
- // Reset form
- setSelectedSosMessage(null);
- setCustomMessage("");
- setErrors({});
- await sendSosMessage(messageToSend);
+ if (!validateForm()) {
+ console.log("Form chưa validate");
+ return; // Không đóng modal nếu validate fail
}
+
+ let messageToSend = "";
+ if (selectedSosMessage === 999) {
+ messageToSend = customMessage.trim();
+ } else {
+ const selectedOption = sosOptions.find(
+ (opt) => opt.ma === selectedSosMessage
+ );
+ messageToSend = selectedOption ? selectedOption.moTa : "";
+ }
+
+ // Gửi dữ liệu đi
+ await sendSosMessage(messageToSend);
+
+ // Đóng modal và reset form sau khi gửi thành công
+ setShowConfirmSosDialog(false);
+ setSelectedSosMessage(null);
+ setCustomMessage("");
+ setErrors({});
};
const handleClickButton = async (isActive: boolean) => {
+ console.log("Is Active: ", isActive);
+
if (isActive) {
const resp = await queryDeleteSos();
if (resp.status === 200) {
@@ -115,167 +112,91 @@ const SosButton = () => {
return (
<>
- }
+ type="danger"
+ size="middle"
onPress={() => handleClickButton(sosData?.active || false)}
+ style={{ borderRadius: 20 }}
>
-
-
- {sosData?.active ? t("home.sos.active") : t("home.sos.inactive")}
-
- {/* */}
- {/* */}
-
+ {sosData?.active ? t("home.sos.active") : t("home.sos.inactive")}
+
{
+ open={showConfirmSosDialog}
+ onCancel={() => {
setShowConfirmSosDialog(false);
setSelectedSosMessage(null);
setCustomMessage("");
setErrors({});
}}
+ okText={t("home.sos.confirm")}
+ cancelText={t("home.sos.cancel")}
+ title={t("home.sos.title")}
+ centered
+ onOk={handleConfirmSos}
>
-
-
-
-
- {t("home.sos.title")}
-
-
-
-
- {/* Dropdown Nội dung SOS */}
-
- {t("home.sos.content")}
- setShowDropdown(!showDropdown)}
- >
-
- {selectedSosMessage !== null
- ? sosOptions.find((opt) => opt.ma === selectedSosMessage)
- ?.moTa || t("home.sos.selectReason")
- : t("home.sos.selectReason")}
-
-
-
- {errors.sosMessage && (
- {errors.sosMessage}
- )}
-
+ {/* Select Nội dung SOS */}
+
+ {t("home.sos.content")}
- {/* Input Custom Message nếu chọn "Khác" */}
- {selectedSosMessage === 999 && (
-
- {t("home.sos.statusInput")}
- {
- setCustomMessage(text);
- if (text.trim() !== "") {
- setErrors((prev) => {
- const newErrors = { ...prev };
- delete newErrors.customMessage;
- return newErrors;
- });
- }
- }}
- multiline
- numberOfLines={4}
- />
- {errors.customMessage && (
- {errors.customMessage}
- )}
-
- )}
-
-
-
-
-
-
-
-
+ }
+ // Clear error if exists
+ if (errors.sosMessage) {
+ setErrors((prev) => {
+ const newErrors = { ...prev };
+ delete newErrors.sosMessage;
+ return newErrors;
+ });
+ }
+ }}
+ showSearch={false}
+ style={[errors.sosMessage ? styles.errorBorder : undefined]}
+ />
+ {errors.sosMessage && (
+ {errors.sosMessage}
+ )}
+
- {/* Dropdown Modal - Nổi lên */}
- {showDropdown && showConfirmSosDialog && (
- setShowDropdown(false)}>
- setShowDropdown(false)}
- >
-
- item.ma.toString()}
- renderItem={({ item }) => (
- {
- setSelectedSosMessage(item.ma);
- setShowDropdown(false);
- // Clear custom message nếu chọn khác lý do
- if (item.ma !== 999) {
- setCustomMessage("");
- }
- }}
- >
-
- {item.moTa}
-
-
- )}
- />
-
-
-
- )}
+ {/* Input Custom Message nếu chọn "Khác" */}
+ {selectedSosMessage === 999 && (
+
+ {t("home.sos.statusInput")}
+ {
+ setCustomMessage(text);
+ if (text.trim() !== "") {
+ setErrors((prev) => {
+ const newErrors = { ...prev };
+ delete newErrors.customMessage;
+ return newErrors;
+ });
+ }
+ }}
+ multiline
+ numberOfLines={4}
+ />
+ {errors.customMessage && (
+ {errors.customMessage}
+ )}
+
+ )}
+
>
);
};
@@ -290,76 +211,9 @@ const styles = StyleSheet.create({
marginBottom: 8,
color: "#333",
},
- dropdownButton: {
- borderWidth: 1,
- borderColor: "#ddd",
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 12,
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- backgroundColor: "#fff",
- },
errorBorder: {
borderColor: "#ff4444",
},
- dropdownButtonText: {
- fontSize: 14,
- color: "#333",
- flex: 1,
- },
- placeholderText: {
- color: "#999",
- },
- dropdownList: {
- borderWidth: 1,
- borderColor: "#ddd",
- borderRadius: 8,
- marginTop: 4,
- backgroundColor: "#fff",
- overflow: "hidden",
- },
- dropdownItem: {
- paddingHorizontal: 12,
- paddingVertical: 12,
- borderBottomWidth: 1,
- borderBottomColor: "#eee",
- },
- dropdownItemText: {
- fontSize: 14,
- color: "#333",
- },
- dropdownOverlay: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- },
- dropdownModalContainer: {
- backgroundColor: "#fff",
- borderRadius: 12,
- maxHeight: 400,
- minWidth: 280,
- shadowColor: "#000",
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 8,
- elevation: 10,
- },
- dropdownModalItem: {
- paddingHorizontal: 16,
- paddingVertical: 14,
- borderBottomWidth: 1,
- borderBottomColor: "#f0f0f0",
- },
- dropdownModalItemText: {
- fontSize: 14,
- color: "#333",
- },
- selectedItemText: {
- fontWeight: "600",
- color: "#1054C9",
- },
input: {
borderWidth: 1,
borderColor: "#ddd",
diff --git a/components/ui/gluestack-ui-provider/alert-dialog/index.tsx b/components/ui/gluestack-ui-provider/alert-dialog/index.tsx
deleted file mode 100644
index f605e51..0000000
--- a/components/ui/gluestack-ui-provider/alert-dialog/index.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-'use client';
-import React from 'react';
-import { createAlertDialog } from '@gluestack-ui/core/alert-dialog/creator';
-import { tva } from '@gluestack-ui/utils/nativewind-utils';
-import {
- withStyleContext,
- useStyleContext,
-} from '@gluestack-ui/utils/nativewind-utils';
-
-import { cssInterop } from 'nativewind';
-import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
-import {
- Motion,
- AnimatePresence,
- createMotionAnimatedComponent,
- MotionComponentProps,
-} from '@legendapp/motion';
-import { View, Pressable, ScrollView, ViewStyle } from 'react-native';
-
-const SCOPE = 'ALERT_DIALOG';
-
-const RootComponent = withStyleContext(View, SCOPE);
-
-type IMotionViewProps = React.ComponentProps &
- MotionComponentProps;
-
-const MotionView = Motion.View as React.ComponentType;
-
-type IAnimatedPressableProps = React.ComponentProps &
- MotionComponentProps;
-
-const AnimatedPressable = createMotionAnimatedComponent(
- Pressable
-) as React.ComponentType;
-
-const UIAccessibleAlertDialog = createAlertDialog({
- Root: RootComponent,
- Body: ScrollView,
- Content: MotionView,
- CloseButton: Pressable,
- Header: View,
- Footer: View,
- Backdrop: AnimatedPressable,
- AnimatePresence: AnimatePresence,
-});
-
-cssInterop(MotionView, { className: 'style' });
-cssInterop(AnimatedPressable, { className: 'style' });
-
-const alertDialogStyle = tva({
- base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
- parentVariants: {
- size: {
- xs: '',
- sm: '',
- md: '',
- lg: '',
- full: '',
- },
- },
-});
-
-const alertDialogContentStyle = tva({
- base: 'bg-background-0 rounded-lg overflow-hidden border border-outline-100 p-6',
- parentVariants: {
- size: {
- xs: 'w-[60%] max-w-[360px]',
- sm: 'w-[70%] max-w-[420px]',
- md: 'w-[80%] max-w-[510px]',
- lg: 'w-[90%] max-w-[640px]',
- full: 'w-full',
- },
- },
-});
-
-const alertDialogCloseButtonStyle = tva({
- base: 'group/alert-dialog-close-button z-10 rounded-sm p-2 data-[focus-visible=true]:bg-background-100 web:cursor-pointer outline-0',
-});
-
-const alertDialogHeaderStyle = tva({
- base: 'justify-between items-center flex-row',
-});
-
-const alertDialogFooterStyle = tva({
- base: 'flex-row justify-end items-center gap-3',
-});
-
-const alertDialogBodyStyle = tva({ base: '' });
-
-const alertDialogBackdropStyle = tva({
- base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
-});
-
-type IAlertDialogProps = React.ComponentPropsWithoutRef<
- typeof UIAccessibleAlertDialog
-> &
- VariantProps;
-
-type IAlertDialogContentProps = React.ComponentPropsWithoutRef<
- typeof UIAccessibleAlertDialog.Content
-> &
- VariantProps & { className?: string };
-
-type IAlertDialogCloseButtonProps = React.ComponentPropsWithoutRef<
- typeof UIAccessibleAlertDialog.CloseButton
-> &
- VariantProps;
-
-type IAlertDialogHeaderProps = React.ComponentPropsWithoutRef<
- typeof UIAccessibleAlertDialog.Header
-> &
- VariantProps;
-
-type IAlertDialogFooterProps = React.ComponentPropsWithoutRef<
- typeof UIAccessibleAlertDialog.Footer
-> &
- VariantProps;
-
-type IAlertDialogBodyProps = React.ComponentPropsWithoutRef<
- typeof UIAccessibleAlertDialog.Body
-> &
- VariantProps;
-
-type IAlertDialogBackdropProps = React.ComponentPropsWithoutRef<
- typeof UIAccessibleAlertDialog.Backdrop
-> &
- VariantProps & { className?: string };
-
-const AlertDialog = React.forwardRef<
- React.ComponentRef,
- IAlertDialogProps
->(function AlertDialog({ className, size = 'md', ...props }, ref) {
- return (
-
- );
-});
-
-const AlertDialogContent = React.forwardRef<
- React.ComponentRef,
- IAlertDialogContentProps
->(function AlertDialogContent({ className, size, ...props }, ref) {
- const { size: parentSize } = useStyleContext(SCOPE);
-
- return (
-
- );
-});
-
-const AlertDialogCloseButton = React.forwardRef<
- React.ComponentRef,
- IAlertDialogCloseButtonProps
->(function AlertDialogCloseButton({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const AlertDialogHeader = React.forwardRef<
- React.ComponentRef,
- IAlertDialogHeaderProps
->(function AlertDialogHeader({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const AlertDialogFooter = React.forwardRef<
- React.ComponentRef,
- IAlertDialogFooterProps
->(function AlertDialogFooter({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const AlertDialogBody = React.forwardRef<
- React.ComponentRef,
- IAlertDialogBodyProps
->(function AlertDialogBody({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const AlertDialogBackdrop = React.forwardRef<
- React.ComponentRef,
- IAlertDialogBackdropProps
->(function AlertDialogBackdrop({ className, ...props }, ref) {
- return (
-
- );
-});
-
-AlertDialog.displayName = 'AlertDialog';
-AlertDialogContent.displayName = 'AlertDialogContent';
-AlertDialogCloseButton.displayName = 'AlertDialogCloseButton';
-AlertDialogHeader.displayName = 'AlertDialogHeader';
-AlertDialogFooter.displayName = 'AlertDialogFooter';
-AlertDialogBody.displayName = 'AlertDialogBody';
-AlertDialogBackdrop.displayName = 'AlertDialogBackdrop';
-
-export {
- AlertDialog,
- AlertDialogContent,
- AlertDialogCloseButton,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogBody,
- AlertDialogBackdrop,
-};
diff --git a/components/ui/gluestack-ui-provider/button/index.tsx b/components/ui/gluestack-ui-provider/button/index.tsx
deleted file mode 100644
index fd4b639..0000000
--- a/components/ui/gluestack-ui-provider/button/index.tsx
+++ /dev/null
@@ -1,434 +0,0 @@
-'use client';
-import React from 'react';
-import { createButton } from '@gluestack-ui/core/button/creator';
-import {
- tva,
- withStyleContext,
- useStyleContext,
- type VariantProps,
-} from '@gluestack-ui/utils/nativewind-utils';
-import { cssInterop } from 'nativewind';
-import { ActivityIndicator, Pressable, Text, View } from 'react-native';
-import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
-
-const SCOPE = 'BUTTON';
-
-const Root = withStyleContext(Pressable, SCOPE);
-
-const UIButton = createButton({
- Root: Root,
- Text,
- Group: View,
- Spinner: ActivityIndicator,
- Icon: UIIcon,
-});
-
-cssInterop(PrimitiveIcon, {
- className: {
- target: 'style',
- nativeStyleToProp: {
- height: true,
- width: true,
- fill: true,
- color: 'classNameColor',
- stroke: true,
- },
- },
-});
-
-const buttonStyle = tva({
- base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
- variants: {
- action: {
- primary:
- 'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info',
- secondary:
- 'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info',
- positive:
- 'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info',
- negative:
- 'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info',
- default:
- 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
- },
- variant: {
- link: 'px-0',
- outline:
- 'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
- solid: '',
- },
-
- size: {
- xs: 'px-3.5 h-8',
- sm: 'px-4 h-9',
- md: 'px-5 h-10',
- lg: 'px-6 h-11',
- xl: 'px-7 h-12',
- },
- },
- compoundVariants: [
- {
- action: 'primary',
- variant: 'link',
- class:
- 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
- },
- {
- action: 'secondary',
- variant: 'link',
- class:
- 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
- },
- {
- action: 'positive',
- variant: 'link',
- class:
- 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
- },
- {
- action: 'negative',
- variant: 'link',
- class:
- 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
- },
- {
- action: 'primary',
- variant: 'outline',
- class:
- 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
- },
- {
- action: 'secondary',
- variant: 'outline',
- class:
- 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
- },
- {
- action: 'positive',
- variant: 'outline',
- class:
- 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
- },
- {
- action: 'negative',
- variant: 'outline',
- class:
- 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
- },
- ],
-});
-
-const buttonTextStyle = tva({
- base: 'text-typography-0 font-semibold web:select-none',
- parentVariants: {
- action: {
- primary:
- 'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
- secondary:
- 'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
- positive:
- 'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
- negative:
- 'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
- },
- variant: {
- link: 'data-[hover=true]:underline data-[active=true]:underline',
- outline: '',
- solid:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- size: {
- xs: 'text-xs',
- sm: 'text-sm',
- md: 'text-base',
- lg: 'text-lg',
- xl: 'text-xl',
- },
- },
- parentCompoundVariants: [
- {
- variant: 'solid',
- action: 'primary',
- class:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- {
- variant: 'solid',
- action: 'secondary',
- class:
- 'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
- },
- {
- variant: 'solid',
- action: 'positive',
- class:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- {
- variant: 'solid',
- action: 'negative',
- class:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- {
- variant: 'outline',
- action: 'primary',
- class:
- 'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
- },
- {
- variant: 'outline',
- action: 'secondary',
- class:
- 'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
- },
- {
- variant: 'outline',
- action: 'positive',
- class:
- 'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
- },
- {
- variant: 'outline',
- action: 'negative',
- class:
- 'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
- },
- ],
-});
-
-const buttonIconStyle = tva({
- base: 'fill-none',
- parentVariants: {
- variant: {
- link: 'data-[hover=true]:underline data-[active=true]:underline',
- outline: '',
- solid:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- size: {
- xs: 'h-3.5 w-3.5',
- sm: 'h-4 w-4',
- md: 'h-[18px] w-[18px]',
- lg: 'h-[18px] w-[18px]',
- xl: 'h-5 w-5',
- },
- action: {
- primary:
- 'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
- secondary:
- 'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
- positive:
- 'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
-
- negative:
- 'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
- },
- },
- parentCompoundVariants: [
- {
- variant: 'solid',
- action: 'primary',
- class:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- {
- variant: 'solid',
- action: 'secondary',
- class:
- 'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
- },
- {
- variant: 'solid',
- action: 'positive',
- class:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- {
- variant: 'solid',
- action: 'negative',
- class:
- 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
- },
- ],
-});
-
-const buttonGroupStyle = tva({
- base: '',
- variants: {
- space: {
- 'xs': 'gap-1',
- 'sm': 'gap-2',
- 'md': 'gap-3',
- 'lg': 'gap-4',
- 'xl': 'gap-5',
- '2xl': 'gap-6',
- '3xl': 'gap-7',
- '4xl': 'gap-8',
- },
- isAttached: {
- true: 'gap-0',
- },
- flexDirection: {
- 'row': 'flex-row',
- 'column': 'flex-col',
- 'row-reverse': 'flex-row-reverse',
- 'column-reverse': 'flex-col-reverse',
- },
- },
-});
-
-type IButtonProps = Omit<
- React.ComponentPropsWithoutRef,
- 'context'
-> &
- VariantProps & { className?: string };
-
-const Button = React.forwardRef<
- React.ElementRef,
- IButtonProps
->(
- (
- { className, variant = 'solid', size = 'md', action = 'primary', ...props },
- ref
- ) => {
- return (
-
- );
- }
-);
-
-type IButtonTextProps = React.ComponentPropsWithoutRef &
- VariantProps & { className?: string };
-
-const ButtonText = React.forwardRef<
- React.ElementRef,
- IButtonTextProps
->(({ className, variant, size, action, ...props }, ref) => {
- const {
- variant: parentVariant,
- size: parentSize,
- action: parentAction,
- } = useStyleContext(SCOPE);
-
- return (
-
- );
-});
-
-const ButtonSpinner = UIButton.Spinner;
-
-type IButtonIcon = React.ComponentPropsWithoutRef &
- VariantProps & {
- className?: string | undefined;
- as?: React.ElementType;
- height?: number;
- width?: number;
- };
-
-const ButtonIcon = React.forwardRef<
- React.ElementRef,
- IButtonIcon
->(({ className, size, ...props }, ref) => {
- const {
- variant: parentVariant,
- size: parentSize,
- action: parentAction,
- } = useStyleContext(SCOPE);
-
- if (typeof size === 'number') {
- return (
-
- );
- } else if (
- (props.height !== undefined || props.width !== undefined) &&
- size === undefined
- ) {
- return (
-
- );
- }
- return (
-
- );
-});
-
-type IButtonGroupProps = React.ComponentPropsWithoutRef &
- VariantProps;
-
-const ButtonGroup = React.forwardRef<
- React.ElementRef,
- IButtonGroupProps
->(
- (
- {
- className,
- space = 'md',
- isAttached = false,
- flexDirection = 'column',
- ...props
- },
- ref
- ) => {
- return (
-
- );
- }
-);
-
-Button.displayName = 'Button';
-ButtonText.displayName = 'ButtonText';
-ButtonSpinner.displayName = 'ButtonSpinner';
-ButtonIcon.displayName = 'ButtonIcon';
-ButtonGroup.displayName = 'ButtonGroup';
-
-export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };
diff --git a/components/ui/gluestack-ui-provider/gluestack-ui-provider/config.ts b/components/ui/gluestack-ui-provider/gluestack-ui-provider/config.ts
deleted file mode 100644
index f388cc6..0000000
--- a/components/ui/gluestack-ui-provider/gluestack-ui-provider/config.ts
+++ /dev/null
@@ -1,309 +0,0 @@
-'use client';
-import { vars } from 'nativewind';
-
-export const config = {
- light: vars({
- '--color-primary-0': '179 179 179',
- '--color-primary-50': '153 153 153',
- '--color-primary-100': '128 128 128',
- '--color-primary-200': '115 115 115',
- '--color-primary-300': '102 102 102',
- '--color-primary-400': '82 82 82',
- '--color-primary-500': '51 51 51',
- '--color-primary-600': '41 41 41',
- '--color-primary-700': '31 31 31',
- '--color-primary-800': '13 13 13',
- '--color-primary-900': '10 10 10',
- '--color-primary-950': '8 8 8',
-
- /* Secondary */
- '--color-secondary-0': '253 253 253',
- '--color-secondary-50': '251 251 251',
- '--color-secondary-100': '246 246 246',
- '--color-secondary-200': '242 242 242',
- '--color-secondary-300': '237 237 237',
- '--color-secondary-400': '230 230 231',
- '--color-secondary-500': '217 217 219',
- '--color-secondary-600': '198 199 199',
- '--color-secondary-700': '189 189 189',
- '--color-secondary-800': '177 177 177',
- '--color-secondary-900': '165 164 164',
- '--color-secondary-950': '157 157 157',
-
- /* Tertiary */
- '--color-tertiary-0': '255 250 245',
- '--color-tertiary-50': '255 242 229',
- '--color-tertiary-100': '255 233 213',
- '--color-tertiary-200': '254 209 170',
- '--color-tertiary-300': '253 180 116',
- '--color-tertiary-400': '251 157 75',
- '--color-tertiary-500': '231 129 40',
- '--color-tertiary-600': '215 117 31',
- '--color-tertiary-700': '180 98 26',
- '--color-tertiary-800': '130 73 23',
- '--color-tertiary-900': '108 61 19',
- '--color-tertiary-950': '84 49 18',
-
- /* Error */
- '--color-error-0': '254 233 233',
- '--color-error-50': '254 226 226',
- '--color-error-100': '254 202 202',
- '--color-error-200': '252 165 165',
- '--color-error-300': '248 113 113',
- '--color-error-400': '239 68 68',
- '--color-error-500': '230 53 53',
- '--color-error-600': '220 38 38',
- '--color-error-700': '185 28 28',
- '--color-error-800': '153 27 27',
- '--color-error-900': '127 29 29',
- '--color-error-950': '83 19 19',
-
- /* Success */
- '--color-success-0': '228 255 244',
- '--color-success-50': '202 255 232',
- '--color-success-100': '162 241 192',
- '--color-success-200': '132 211 162',
- '--color-success-300': '102 181 132',
- '--color-success-400': '72 151 102',
- '--color-success-500': '52 131 82',
- '--color-success-600': '42 121 72',
- '--color-success-700': '32 111 62',
- '--color-success-800': '22 101 52',
- '--color-success-900': '20 83 45',
- '--color-success-950': '27 50 36',
-
- /* Warning */
- '--color-warning-0': '255 249 245',
- '--color-warning-50': '255 244 236',
- '--color-warning-100': '255 231 213',
- '--color-warning-200': '254 205 170',
- '--color-warning-300': '253 173 116',
- '--color-warning-400': '251 149 75',
- '--color-warning-500': '231 120 40',
- '--color-warning-600': '215 108 31',
- '--color-warning-700': '180 90 26',
- '--color-warning-800': '130 68 23',
- '--color-warning-900': '108 56 19',
- '--color-warning-950': '84 45 18',
-
- /* Info */
- '--color-info-0': '236 248 254',
- '--color-info-50': '199 235 252',
- '--color-info-100': '162 221 250',
- '--color-info-200': '124 207 248',
- '--color-info-300': '87 194 246',
- '--color-info-400': '50 180 244',
- '--color-info-500': '13 166 242',
- '--color-info-600': '11 141 205',
- '--color-info-700': '9 115 168',
- '--color-info-800': '7 90 131',
- '--color-info-900': '5 64 93',
- '--color-info-950': '3 38 56',
-
- /* Typography */
- '--color-typography-0': '254 254 255',
- '--color-typography-50': '245 245 245',
- '--color-typography-100': '229 229 229',
- '--color-typography-200': '219 219 220',
- '--color-typography-300': '212 212 212',
- '--color-typography-400': '163 163 163',
- '--color-typography-500': '140 140 140',
- '--color-typography-600': '115 115 115',
- '--color-typography-700': '82 82 82',
- '--color-typography-800': '64 64 64',
- '--color-typography-900': '38 38 39',
- '--color-typography-950': '23 23 23',
-
- /* Outline */
- '--color-outline-0': '253 254 254',
- '--color-outline-50': '243 243 243',
- '--color-outline-100': '230 230 230',
- '--color-outline-200': '221 220 219',
- '--color-outline-300': '211 211 211',
- '--color-outline-400': '165 163 163',
- '--color-outline-500': '140 141 141',
- '--color-outline-600': '115 116 116',
- '--color-outline-700': '83 82 82',
- '--color-outline-800': '65 65 65',
- '--color-outline-900': '39 38 36',
- '--color-outline-950': '26 23 23',
-
- /* Background */
- '--color-background-0': '255 255 255',
- '--color-background-50': '246 246 246',
- '--color-background-100': '242 241 241',
- '--color-background-200': '220 219 219',
- '--color-background-300': '213 212 212',
- '--color-background-400': '162 163 163',
- '--color-background-500': '142 142 142',
- '--color-background-600': '116 116 116',
- '--color-background-700': '83 82 82',
- '--color-background-800': '65 64 64',
- '--color-background-900': '39 38 37',
- '--color-background-950': '18 18 18',
-
- /* Background Special */
- '--color-background-error': '254 241 241',
- '--color-background-warning': '255 243 234',
- '--color-background-success': '237 252 242',
- '--color-background-muted': '247 248 247',
- '--color-background-info': '235 248 254',
-
- /* Focus Ring Indicator */
- '--color-indicator-primary': '55 55 55',
- '--color-indicator-info': '83 153 236',
- '--color-indicator-error': '185 28 28',
- }),
- dark: vars({
- '--color-primary-0': '166 166 166',
- '--color-primary-50': '175 175 175',
- '--color-primary-100': '186 186 186',
- '--color-primary-200': '197 197 197',
- '--color-primary-300': '212 212 212',
- '--color-primary-400': '221 221 221',
- '--color-primary-500': '230 230 230',
- '--color-primary-600': '240 240 240',
- '--color-primary-700': '250 250 250',
- '--color-primary-800': '253 253 253',
- '--color-primary-900': '254 249 249',
- '--color-primary-950': '253 252 252',
-
- /* Secondary */
- '--color-secondary-0': '20 20 20',
- '--color-secondary-50': '23 23 23',
- '--color-secondary-100': '31 31 31',
- '--color-secondary-200': '39 39 39',
- '--color-secondary-300': '44 44 44',
- '--color-secondary-400': '56 57 57',
- '--color-secondary-500': '63 64 64',
- '--color-secondary-600': '86 86 86',
- '--color-secondary-700': '110 110 110',
- '--color-secondary-800': '135 135 135',
- '--color-secondary-900': '150 150 150',
- '--color-secondary-950': '164 164 164',
-
- /* Tertiary */
- '--color-tertiary-0': '84 49 18',
- '--color-tertiary-50': '108 61 19',
- '--color-tertiary-100': '130 73 23',
- '--color-tertiary-200': '180 98 26',
- '--color-tertiary-300': '215 117 31',
- '--color-tertiary-400': '231 129 40',
- '--color-tertiary-500': '251 157 75',
- '--color-tertiary-600': '253 180 116',
- '--color-tertiary-700': '254 209 170',
- '--color-tertiary-800': '255 233 213',
- '--color-tertiary-900': '255 242 229',
- '--color-tertiary-950': '255 250 245',
-
- /* Error */
- '--color-error-0': '83 19 19',
- '--color-error-50': '127 29 29',
- '--color-error-100': '153 27 27',
- '--color-error-200': '185 28 28',
- '--color-error-300': '220 38 38',
- '--color-error-400': '230 53 53',
- '--color-error-500': '239 68 68',
- '--color-error-600': '249 97 96',
- '--color-error-700': '229 91 90',
- '--color-error-800': '254 202 202',
- '--color-error-900': '254 226 226',
- '--color-error-950': '254 233 233',
-
- /* Success */
- '--color-success-0': '27 50 36',
- '--color-success-50': '20 83 45',
- '--color-success-100': '22 101 52',
- '--color-success-200': '32 111 62',
- '--color-success-300': '42 121 72',
- '--color-success-400': '52 131 82',
- '--color-success-500': '72 151 102',
- '--color-success-600': '102 181 132',
- '--color-success-700': '132 211 162',
- '--color-success-800': '162 241 192',
- '--color-success-900': '202 255 232',
- '--color-success-950': '228 255 244',
-
- /* Warning */
- '--color-warning-0': '84 45 18',
- '--color-warning-50': '108 56 19',
- '--color-warning-100': '130 68 23',
- '--color-warning-200': '180 90 26',
- '--color-warning-300': '215 108 31',
- '--color-warning-400': '231 120 40',
- '--color-warning-500': '251 149 75',
- '--color-warning-600': '253 173 116',
- '--color-warning-700': '254 205 170',
- '--color-warning-800': '255 231 213',
- '--color-warning-900': '255 244 237',
- '--color-warning-950': '255 249 245',
-
- /* Info */
- '--color-info-0': '3 38 56',
- '--color-info-50': '5 64 93',
- '--color-info-100': '7 90 131',
- '--color-info-200': '9 115 168',
- '--color-info-300': '11 141 205',
- '--color-info-400': '13 166 242',
- '--color-info-500': '50 180 244',
- '--color-info-600': '87 194 246',
- '--color-info-700': '124 207 248',
- '--color-info-800': '162 221 250',
- '--color-info-900': '199 235 252',
- '--color-info-950': '236 248 254',
-
- /* Typography */
- '--color-typography-0': '23 23 23',
- '--color-typography-50': '38 38 39',
- '--color-typography-100': '64 64 64',
- '--color-typography-200': '82 82 82',
- '--color-typography-300': '115 115 115',
- '--color-typography-400': '140 140 140',
- '--color-typography-500': '163 163 163',
- '--color-typography-600': '212 212 212',
- '--color-typography-700': '219 219 220',
- '--color-typography-800': '229 229 229',
- '--color-typography-900': '245 245 245',
- '--color-typography-950': '254 254 255',
-
- /* Outline */
- '--color-outline-0': '26 23 23',
- '--color-outline-50': '39 38 36',
- '--color-outline-100': '65 65 65',
- '--color-outline-200': '83 82 82',
- '--color-outline-300': '115 116 116',
- '--color-outline-400': '140 141 141',
- '--color-outline-500': '165 163 163',
- '--color-outline-600': '211 211 211',
- '--color-outline-700': '221 220 219',
- '--color-outline-800': '230 230 230',
- '--color-outline-900': '243 243 243',
- '--color-outline-950': '253 254 254',
-
- /* Background */
- '--color-background-0': '18 18 18',
- '--color-background-50': '39 38 37',
- '--color-background-100': '65 64 64',
- '--color-background-200': '83 82 82',
- '--color-background-300': '116 116 116',
- '--color-background-400': '142 142 142',
- '--color-background-500': '162 163 163',
- '--color-background-600': '213 212 212',
- '--color-background-700': '229 228 228',
- '--color-background-800': '242 241 241',
- '--color-background-900': '246 246 246',
- '--color-background-950': '255 255 255',
-
- /* Background Special */
- '--color-background-error': '66 43 43',
- '--color-background-warning': '65 47 35',
- '--color-background-success': '28 43 33',
- '--color-background-muted': '51 51 51',
- '--color-background-info': '26 40 46',
-
- /* Focus Ring Indicator */
- '--color-indicator-primary': '247 247 247',
- '--color-indicator-info': '161 199 245',
- '--color-indicator-error': '232 70 69',
- }),
-};
diff --git a/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.next15.tsx b/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.next15.tsx
deleted file mode 100644
index 4fafc40..0000000
--- a/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.next15.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-// This is a Next.js 15 compatible version of the GluestackUIProvider
-'use client';
-import React, { useEffect, useLayoutEffect } from 'react';
-import { config } from './config';
-import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
-import { ToastProvider } from '@gluestack-ui/core/toast/creator';
-import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
-import { script } from './script';
-
-const variableStyleTagId = 'nativewind-style';
-const createStyle = (styleTagId: string) => {
- const style = document.createElement('style');
- style.id = styleTagId;
- style.appendChild(document.createTextNode(''));
- return style;
-};
-
-export const useSafeLayoutEffect =
- typeof window !== 'undefined' ? useLayoutEffect : useEffect;
-
-export function GluestackUIProvider({
- mode = 'light',
- ...props
-}: {
- mode?: 'light' | 'dark' | 'system';
- children?: React.ReactNode;
-}) {
- let cssVariablesWithMode = ``;
- Object.keys(config).forEach((configKey) => {
- cssVariablesWithMode +=
- configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
- const cssVariables = Object.keys(
- config[configKey as keyof typeof config]
- ).reduce((acc: string, curr: string) => {
- acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
- return acc;
- }, '');
- cssVariablesWithMode += `${cssVariables} \n}`;
- });
-
- setFlushStyles(cssVariablesWithMode);
-
- const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
- script(e.matches ? 'dark' : 'light');
- }, []);
-
- useSafeLayoutEffect(() => {
- if (mode !== 'system') {
- const documentElement = document.documentElement;
- if (documentElement) {
- documentElement.classList.add(mode);
- documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
- documentElement.style.colorScheme = mode;
- }
- }
- }, [mode]);
-
- useSafeLayoutEffect(() => {
- if (mode !== 'system') return;
- const media = window.matchMedia('(prefers-color-scheme: dark)');
-
- media.addListener(handleMediaQuery);
-
- return () => media.removeListener(handleMediaQuery);
- }, [handleMediaQuery]);
-
- useSafeLayoutEffect(() => {
- if (typeof window !== 'undefined') {
- const documentElement = document.documentElement;
- if (documentElement) {
- const head = documentElement.querySelector('head');
- let style = head?.querySelector(`[id='${variableStyleTagId}']`);
- if (!style) {
- style = createStyle(variableStyleTagId);
- style.innerHTML = cssVariablesWithMode;
- if (head) head.appendChild(style);
- }
- }
- }
- }, []);
-
- return (
-
- {props.children}
-
- );
-}
diff --git a/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.tsx b/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.tsx
deleted file mode 100644
index 3453713..0000000
--- a/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { useEffect } from 'react';
-import { config } from './config';
-import { View, ViewProps } from 'react-native';
-import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
-import { ToastProvider } from '@gluestack-ui/core/toast/creator';
-import { useColorScheme } from 'nativewind';
-
-export type ModeType = 'light' | 'dark' | 'system';
-
-export function GluestackUIProvider({
- mode = 'light',
- ...props
-}: {
- mode?: ModeType;
- children?: React.ReactNode;
- style?: ViewProps['style'];
-}) {
- const { colorScheme, setColorScheme } = useColorScheme();
-
- useEffect(() => {
- setColorScheme(mode);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [mode]);
-
- return (
-
-
- {props.children}
-
-
- );
-}
diff --git a/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.web.tsx b/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.web.tsx
deleted file mode 100644
index 610b6ad..0000000
--- a/components/ui/gluestack-ui-provider/gluestack-ui-provider/index.web.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-'use client';
-import React, { useEffect, useLayoutEffect } from 'react';
-import { config } from './config';
-import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
-import { ToastProvider } from '@gluestack-ui/core/toast/creator';
-import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
-import { script } from './script';
-
-export type ModeType = 'light' | 'dark' | 'system';
-
-const variableStyleTagId = 'nativewind-style';
-const createStyle = (styleTagId: string) => {
- const style = document.createElement('style');
- style.id = styleTagId;
- style.appendChild(document.createTextNode(''));
- return style;
-};
-
-export const useSafeLayoutEffect =
- typeof window !== 'undefined' ? useLayoutEffect : useEffect;
-
-export function GluestackUIProvider({
- mode = 'light',
- ...props
-}: {
- mode?: ModeType;
- children?: React.ReactNode;
-}) {
- let cssVariablesWithMode = ``;
- Object.keys(config).forEach((configKey) => {
- cssVariablesWithMode +=
- configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
- const cssVariables = Object.keys(
- config[configKey as keyof typeof config]
- ).reduce((acc: string, curr: string) => {
- acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
- return acc;
- }, '');
- cssVariablesWithMode += `${cssVariables} \n}`;
- });
-
- setFlushStyles(cssVariablesWithMode);
-
- const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
- script(e.matches ? 'dark' : 'light');
- }, []);
-
- useSafeLayoutEffect(() => {
- if (mode !== 'system') {
- const documentElement = document.documentElement;
- if (documentElement) {
- documentElement.classList.add(mode);
- documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
- documentElement.style.colorScheme = mode;
- }
- }
- }, [mode]);
-
- useSafeLayoutEffect(() => {
- if (mode !== 'system') return;
- const media = window.matchMedia('(prefers-color-scheme: dark)');
-
- media.addListener(handleMediaQuery);
-
- return () => media.removeListener(handleMediaQuery);
- }, [handleMediaQuery]);
-
- useSafeLayoutEffect(() => {
- if (typeof window !== 'undefined') {
- const documentElement = document.documentElement;
- if (documentElement) {
- const head = documentElement.querySelector('head');
- let style = head?.querySelector(`[id='${variableStyleTagId}']`);
- if (!style) {
- style = createStyle(variableStyleTagId);
- style.innerHTML = cssVariablesWithMode;
- if (head) head.appendChild(style);
- }
- }
- }
- }, []);
-
- return (
- <>
-
-
- {props.children}
-
- >
- );
-}
diff --git a/components/ui/gluestack-ui-provider/gluestack-ui-provider/script.ts b/components/ui/gluestack-ui-provider/gluestack-ui-provider/script.ts
deleted file mode 100644
index 732d136..0000000
--- a/components/ui/gluestack-ui-provider/gluestack-ui-provider/script.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export const script = (mode: string) => {
- const documentElement = document.documentElement;
-
- function getSystemColorMode() {
- return window.matchMedia('(prefers-color-scheme: dark)').matches
- ? 'dark'
- : 'light';
- }
-
- try {
- const isSystem = mode === 'system';
- const theme = isSystem ? getSystemColorMode() : mode;
- documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
- documentElement.classList.add(theme);
- documentElement.style.colorScheme = theme;
- } catch (e) {
- console.error(e);
- }
-};
diff --git a/components/ui/gluestack-ui-provider/modal/index.tsx b/components/ui/gluestack-ui-provider/modal/index.tsx
deleted file mode 100644
index 5280daa..0000000
--- a/components/ui/gluestack-ui-provider/modal/index.tsx
+++ /dev/null
@@ -1,276 +0,0 @@
-'use client';
-import React from 'react';
-import { createModal } from '@gluestack-ui/core/modal/creator';
-import { Pressable, View, ScrollView, ViewStyle } from 'react-native';
-import {
- Motion,
- AnimatePresence,
- createMotionAnimatedComponent,
- MotionComponentProps,
-} from '@legendapp/motion';
-import { tva } from '@gluestack-ui/utils/nativewind-utils';
-import {
- withStyleContext,
- useStyleContext,
-} from '@gluestack-ui/utils/nativewind-utils';
-import { cssInterop } from 'nativewind';
-import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
-
-type IAnimatedPressableProps = React.ComponentProps &
- MotionComponentProps;
-
-const AnimatedPressable = createMotionAnimatedComponent(
- Pressable
-) as React.ComponentType;
-const SCOPE = 'MODAL';
-
-type IMotionViewProps = React.ComponentProps &
- MotionComponentProps;
-
-const MotionView = Motion.View as React.ComponentType;
-
-const UIModal = createModal({
- Root: withStyleContext(View, SCOPE),
- Backdrop: AnimatedPressable,
- Content: MotionView,
- Body: ScrollView,
- CloseButton: Pressable,
- Footer: View,
- Header: View,
- AnimatePresence: AnimatePresence,
-});
-
-cssInterop(AnimatedPressable, { className: 'style' });
-cssInterop(MotionView, { className: 'style' });
-
-const modalStyle = tva({
- base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
- variants: {
- size: {
- xs: '',
- sm: '',
- md: '',
- lg: '',
- full: '',
- },
- },
-});
-
-const modalBackdropStyle = tva({
- base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
-});
-
-const modalContentStyle = tva({
- base: 'bg-background-0 rounded-md overflow-hidden border border-outline-100 shadow-hard-2 p-6',
- parentVariants: {
- size: {
- xs: 'w-[60%] max-w-[360px]',
- sm: 'w-[70%] max-w-[420px]',
- md: 'w-[80%] max-w-[510px]',
- lg: 'w-[90%] max-w-[640px]',
- full: 'w-full',
- },
- },
-});
-
-const modalBodyStyle = tva({
- base: 'mt-2 mb-6',
-});
-
-const modalCloseButtonStyle = tva({
- base: 'group/modal-close-button z-10 rounded data-[focus-visible=true]:web:bg-background-100 web:outline-0 cursor-pointer',
-});
-
-const modalHeaderStyle = tva({
- base: 'justify-between items-center flex-row',
-});
-
-const modalFooterStyle = tva({
- base: 'flex-row justify-end items-center gap-2',
-});
-
-type IModalProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IModalBackdropProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IModalContentProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IModalHeaderProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IModalBodyProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IModalFooterProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IModalCloseButtonProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-const Modal = React.forwardRef, IModalProps>(
- ({ className, size = 'md', ...props }, ref) => (
-
- )
-);
-
-const ModalBackdrop = React.forwardRef<
- React.ComponentRef,
- IModalBackdropProps
->(function ModalBackdrop({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const ModalContent = React.forwardRef<
- React.ComponentRef,
- IModalContentProps
->(function ModalContent({ className, size, ...props }, ref) {
- const { size: parentSize } = useStyleContext(SCOPE);
-
- return (
-
- );
-});
-
-const ModalHeader = React.forwardRef<
- React.ComponentRef,
- IModalHeaderProps
->(function ModalHeader({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const ModalBody = React.forwardRef<
- React.ComponentRef,
- IModalBodyProps
->(function ModalBody({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const ModalFooter = React.forwardRef<
- React.ComponentRef,
- IModalFooterProps
->(function ModalFooter({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const ModalCloseButton = React.forwardRef<
- React.ComponentRef,
- IModalCloseButtonProps
->(function ModalCloseButton({ className, ...props }, ref) {
- return (
-
- );
-});
-
-Modal.displayName = 'Modal';
-ModalBackdrop.displayName = 'ModalBackdrop';
-ModalContent.displayName = 'ModalContent';
-ModalHeader.displayName = 'ModalHeader';
-ModalBody.displayName = 'ModalBody';
-ModalFooter.displayName = 'ModalFooter';
-ModalCloseButton.displayName = 'ModalCloseButton';
-
-export {
- Modal,
- ModalBackdrop,
- ModalContent,
- ModalCloseButton,
- ModalHeader,
- ModalBody,
- ModalFooter,
-};
diff --git a/components/ui/gluestack-ui-provider/popover/index.tsx b/components/ui/gluestack-ui-provider/popover/index.tsx
deleted file mode 100644
index 6fc24b4..0000000
--- a/components/ui/gluestack-ui-provider/popover/index.tsx
+++ /dev/null
@@ -1,345 +0,0 @@
-'use client';
-import React from 'react';
-import { View, Pressable, ScrollView, ViewStyle } from 'react-native';
-import {
- Motion,
- createMotionAnimatedComponent,
- AnimatePresence,
- MotionComponentProps,
-} from '@legendapp/motion';
-import { createPopover } from '@gluestack-ui/core/popover/creator';
-import { tva } from '@gluestack-ui/utils/nativewind-utils';
-import {
- withStyleContext,
- useStyleContext,
-} from '@gluestack-ui/utils/nativewind-utils';
-import { cssInterop } from 'nativewind';
-import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
-
-type IAnimatedPressableProps = React.ComponentProps &
- MotionComponentProps;
-
-const AnimatedPressable = createMotionAnimatedComponent(
- Pressable
-) as React.ComponentType;
-
-const SCOPE = 'POPOVER';
-
-type IMotionViewProps = React.ComponentProps &
- MotionComponentProps;
-
-const MotionView = Motion.View as React.ComponentType;
-
-const UIPopover = createPopover({
- Root: withStyleContext(View, SCOPE),
- Arrow: MotionView,
- Backdrop: AnimatedPressable,
- Body: ScrollView,
- CloseButton: Pressable,
- Content: MotionView,
- Footer: View,
- Header: View,
- AnimatePresence: AnimatePresence,
-});
-
-cssInterop(MotionView, { className: 'style' });
-cssInterop(AnimatedPressable, { className: 'style' });
-
-const popoverStyle = tva({
- base: 'group/popover w-full h-full justify-center items-center web:pointer-events-none',
- variants: {
- size: {
- xs: '',
- sm: '',
- md: '',
- lg: '',
- full: '',
- },
- },
-});
-
-const popoverArrowStyle = tva({
- base: 'bg-background-0 z-[1] border absolute overflow-hidden h-3.5 w-3.5 border-outline-100',
- variants: {
- placement: {
- 'top left':
- 'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
- 'top':
- 'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
- 'top right':
- 'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
- 'bottom':
- 'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
- 'bottom left':
- 'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
- 'bottom right':
- 'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
- 'left':
- 'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
- 'left top':
- 'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
- 'left bottom':
- 'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
- 'right':
- 'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
- 'right top':
- 'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
- 'right bottom':
- 'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
- },
- },
-});
-
-const popoverBackdropStyle = tva({
- base: 'absolute left-0 top-0 right-0 bottom-0 web:cursor-default',
-});
-
-const popoverCloseButtonStyle = tva({
- base: 'group/popover-close-button z-[1] rounded-sm data-[focus-visible=true]:web:bg-background-100 web:outline-0 web:cursor-pointer',
-});
-
-const popoverContentStyle = tva({
- base: 'bg-background-0 rounded-lg overflow-hidden border border-outline-100 w-full',
- parentVariants: {
- size: {
- xs: 'max-w-[360px] p-3.5',
- sm: 'max-w-[420px] p-4',
- md: 'max-w-[510px] p-[18px]',
- lg: 'max-w-[640px] p-5',
- full: 'p-6',
- },
- },
-});
-
-const popoverHeaderStyle = tva({
- base: 'flex-row justify-between items-center',
-});
-
-const popoverBodyStyle = tva({
- base: '',
-});
-
-const popoverFooterStyle = tva({
- base: 'flex-row justify-between items-center',
-});
-
-type IPopoverProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IPopoverArrowProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IPopoverContentProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IPopoverHeaderProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IPopoverFooterProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IPopoverBodyProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IPopoverBackdropProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-type IPopoverCloseButtonProps = React.ComponentProps<
- typeof UIPopover.CloseButton
-> &
- VariantProps & { className?: string };
-
-const Popover = React.forwardRef<
- React.ComponentRef,
- IPopoverProps
->(function Popover(
- { className, size = 'md', placement = 'bottom', ...props },
- ref
-) {
- return (
-
- );
-});
-
-const PopoverContent = React.forwardRef<
- React.ComponentRef,
- IPopoverContentProps
->(function PopoverContent({ className, size, ...props }, ref) {
- const { size: parentSize } = useStyleContext(SCOPE);
-
- return (
-
- );
-});
-
-const PopoverArrow = React.forwardRef<
- React.ComponentRef,
- IPopoverArrowProps
->(function PopoverArrow({ className, ...props }, ref) {
- const { placement } = useStyleContext(SCOPE);
- return (
-
- );
-});
-
-const PopoverBackdrop = React.forwardRef<
- React.ComponentRef,
- IPopoverBackdropProps
->(function PopoverBackdrop({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const PopoverBody = React.forwardRef<
- React.ComponentRef,
- IPopoverBodyProps
->(function PopoverBody({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const PopoverCloseButton = React.forwardRef<
- React.ComponentRef,
- IPopoverCloseButtonProps
->(function PopoverCloseButton({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const PopoverFooter = React.forwardRef<
- React.ComponentRef,
- IPopoverFooterProps
->(function PopoverFooter({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const PopoverHeader = React.forwardRef<
- React.ComponentRef,
- IPopoverHeaderProps
->(function PopoverHeader({ className, ...props }, ref) {
- return (
-
- );
-});
-
-Popover.displayName = 'Popover';
-PopoverArrow.displayName = 'PopoverArrow';
-PopoverBackdrop.displayName = 'PopoverBackdrop';
-PopoverContent.displayName = 'PopoverContent';
-PopoverHeader.displayName = 'PopoverHeader';
-PopoverFooter.displayName = 'PopoverFooter';
-PopoverBody.displayName = 'PopoverBody';
-PopoverCloseButton.displayName = 'PopoverCloseButton';
-
-export {
- Popover,
- PopoverBackdrop,
- PopoverArrow,
- PopoverCloseButton,
- PopoverFooter,
- PopoverHeader,
- PopoverBody,
- PopoverContent,
-};
diff --git a/components/ui/gluestack-ui-provider/tooltip/index.tsx b/components/ui/gluestack-ui-provider/tooltip/index.tsx
deleted file mode 100644
index 082a0be..0000000
--- a/components/ui/gluestack-ui-provider/tooltip/index.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-'use client';
-import React from 'react';
-import { createTooltip } from '@gluestack-ui/core/tooltip/creator';
-import { View, Text, ViewStyle } from 'react-native';
-import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
-import { tva } from '@gluestack-ui/utils/nativewind-utils';
-import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils';
-import {
- Motion,
- AnimatePresence,
- MotionComponentProps,
-} from '@legendapp/motion';
-import { cssInterop } from 'nativewind';
-
-type IMotionViewProps = React.ComponentProps &
- MotionComponentProps;
-
-const MotionView = Motion.View as React.ComponentType;
-
-export const UITooltip = createTooltip({
- Root: withStyleContext(View),
- Content: MotionView,
- Text: Text,
- AnimatePresence: AnimatePresence,
-});
-
-cssInterop(MotionView, { className: 'style' });
-
-const tooltipStyle = tva({
- base: 'w-full h-full web:pointer-events-none',
-});
-
-const tooltipContentStyle = tva({
- base: 'py-1 px-3 rounded-sm bg-background-900 web:pointer-events-auto',
-});
-
-const tooltipTextStyle = tva({
- base: 'font-normal tracking-normal web:select-none text-xs text-typography-50',
-
- variants: {
- isTruncated: {
- true: 'line-clamp-1 truncate',
- },
- bold: {
- true: 'font-bold',
- },
- underline: {
- true: 'underline',
- },
- strikeThrough: {
- true: 'line-through',
- },
- size: {
- '2xs': 'text-2xs',
- 'xs': 'text-xs',
- 'sm': 'text-sm',
- 'md': 'text-base',
- 'lg': 'text-lg',
- 'xl': 'text-xl',
- '2xl': 'text-2xl',
- '3xl': 'text-3xl',
- '4xl': 'text-4xl',
- '5xl': 'text-5xl',
- '6xl': 'text-6xl',
- },
- sub: {
- true: 'text-xs',
- },
- italic: {
- true: 'italic',
- },
- highlight: {
- true: 'bg-yellow-500',
- },
- },
-});
-
-type ITooltipProps = React.ComponentProps &
- VariantProps & { className?: string };
-type ITooltipContentProps = React.ComponentProps &
- VariantProps & { className?: string };
-type ITooltipTextProps = React.ComponentProps &
- VariantProps & { className?: string };
-
-const Tooltip = React.forwardRef<
- React.ComponentRef,
- ITooltipProps
->(function Tooltip({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const TooltipContent = React.forwardRef<
- React.ComponentRef,
- ITooltipContentProps & { className?: string }
->(function TooltipContent({ className, ...props }, ref) {
- return (
-
- );
-});
-
-const TooltipText = React.forwardRef<
- React.ComponentRef,
- ITooltipTextProps & { className?: string }
->(function TooltipText({ size, className, ...props }, ref) {
- return (
-
- );
-});
-
-Tooltip.displayName = 'Tooltip';
-TooltipContent.displayName = 'TooltipContent';
-TooltipText.displayName = 'TooltipText';
-
-export { Tooltip, TooltipContent, TooltipText };
diff --git a/components/ui/modal.tsx b/components/ui/modal.tsx
new file mode 100644
index 0000000..5fdf7fc
--- /dev/null
+++ b/components/ui/modal.tsx
@@ -0,0 +1,578 @@
+import { Ionicons } from "@expo/vector-icons";
+import React, { ReactNode, useEffect, useState } from "react";
+import {
+ ActivityIndicator,
+ Dimensions,
+ Pressable,
+ Modal as RNModal,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+
+// Types
+export interface ModalProps {
+ /** Whether the modal dialog is visible or not */
+ open?: boolean;
+ /** The modal dialog's title */
+ title?: ReactNode;
+ /** Whether a close (x) button is visible on top right or not */
+ closable?: boolean;
+ /** Custom close icon */
+ closeIcon?: ReactNode;
+ /** Whether to close the modal dialog when the mask (area outside the modal) is clicked */
+ maskClosable?: boolean;
+ /** Centered Modal */
+ centered?: boolean;
+ /** Width of the modal dialog */
+ width?: number | string;
+ /** Whether to apply loading visual effect for OK button or not */
+ confirmLoading?: boolean;
+ /** Text of the OK button */
+ okText?: string;
+ /** Text of the Cancel button */
+ cancelText?: string;
+ /** Button type of the OK button */
+ okType?: "primary" | "default" | "dashed" | "text" | "link";
+ /** Footer content, set as footer={null} when you don't need default buttons */
+ footer?: ReactNode | null;
+ /** Whether show mask or not */
+ mask?: boolean;
+ /** The z-index of the Modal */
+ zIndex?: number;
+ /** Specify a function that will be called when a user clicks the OK button */
+ onOk?: (e?: any) => void | Promise;
+ /** Specify a function that will be called when a user clicks mask, close button on top right or Cancel button */
+ onCancel?: (e?: any) => void;
+ /** Callback when the animation ends when Modal is turned on and off */
+ afterOpenChange?: (open: boolean) => void;
+ /** Specify a function that will be called when modal is closed completely */
+ afterClose?: () => void;
+ /** Custom className */
+ className?: string;
+ /** Modal body content */
+ children?: ReactNode;
+ /** Whether to unmount child components on close */
+ destroyOnClose?: boolean;
+ /** The ok button props */
+ okButtonProps?: any;
+ /** The cancel button props */
+ cancelButtonProps?: any;
+ /** Whether support press esc to close */
+ keyboard?: boolean;
+}
+
+export interface ConfirmModalProps extends Omit {
+ /** Type of the confirm modal */
+ type?: "info" | "success" | "error" | "warning" | "confirm";
+ /** Content */
+ content?: ReactNode;
+ /** Custom icon */
+ icon?: ReactNode;
+}
+
+// Modal Component
+const Modal: React.FC & {
+ info: (props: ConfirmModalProps) => ModalInstance;
+ success: (props: ConfirmModalProps) => ModalInstance;
+ error: (props: ConfirmModalProps) => ModalInstance;
+ warning: (props: ConfirmModalProps) => ModalInstance;
+ confirm: (props: ConfirmModalProps) => ModalInstance;
+ useModal: () => [
+ {
+ info: (props: ConfirmModalProps) => ModalInstance;
+ success: (props: ConfirmModalProps) => ModalInstance;
+ error: (props: ConfirmModalProps) => ModalInstance;
+ warning: (props: ConfirmModalProps) => ModalInstance;
+ confirm: (props: ConfirmModalProps) => ModalInstance;
+ },
+ ReactNode
+ ];
+} = ({
+ open = false,
+ title,
+ closable = true,
+ closeIcon,
+ maskClosable = true,
+ centered = false,
+ width = 520,
+ confirmLoading = false,
+ okText = "OK",
+ cancelText = "Cancel",
+ okType = "primary",
+ footer,
+ mask = true,
+ zIndex = 1000,
+ onOk,
+ onCancel,
+ afterOpenChange,
+ afterClose,
+ className,
+ children,
+ destroyOnClose = false,
+ okButtonProps,
+ cancelButtonProps,
+ keyboard = true,
+}) => {
+ const [visible, setVisible] = useState(open);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ setVisible(open);
+ if (afterOpenChange) {
+ afterOpenChange(open);
+ }
+ }, [open, afterOpenChange]);
+
+ const handleOk = async () => {
+ if (onOk) {
+ setLoading(true);
+ try {
+ await onOk();
+ // Không tự động đóng modal - để parent component quyết định
+ } catch (error) {
+ console.error("Modal onOk error:", error);
+ } finally {
+ setLoading(false);
+ }
+ } else {
+ setVisible(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (onCancel) {
+ onCancel();
+ } else {
+ // Nếu không có onCancel, tự động đóng modal
+ setVisible(false);
+ }
+ };
+
+ const handleMaskPress = () => {
+ if (maskClosable) {
+ handleCancel();
+ }
+ };
+
+ const handleRequestClose = () => {
+ if (keyboard) {
+ handleCancel();
+ }
+ };
+
+ useEffect(() => {
+ if (!visible && afterClose) {
+ const timer = setTimeout(() => {
+ afterClose();
+ }, 300); // Wait for animation to complete
+ return () => clearTimeout(timer);
+ }
+ }, [visible, afterClose]);
+
+ const renderFooter = () => {
+ if (footer === null) {
+ return null;
+ }
+
+ if (footer !== undefined) {
+ return {footer};
+ }
+
+ return (
+
+
+ {cancelText}
+
+
+ {loading || confirmLoading ? (
+
+ ) : (
+ {okText}
+ )}
+
+
+ );
+ };
+
+ const modalWidth =
+ typeof width === "number" ? width : Dimensions.get("window").width * 0.9;
+
+ return (
+
+
+ e.stopPropagation()}
+ >
+ {/* Header */}
+ {(title || closable) && (
+
+ {title && (
+
+ {title}
+
+ )}
+ {closable && (
+
+ {closeIcon || (
+
+ )}
+
+ )}
+
+ )}
+
+ {/* Body */}
+
+ {(!destroyOnClose || visible) && children}
+
+
+ {/* Footer */}
+ {renderFooter()}
+
+
+
+ );
+};
+
+// Confirm Modal Component
+const ConfirmModal: React.FC<
+ ConfirmModalProps & { visible: boolean; onClose: () => void }
+> = ({
+ visible,
+ onClose,
+ type = "confirm",
+ title,
+ content,
+ icon,
+ okText = "OK",
+ cancelText = "Cancel",
+ onOk,
+ onCancel,
+ ...restProps
+}) => {
+ const [loading, setLoading] = useState(false);
+
+ const getIcon = () => {
+ if (icon !== undefined) return icon;
+
+ const iconProps = { size: 24, style: { marginRight: 12 } };
+ switch (type) {
+ case "info":
+ return (
+
+ );
+ case "success":
+ return (
+
+ );
+ case "error":
+ return ;
+ case "warning":
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const handleOk = async () => {
+ if (onOk) {
+ setLoading(true);
+ try {
+ await onOk();
+ onClose();
+ } catch (error) {
+ console.error("Confirm modal onOk error:", error);
+ } finally {
+ setLoading(false);
+ }
+ } else {
+ onClose();
+ }
+ };
+
+ const handleCancel = () => {
+ if (onCancel) {
+ onCancel();
+ }
+ onClose();
+ };
+
+ return (
+
+
+ {loading ? (
+
+ ) : (
+ {okText}
+ )}
+
+
+ )
+ }
+ {...restProps}
+ >
+
+ {getIcon()}
+ {content}
+
+
+ );
+};
+
+// Modal Instance
+export interface ModalInstance {
+ destroy: () => void;
+ update: (config: ConfirmModalProps) => void;
+}
+
+// Container for imperatively created modals - Not used in React Native
+// Static methods will return instance but won't render imperatively
+// Use Modal.useModal() hook for proper context support
+
+const createConfirmModal = (config: ConfirmModalProps): ModalInstance => {
+ console.warn(
+ "Modal static methods are not fully supported in React Native. Please use Modal.useModal() hook for better context support."
+ );
+
+ return {
+ destroy: () => {
+ console.warn(
+ "Modal.destroy() called but static modals are not supported in React Native"
+ );
+ },
+ update: (newConfig: ConfirmModalProps) => {
+ console.warn(
+ "Modal.update() called but static modals are not supported in React Native"
+ );
+ },
+ };
+};
+
+// Static methods
+Modal.info = (props: ConfirmModalProps) =>
+ createConfirmModal({ ...props, type: "info" });
+Modal.success = (props: ConfirmModalProps) =>
+ createConfirmModal({ ...props, type: "success" });
+Modal.error = (props: ConfirmModalProps) =>
+ createConfirmModal({ ...props, type: "error" });
+Modal.warning = (props: ConfirmModalProps) =>
+ createConfirmModal({ ...props, type: "warning" });
+Modal.confirm = (props: ConfirmModalProps) =>
+ createConfirmModal({ ...props, type: "confirm" });
+
+// useModal hook
+Modal.useModal = () => {
+ const [modals, setModals] = useState([]);
+
+ const createModal = (
+ config: ConfirmModalProps,
+ type: ConfirmModalProps["type"]
+ ) => {
+ const id = `modal-${Date.now()}-${Math.random()}`;
+
+ const destroy = () => {
+ setModals((prev) => prev.filter((modal: any) => modal.key !== id));
+ };
+
+ const update = (newConfig: ConfirmModalProps) => {
+ setModals((prev) =>
+ prev.map((modal: any) =>
+ modal.key === id ? (
+
+ ) : (
+ modal
+ )
+ )
+ );
+ };
+
+ const modalElement = (
+
+ );
+
+ setModals((prev) => [...prev, modalElement]);
+
+ return { destroy, update };
+ };
+
+ const modalMethods = {
+ info: (props: ConfirmModalProps) => createModal(props, "info"),
+ success: (props: ConfirmModalProps) => createModal(props, "success"),
+ error: (props: ConfirmModalProps) => createModal(props, "error"),
+ warning: (props: ConfirmModalProps) => createModal(props, "warning"),
+ confirm: (props: ConfirmModalProps) => createModal(props, "confirm"),
+ };
+
+ const contextHolder = <>{modals}>;
+
+ return [modalMethods, contextHolder];
+};
+
+// Styles
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.45)",
+ justifyContent: "flex-start",
+ paddingTop: 100,
+ alignItems: "center",
+ },
+ centered: {
+ justifyContent: "center",
+ paddingTop: 0,
+ },
+ noMask: {
+ backgroundColor: "transparent",
+ },
+ modal: {
+ backgroundColor: "#fff",
+ borderRadius: 8,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.15,
+ shadowRadius: 12,
+ elevation: 8,
+ maxHeight: "90%",
+ },
+ header: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ paddingHorizontal: 24,
+ paddingTop: 20,
+ paddingBottom: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: "#f0f0f0",
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: "#000",
+ flex: 1,
+ },
+ closeButton: {
+ padding: 4,
+ marginLeft: 12,
+ },
+ body: {
+ paddingHorizontal: 24,
+ paddingVertical: 20,
+ },
+ footer: {
+ flexDirection: "row",
+ justifyContent: "flex-end",
+ alignItems: "center",
+ paddingHorizontal: 24,
+ paddingBottom: 20,
+ paddingTop: 12,
+ gap: 8,
+ },
+ button: {
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ borderRadius: 6,
+ minWidth: 70,
+ alignItems: "center",
+ justifyContent: "center",
+ height: 36,
+ },
+ cancelButton: {
+ backgroundColor: "#fff",
+ borderWidth: 1,
+ borderColor: "#d9d9d9",
+ },
+ cancelButtonText: {
+ color: "#000",
+ fontSize: 14,
+ fontWeight: "500",
+ },
+ okButton: {
+ backgroundColor: "#1890ff",
+ },
+ primaryButton: {
+ backgroundColor: "#1890ff",
+ },
+ okButtonText: {
+ color: "#fff",
+ fontSize: 14,
+ fontWeight: "500",
+ },
+ disabledButton: {
+ opacity: 0.6,
+ },
+ confirmContent: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ },
+ confirmText: {
+ flex: 1,
+ fontSize: 14,
+ color: "#000",
+ lineHeight: 22,
+ },
+});
+
+export default Modal;
diff --git a/hooks/use-theme-context.tsx b/hooks/use-theme-context.tsx
index 0077a0a..fd121ad 100644
--- a/hooks/use-theme-context.tsx
+++ b/hooks/use-theme-context.tsx
@@ -1,26 +1,30 @@
/**
- * Theme Context Hook for managing app-wide theme state
- * Supports Light, Dark, and System (automatic) modes
+ * Theme Context Hook for managing app-wide theme state.
+ * Supports Light, Dark, and System (automatic) modes across Expo platforms.
*
- * IMPORTANT: Requires expo-system-ui plugin in app.json for Android support
+ * IMPORTANT: Requires expo-system-ui plugin in app.json for Android status bar support.
*/
-import { Colors, ColorName } from "@/constants/theme";
+import { ColorName, Colors } from "@/constants/theme";
import { getStorageItem, setStorageItem } from "@/utils/storage";
import {
createContext,
+ ReactNode,
+ useCallback,
useContext,
useEffect,
+ useMemo,
useState,
- ReactNode,
} from "react";
import {
- useColorScheme as useSystemColorScheme,
Appearance,
+ AppState,
+ AppStateStatus,
+ useColorScheme as useRNColorScheme,
} from "react-native";
-type ThemeMode = "light" | "dark" | "system";
-type ColorScheme = "light" | "dark";
+export type ThemeMode = "light" | "dark" | "system";
+export type ColorScheme = "light" | "dark";
interface ThemeContextType {
themeMode: ThemeMode;
@@ -28,146 +32,162 @@ interface ThemeContextType {
colors: typeof Colors.light;
setThemeMode: (mode: ThemeMode) => Promise;
getColor: (colorName: ColorName) => string;
+ isHydrated: boolean;
}
const ThemeContext = createContext(undefined);
const THEME_STORAGE_KEY = "theme_mode";
+const getSystemScheme = (): ColorScheme => {
+ const scheme = Appearance.getColorScheme();
+ // console.log("[Theme] Appearance.getColorScheme():", scheme);
+ return scheme === "dark" ? "dark" : "light";
+};
+
+const isThemeMode = (value: unknown): value is ThemeMode => {
+ return value === "light" || value === "dark" || value === "system";
+};
+
export function ThemeProvider({ children }: { children: ReactNode }) {
- // State để force re-render khi system theme thay đổi
- const [systemTheme, setSystemTheme] = useState(() => {
- const current = Appearance.getColorScheme();
- console.log("[Theme] Initial system theme:", current);
- return current === "dark" ? "dark" : "light";
- });
-
- // State lưu user's choice (light/dark/system)
+ const [systemScheme, setSystemScheme] =
+ useState(getSystemScheme);
const [themeMode, setThemeModeState] = useState("system");
- const [isLoaded, setIsLoaded] = useState(false);
+ const [isHydrated, setIsHydrated] = useState(false);
- // Listen vào system theme changes - đăng ký ngay từ đầu
+ const syncSystemScheme = useCallback(() => {
+ const next = getSystemScheme();
+ // console.log("[Theme] syncSystemScheme computed:", next);
+ setSystemScheme((current) => (current === next ? current : next));
+ }, []);
+
+ const rnScheme = useRNColorScheme();
useEffect(() => {
- console.log("[Theme] Registering appearance listener");
+ if (!rnScheme) return;
+ const next = rnScheme === "dark" ? "dark" : "light";
+ // console.log("[Theme] useColorScheme hook emitted:", rnScheme);
+ setSystemScheme((current) => (current === next ? current : next));
+ }, [rnScheme]);
+ useEffect(() => {
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
- const newScheme = colorScheme === "dark" ? "dark" : "light";
- console.log(
- "[Theme] System theme changed to:",
- newScheme,
- "at",
- new Date().toLocaleTimeString()
- );
- setSystemTheme(newScheme);
+ const next = colorScheme === "dark" ? "dark" : "light";
+ // console.log("[Theme] Appearance listener fired with:", colorScheme);
+ setSystemScheme((current) => (current === next ? current : next));
});
- // Double check current theme khi mount
- const currentScheme = Appearance.getColorScheme();
- const current = currentScheme === "dark" ? "dark" : "light";
- if (current !== systemTheme) {
- console.log("[Theme] Syncing system theme on mount:", current);
- setSystemTheme(current);
- }
+ syncSystemScheme();
return () => {
- console.log("[Theme] Removing appearance listener");
subscription.remove();
};
- }, []);
-
- // Xác định colorScheme cuối cùng
- const colorScheme: ColorScheme =
- themeMode === "system" ? systemTheme : themeMode;
-
- const colors = Colors[colorScheme];
-
- // Log để debug
- useEffect(() => {
- console.log(
- "[Theme] Current state - Mode:",
- themeMode,
- "| Scheme:",
- colorScheme,
- "| System:",
- systemTheme
- );
- }, [themeMode, colorScheme, systemTheme]);
+ }, [syncSystemScheme]);
useEffect(() => {
- const loadThemeMode = async () => {
- try {
- const savedThemeMode = await getStorageItem(THEME_STORAGE_KEY);
- if (
- savedThemeMode &&
- ["light", "dark", "system"].includes(savedThemeMode)
- ) {
- setThemeModeState(savedThemeMode as ThemeMode);
- }
- } catch (error) {
- console.warn("Failed to load theme mode:", error);
- } finally {
- setIsLoaded(true);
+ // console.log("[Theme] System scheme detected:", systemScheme);
+ }, [systemScheme]);
+
+ useEffect(() => {
+ const handleAppStateChange = (nextState: AppStateStatus) => {
+ if (nextState === "active") {
+ // console.log("[Theme] AppState active → scheduling system scheme sync");
+ setTimeout(() => {
+ // console.log("[Theme] AppState sync callback running");
+ syncSystemScheme();
+ }, 100);
}
};
- loadThemeMode();
+
+ const subscription = AppState.addEventListener(
+ "change",
+ handleAppStateChange
+ );
+ return () => subscription.remove();
+ }, [syncSystemScheme]);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const hydrateThemeMode = async () => {
+ try {
+ const savedThemeMode = await getStorageItem(THEME_STORAGE_KEY);
+ if (isMounted && isThemeMode(savedThemeMode)) {
+ setThemeModeState(savedThemeMode);
+ }
+ } catch (error) {
+ console.warn("[Theme] Failed to load theme mode:", error);
+ } finally {
+ if (isMounted) {
+ setIsHydrated(true);
+ }
+ }
+ };
+
+ hydrateThemeMode();
+
+ return () => {
+ isMounted = false;
+ };
}, []);
- const setThemeMode = async (mode: ThemeMode) => {
+ const colorScheme: ColorScheme =
+ themeMode === "system" ? systemScheme : themeMode;
+
+ const colors = useMemo(() => Colors[colorScheme], [colorScheme]);
+
+ const setThemeMode = useCallback(async (mode: ThemeMode) => {
+ setThemeModeState(mode);
try {
- setThemeModeState(mode);
await setStorageItem(THEME_STORAGE_KEY, mode);
- console.log("[Theme] Changed to:", mode);
} catch (error) {
- console.warn("Failed to save theme mode:", error);
+ console.warn("[Theme] Failed to save theme mode:", error);
}
- };
+ }, []);
- const getColor = (colorName: ColorName): string => {
- return colors[colorName] || colors.text;
- };
+ useEffect(() => {
+ // console.log("[Theme] window defined:", typeof window !== "undefined");
+ }, []);
- // Chờ theme load xong trước khi render
- if (!isLoaded) {
- // Render với default theme (system) khi đang load
- return (
- {},
- getColor: (colorName: ColorName) =>
- Colors[systemTheme][colorName] || Colors[systemTheme].text,
- }}
- >
- {children}
-
- );
- }
+ const getColor = useCallback(
+ (colorName: ColorName) => colors[colorName] ?? colors.text,
+ [colors]
+ );
- const value: ThemeContextType = {
- themeMode,
- colorScheme,
- colors,
- setThemeMode,
- getColor,
- };
+ useEffect(() => {
+ // console.log("[Theme] Mode:", themeMode);
+ }, [themeMode]);
+
+ useEffect(() => {
+ // console.log("[Theme] Derived colorScheme:", colorScheme);
+ }, [colorScheme]);
+
+ const value = useMemo(
+ () => ({
+ themeMode,
+ colorScheme,
+ colors,
+ setThemeMode,
+ getColor,
+ isHydrated,
+ }),
+ [themeMode, colorScheme, colors, setThemeMode, getColor, isHydrated]
+ );
return (
{children}
);
}
-export function useThemeContext(): ThemeContextType {
+export function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (context === undefined) {
- throw new Error("useThemeContext must be used within a ThemeProvider");
+ throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
-// Legacy hook cho backward compatibility
+export const useThemeContext = useTheme;
+
export function useColorScheme(): ColorScheme {
- const { colorScheme } = useThemeContext();
- return colorScheme;
+ return useTheme().colorScheme;
}
diff --git a/package-lock.json b/package-lock.json
index edecd7d..4e8c6a3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,8 +10,6 @@
"dependencies": {
"@expo/html-elements": "^0.10.1",
"@expo/vector-icons": "^15.0.3",
- "@gluestack-ui/core": "^3.0.12",
- "@gluestack-ui/utils": "^3.0.11",
"@hookform/resolvers": "^5.2.2",
"@islacel/react-native-custom-switch": "^1.0.10",
"@legendapp/motion": "^2.5.3",
@@ -2357,43 +2355,6 @@
"tslib": "^2.8.0"
}
},
- "node_modules/@gluestack-ui/core": {
- "version": "3.0.12",
- "resolved": "https://registry.npmjs.org/@gluestack-ui/core/-/core-3.0.12.tgz",
- "integrity": "sha512-TyNjDUJrZF/FTqcSEPBR87wZQ3yvbWuTjn0tG5AFYzYfMCw0IpfTigmzoajN9KHensN0xNwHoAkXKaHlhy11yQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@gluestack-ui/utils": ">=2.0.0",
- "react": ">=16.8.0",
- "react-native": ">=0.64.0",
- "react-native-safe-area-context": ">=4.0.0",
- "react-native-svg": ">=12.0.0",
- "react-native-web": ">=0.19.0"
- }
- },
- "node_modules/@gluestack-ui/utils": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/@gluestack-ui/utils/-/utils-3.0.11.tgz",
- "integrity": "sha512-4stxK98v07NFAGvSI4Dxje/xbnftaY45VcZglZUxlAr8FFVLNFcjXUTSnVWqog0DBp2oJ7Nk/AYUpT2KkpI+7A==",
- "dependencies": {
- "dom-helpers": "^6.0.1",
- "react-aria": "^3.41.1",
- "react-stately": "^3.39.0",
- "tailwind-variants": "0.1.20"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "react": ">=16.8.0",
- "react-native": ">=0.64.0",
- "react-native-web": ">=0.19.0",
- "tailwindcss": ">=3.0.0"
- }
- },
"node_modules/@hookform/resolvers": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
@@ -7379,6 +7340,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/data-view-buffer": {
@@ -7625,16 +7587,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/dom-helpers": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-6.0.1.tgz",
- "integrity": "sha512-IKySryuFwseGkrCA/pIqlwUPOD50w1Lj/B2Yief3vBOP18k5y4t+hTqKh55gULDVeJMRitcozve+g/wVFf4sFQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.27.1",
- "csstype": "^3.1.3"
- }
- },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
diff --git a/package.json b/package.json
index 6bf610b..53433d7 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,6 @@
"dependencies": {
"@expo/html-elements": "^0.10.1",
"@expo/vector-icons": "^15.0.3",
- "@gluestack-ui/core": "^3.0.12",
- "@gluestack-ui/utils": "^3.0.11",
"@hookform/resolvers": "^5.2.2",
"@islacel/react-native-custom-switch": "^1.0.10",
"@legendapp/motion": "^2.5.3",