Compare commits
8 Commits
f3cf10e5e6
...
MinhNN
| Author | SHA1 | Date | |
|---|---|---|---|
| 554289ee1e | |||
| 6975358a7f | |||
| 51327c7d01 | |||
| 1d5b29e4a7 | |||
| 7cb35efd30 | |||
|
|
d8874fbe60 | ||
| 00fd53bbd4 | |||
|
|
742d8f6bcc |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -3,5 +3,6 @@
|
|||||||
"source.fixAll": "explicit",
|
"source.fixAll": "explicit",
|
||||||
"source.organizeImports": "explicit",
|
"source.organizeImports": "explicit",
|
||||||
"source.sortMembers": "explicit"
|
"source.sortMembers": "explicit"
|
||||||
}
|
},
|
||||||
|
"postman.settings.dotenv-detection-notification-visibility": false
|
||||||
}
|
}
|
||||||
|
|||||||
409
MODAL_USAGE.md
Normal file
409
MODAL_USAGE.md
Normal file
@@ -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 (
|
||||||
|
<View>
|
||||||
|
<Button title="Open Modal" onPress={() => setOpen(true)} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Basic Modal"
|
||||||
|
onOk={() => {
|
||||||
|
console.log("OK clicked");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>Some contents...</Text>
|
||||||
|
<Text>Some contents...</Text>
|
||||||
|
<Text>Some contents...</Text>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (
|
||||||
|
<View>
|
||||||
|
<Button title="Open Modal" onPress={() => setOpen(true)} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Async Modal"
|
||||||
|
confirmLoading={loading}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>Modal will be closed after 2 seconds when you click OK.</Text>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (
|
||||||
|
<View>
|
||||||
|
<Button title="Open Modal" onPress={() => setOpen(true)} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Custom Footer Modal"
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
footer={
|
||||||
|
<View style={styles.customFooter}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.customButton}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Return</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.customButton, styles.submitButton]}
|
||||||
|
onPress={() => {
|
||||||
|
console.log("Submit");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.buttonText, styles.submitText]}>Submit</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text>Custom footer buttons</Text>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Modal Without Footer"
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>Modal content without footer buttons</Text>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Centered Modal
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Centered Modal"
|
||||||
|
centered
|
||||||
|
onOk={() => setOpen(false)}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>This modal is centered on the screen</Text>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Custom Width
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Custom Width Modal"
|
||||||
|
width={700}
|
||||||
|
onOk={() => setOpen(false)}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>This modal has custom width</Text>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (
|
||||||
|
<View style={{ padding: 20, gap: 10 }}>
|
||||||
|
{/* contextHolder phải được đặt trong component */}
|
||||||
|
{contextHolder}
|
||||||
|
|
||||||
|
<Button title="Confirm" onPress={showConfirm} />
|
||||||
|
<Button title="Info" onPress={showInfo} />
|
||||||
|
<Button title="Success" onPress={showSuccess} />
|
||||||
|
<Button title="Error" onPress={showError} />
|
||||||
|
<Button title="Warning" onPress={showWarning} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (
|
||||||
|
<View>
|
||||||
|
{contextHolder}
|
||||||
|
<Button title="Show Updating Modal" onPress={showModal} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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<void> | - | 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<void> | - | 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
|
||||||
32
README.md
32
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.
|
- [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.
|
- [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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
444
THEME_GUIDE.md
444
THEME_GUIDE.md
@@ -2,11 +2,49 @@
|
|||||||
|
|
||||||
## Tổng quan
|
## Tổng quan
|
||||||
|
|
||||||
Hệ thống theme đã được cấu hình để hỗ trợ Light Mode, Dark Mode và System Mode (tự động theo hệ thống). Theme được lưu trữ trong AsyncStorage và sẽ được khôi phục khi khởi động lại ứng dụng.
|
Hệ thống theme hỗ trợ **Light Mode**, **Dark Mode** và **System Mode** (tự động theo hệ thống). Theme preference được lưu trong AsyncStorage và tự động khôi phục khi khởi động lại ứng dụng.
|
||||||
|
|
||||||
## Cấu trúc Theme
|
## Kiến trúc Theme System
|
||||||
|
|
||||||
### 1. Colors Configuration (`constants/theme.ts`)
|
### 1. Theme Provider (`hooks/use-theme-context.tsx`)
|
||||||
|
|
||||||
|
Theme Provider là core của hệ thống theme, quản lý state và đồng bộ với system theme.
|
||||||
|
|
||||||
|
**Các tính năng chính:**
|
||||||
|
|
||||||
|
- Quản lý `themeMode`: `'light' | 'dark' | 'system'`
|
||||||
|
- Tự động detect system theme thông qua nhiều nguồn:
|
||||||
|
- `Appearance.getColorScheme()` - iOS/Android system theme
|
||||||
|
- `useColorScheme()` hook từ React Native
|
||||||
|
- `Appearance.addChangeListener()` - listen system theme changes
|
||||||
|
- `AppState` listener - sync lại khi app active
|
||||||
|
- Lưu và restore theme preference từ AsyncStorage
|
||||||
|
- Export ra `colorScheme` cuối cùng: `'light' | 'dark'`
|
||||||
|
|
||||||
|
**ThemeContextType:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ThemeContextType {
|
||||||
|
themeMode: ThemeMode; // User's choice: 'light' | 'dark' | 'system'
|
||||||
|
colorScheme: ColorScheme; // Final theme: 'light' | 'dark'
|
||||||
|
colors: typeof Colors.light; // Theme colors object
|
||||||
|
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
||||||
|
getColor: (colorName: ColorName) => string;
|
||||||
|
isHydrated: boolean; // AsyncStorage đã load xong
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cách hoạt động:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Xác định colorScheme cuối cùng
|
||||||
|
const colorScheme: ColorScheme =
|
||||||
|
themeMode === "system" ? systemScheme : themeMode;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Colors Configuration (`constants/theme.ts`)
|
||||||
|
|
||||||
|
Định nghĩa tất cả colors cho light và dark theme:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const Colors = {
|
export const Colors = {
|
||||||
@@ -17,11 +55,16 @@ export const Colors = {
|
|||||||
backgroundSecondary: "#f5f5f5",
|
backgroundSecondary: "#f5f5f5",
|
||||||
surface: "#ffffff",
|
surface: "#ffffff",
|
||||||
surfaceSecondary: "#f8f9fa",
|
surfaceSecondary: "#f8f9fa",
|
||||||
|
tint: "#0a7ea4",
|
||||||
primary: "#007AFF",
|
primary: "#007AFF",
|
||||||
secondary: "#5AC8FA",
|
secondary: "#5AC8FA",
|
||||||
success: "#34C759",
|
success: "#34C759",
|
||||||
warning: "#FF9500",
|
warning: "#ff6600",
|
||||||
error: "#FF3B30",
|
error: "#FF3B30",
|
||||||
|
icon: "#687076",
|
||||||
|
border: "#C6C6C8",
|
||||||
|
separator: "#E5E5E7",
|
||||||
|
card: "#ffffff",
|
||||||
// ... more colors
|
// ... more colors
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
@@ -31,107 +74,110 @@ export const Colors = {
|
|||||||
backgroundSecondary: "#1C1C1E",
|
backgroundSecondary: "#1C1C1E",
|
||||||
surface: "#1C1C1E",
|
surface: "#1C1C1E",
|
||||||
surfaceSecondary: "#2C2C2E",
|
surfaceSecondary: "#2C2C2E",
|
||||||
|
tint: "#fff",
|
||||||
primary: "#0A84FF",
|
primary: "#0A84FF",
|
||||||
secondary: "#64D2FF",
|
secondary: "#64D2FF",
|
||||||
success: "#30D158",
|
success: "#30D158",
|
||||||
warning: "#FF9F0A",
|
warning: "#ff6600",
|
||||||
error: "#FF453A",
|
error: "#FF453A",
|
||||||
|
icon: "#8E8E93",
|
||||||
|
border: "#38383A",
|
||||||
|
separator: "#38383A",
|
||||||
|
card: "#1C1C1E",
|
||||||
// ... more colors
|
// ... more colors
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ColorName = keyof typeof Colors.light;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Theme Context (`hooks/use-theme-context.tsx`)
|
### 3. Setup trong App (`app/_layout.tsx`)
|
||||||
|
|
||||||
Cung cấp theme state và functions cho toàn bộ app:
|
Theme Provider phải wrap toàn bộ app:
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
interface ThemeContextType {
|
export default function RootLayout() {
|
||||||
themeMode: ThemeMode; // 'light' | 'dark' | 'system'
|
return (
|
||||||
colorScheme: ColorScheme; // 'light' | 'dark'
|
<I18nProvider>
|
||||||
colors: typeof Colors.light;
|
<AppThemeProvider>
|
||||||
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
<AppContent />
|
||||||
getColor: (colorName: ColorName) => string;
|
</AppThemeProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
const { colorScheme } = useThemeContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||||
|
<Stack>{/* ... routes */}</Stack>
|
||||||
|
<StatusBar style="auto" />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cách sử dụng Theme
|
## Cách sử dụng Theme
|
||||||
|
|
||||||
### 1. Sử dụng Themed Components
|
### 1. useThemeContext (Core Hook)
|
||||||
|
|
||||||
```tsx
|
Hook chính để access theme state:
|
||||||
import { ThemedText } from "@/components/themed-text";
|
|
||||||
import { ThemedView } from "@/components/themed-view";
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
return (
|
|
||||||
<ThemedView>
|
|
||||||
<ThemedText type="title">Title Text</ThemedText>
|
|
||||||
<ThemedText type="default">Regular Text</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Sử dụng Theme Hook
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
|
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
const { colors, colorScheme, setThemeMode } = useThemeContext();
|
const {
|
||||||
|
themeMode, // 'light' | 'dark' | 'system'
|
||||||
|
colorScheme, // 'light' | 'dark'
|
||||||
|
colors, // Colors object
|
||||||
|
setThemeMode, // Change theme
|
||||||
|
getColor, // Get color by name
|
||||||
|
isHydrated, // AsyncStorage loaded
|
||||||
|
} = useThemeContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ backgroundColor: colors.background }}>
|
<View style={{ backgroundColor: colors.background }}>
|
||||||
<Text style={{ color: colors.text }}>Current theme: {colorScheme}</Text>
|
<Text style={{ color: colors.text }}>
|
||||||
|
Mode: {themeMode}, Scheme: {colorScheme}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Sử dụng App Theme Hook (Recommended)
|
### 2. useColorScheme Hook
|
||||||
|
|
||||||
|
Alias để lấy colorScheme nhanh:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
import { useColorScheme } from "@/hooks/use-theme-context";
|
||||||
|
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
const { colors, styles, utils } = useAppTheme();
|
const colorScheme = useColorScheme(); // 'light' | 'dark'
|
||||||
|
|
||||||
return (
|
return <Text>Current theme: {colorScheme}</Text>;
|
||||||
<View style={styles.container}>
|
|
||||||
<TouchableOpacity style={styles.primaryButton}>
|
|
||||||
<Text style={styles.primaryButtonText}>Button</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.surface,
|
|
||||||
{
|
|
||||||
backgroundColor: utils.getOpacityColor("primary", 0.1),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={{ color: colors.text }}>
|
|
||||||
Theme is {utils.isDark ? "Dark" : "Light"}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Sử dụng useThemeColor Hook
|
**⚠️ Lưu ý:** `useColorScheme` từ `use-theme-context.tsx`, KHÔNG phải từ `react-native`.
|
||||||
|
|
||||||
|
### 3. useThemeColor Hook
|
||||||
|
|
||||||
|
Override colors cho specific themes:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useThemeColor } from "@/hooks/use-theme-color";
|
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||||
|
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
// Override colors for specific themes
|
// Với override
|
||||||
const backgroundColor = useThemeColor(
|
const backgroundColor = useThemeColor(
|
||||||
{ light: "#ffffff", dark: "#1C1C1E" },
|
{ light: "#ffffff", dark: "#1C1C1E" },
|
||||||
"surface"
|
"surface"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Không override, dùng color từ theme
|
||||||
const textColor = useThemeColor({}, "text");
|
const textColor = useThemeColor({}, "text");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -142,9 +188,84 @@ function MyComponent() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Theme Toggle Component
|
**Cách hoạt động:**
|
||||||
|
|
||||||
Sử dụng `ThemeToggle` component để cho phép user chọn theme:
|
```typescript
|
||||||
|
// Ưu tiên props override trước, sau đó mới dùng Colors
|
||||||
|
const colorFromProps = props[colorScheme];
|
||||||
|
return colorFromProps || Colors[colorScheme][colorName];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. useAppTheme Hook (Recommended)
|
||||||
|
|
||||||
|
Hook tiện lợi với pre-built styles và utilities:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { colors, styles, utils } = useAppTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<TouchableOpacity style={styles.primaryButton}>
|
||||||
|
<Text style={styles.primaryButtonText}>Primary Button</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={{ color: colors.text }}>
|
||||||
|
Theme is {utils.isDark ? "Dark" : "Light"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: utils.getOpacityColor("primary", 0.1),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>Transparent background</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Themed Components
|
||||||
|
|
||||||
|
**ThemedView** và **ThemedText** - Tự động apply theme colors:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ThemedText } from "@/components/themed-text";
|
||||||
|
import { ThemedView } from "@/components/themed-view";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
return (
|
||||||
|
<ThemedView>
|
||||||
|
<ThemedText type="title">Title Text</ThemedText>
|
||||||
|
<ThemedText type="subtitle">Subtitle</ThemedText>
|
||||||
|
<ThemedText type="default">Regular Text</ThemedText>
|
||||||
|
<ThemedText type="link">Link Text</ThemedText>
|
||||||
|
|
||||||
|
{/* Override với custom colors */}
|
||||||
|
<ThemedText lightColor="#000000" darkColor="#FFFFFF">
|
||||||
|
Custom colored text
|
||||||
|
</ThemedText>
|
||||||
|
</ThemedView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ThemedText types:**
|
||||||
|
|
||||||
|
- `default` - 16px regular
|
||||||
|
- `title` - 32px bold
|
||||||
|
- `subtitle` - 20px bold
|
||||||
|
- `defaultSemiBold` - 16px semibold
|
||||||
|
- `link` - 16px với color #0a7ea4
|
||||||
|
|
||||||
|
### 6. Theme Toggle Component
|
||||||
|
|
||||||
|
Component có sẵn để user chọn theme:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
@@ -158,6 +279,8 @@ function SettingsScreen() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Component này hiển thị 3 options: Light, Dark, System với icons và labels đa ngôn ngữ.
|
||||||
|
|
||||||
## Available Styles từ useAppTheme
|
## Available Styles từ useAppTheme
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -165,25 +288,25 @@ const { styles } = useAppTheme();
|
|||||||
|
|
||||||
// Container styles
|
// Container styles
|
||||||
styles.container; // Flex 1 container với background
|
styles.container; // Flex 1 container với background
|
||||||
styles.surface; // Card surface với padding
|
styles.surface; // Surface với padding 16, borderRadius 12
|
||||||
styles.card; // Card với shadow và border radius
|
styles.card; // Card với shadow, elevation
|
||||||
|
|
||||||
// Button styles
|
// Button styles
|
||||||
styles.primaryButton; // Primary button style
|
styles.primaryButton; // Primary button với colors.primary
|
||||||
styles.secondaryButton; // Secondary button với border
|
styles.secondaryButton; // Secondary button với border
|
||||||
styles.primaryButtonText; // White text cho primary button
|
styles.primaryButtonText; // White text cho primary button
|
||||||
styles.secondaryButtonText; // Theme text cho secondary button
|
styles.secondaryButtonText; // Theme text cho secondary button
|
||||||
|
|
||||||
// Input styles
|
// Input styles
|
||||||
styles.textInput; // Text input với border và padding
|
styles.textInput; // Text input với border, padding
|
||||||
|
|
||||||
// Status styles
|
// Status styles
|
||||||
styles.successContainer; // Success status container
|
styles.successContainer; // Success background với border
|
||||||
styles.warningContainer; // Warning status container
|
styles.warningContainer; // Warning background với border
|
||||||
styles.errorContainer; // Error status container
|
styles.errorContainer; // Error background với border
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
styles.separator; // Line separator
|
styles.separator; // 1px line separator
|
||||||
```
|
```
|
||||||
|
|
||||||
## Theme Utilities
|
## Theme Utilities
|
||||||
@@ -191,44 +314,189 @@ styles.separator; // Line separator
|
|||||||
```typescript
|
```typescript
|
||||||
const { utils } = useAppTheme();
|
const { utils } = useAppTheme();
|
||||||
|
|
||||||
utils.isDark; // boolean - kiểm tra dark mode
|
// Check theme
|
||||||
utils.isLight; // boolean - kiểm tra light mode
|
utils.isDark; // boolean - true nếu dark mode
|
||||||
utils.toggleTheme(); // function - toggle giữa light/dark
|
utils.isLight; // boolean - true nếu light mode
|
||||||
utils.getOpacityColor(colorName, opacity); // Tạo màu với opacity
|
|
||||||
|
// Toggle theme (ignores system mode)
|
||||||
|
utils.toggleTheme(); // Switch giữa light ↔ dark
|
||||||
|
|
||||||
|
// Get color với opacity
|
||||||
|
utils.getOpacityColor("primary", 0.1); // rgba(0, 122, 255, 0.1)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lưu trữ Theme Preference
|
## Luồng hoạt động của Theme System
|
||||||
|
|
||||||
Theme preference được tự động lưu trong AsyncStorage với key `'theme_mode'`. Khi app khởi động, theme sẽ được khôi phục từ storage.
|
```
|
||||||
|
1. App khởi động
|
||||||
|
└─→ ThemeProvider mount
|
||||||
|
├─→ Load saved themeMode từ AsyncStorage ('light'/'dark'/'system')
|
||||||
|
├─→ Detect systemScheme từ OS
|
||||||
|
│ ├─→ Appearance.getColorScheme()
|
||||||
|
│ ├─→ useColorScheme() hook
|
||||||
|
│ └─→ Appearance.addChangeListener()
|
||||||
|
└─→ Tính toán colorScheme cuối cùng
|
||||||
|
└─→ themeMode === 'system' ? systemScheme : themeMode
|
||||||
|
|
||||||
|
2. User thay đổi system theme
|
||||||
|
└─→ Appearance listener fire
|
||||||
|
└─→ Update systemScheme state
|
||||||
|
└─→ Nếu themeMode === 'system'
|
||||||
|
└─→ colorScheme tự động update
|
||||||
|
└─→ Components re-render với colors mới
|
||||||
|
|
||||||
|
3. User chọn theme trong app
|
||||||
|
└─→ setThemeMode('light'/'dark'/'system')
|
||||||
|
├─→ Update themeMode state
|
||||||
|
├─→ Save vào AsyncStorage
|
||||||
|
└─→ colorScheme update
|
||||||
|
└─→ Components re-render
|
||||||
|
|
||||||
|
4. App về foreground
|
||||||
|
└─→ AppState listener fire
|
||||||
|
└─→ Sync lại systemScheme (phòng user đổi system theme khi app background)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Theme preference được lưu với key: `'theme_mode'`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Tự động xử lý bởi ThemeProvider
|
||||||
|
await setStorageItem("theme_mode", "light" | "dark" | "system");
|
||||||
|
const savedMode = await getStorageItem("theme_mode");
|
||||||
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Sử dụng `useAppTheme`** thay vì access colors trực tiếp
|
1. **Sử dụng hooks đúng context:**
|
||||||
2. **Sử dụng pre-defined styles** từ `useAppTheme().styles`
|
|
||||||
3. **Kiểm tra theme** bằng `utils.isDark` thay vì check colorScheme
|
|
||||||
4. **Sử dụng opacity colors** cho backgrounds: `utils.getOpacityColor('primary', 0.1)`
|
|
||||||
5. **Tận dụng ThemedText và ThemedView** cho các component đơn giản
|
|
||||||
|
|
||||||
## Migration từ theme cũ
|
- `useThemeContext()` - Khi cần full control (themeMode, setThemeMode)
|
||||||
|
- `useColorScheme()` - Chỉ cần biết light/dark
|
||||||
|
- `useAppTheme()` - Recommended cho UI components (có styles + utils)
|
||||||
|
- `useThemeColor()` - Khi cần override colors
|
||||||
|
|
||||||
Nếu bạn đang sử dụng theme cũ:
|
2. **Sử dụng Themed Components:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Good ✅
|
||||||
|
<ThemedView>
|
||||||
|
<ThemedText>Hello</ThemedText>
|
||||||
|
</ThemedView>;
|
||||||
|
|
||||||
|
// Also good ✅
|
||||||
|
const { colors } = useAppTheme();
|
||||||
|
<View style={{ backgroundColor: colors.background }}>
|
||||||
|
<Text style={{ color: colors.text }}>Hello</Text>
|
||||||
|
</View>;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Tận dụng pre-built styles:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Good ✅
|
||||||
|
const { styles } = useAppTheme();
|
||||||
|
<TouchableOpacity style={styles.primaryButton}>
|
||||||
|
|
||||||
|
// Less good ❌
|
||||||
|
<TouchableOpacity style={{
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 12
|
||||||
|
}}>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Sử dụng opacity colors:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { utils } = useAppTheme();
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: utils.getOpacityColor("primary", 0.1),
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Check theme correctly:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Good ✅
|
||||||
|
const { utils } = useAppTheme();
|
||||||
|
if (utils.isDark) { ... }
|
||||||
|
|
||||||
|
// Also good ✅
|
||||||
|
const { colorScheme } = useThemeContext();
|
||||||
|
if (colorScheme === 'dark') { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Theme không được lưu
|
||||||
|
|
||||||
|
- Kiểm tra AsyncStorage permissions
|
||||||
|
- Check logs trong console: `[Theme] Failed to save theme mode:`
|
||||||
|
|
||||||
|
### Flash màu sắc khi khởi động
|
||||||
|
|
||||||
|
- ThemeProvider đã xử lý với `isHydrated` state
|
||||||
|
- Chờ AsyncStorage load xong trước khi render
|
||||||
|
|
||||||
|
### System theme không update
|
||||||
|
|
||||||
|
- Check Appearance listener đã register: `[Theme] Registering appearance listener`
|
||||||
|
- Check logs: `[Theme] System theme changed to: ...`
|
||||||
|
- iOS: Restart app sau khi đổi system theme
|
||||||
|
- Android: Cần `expo-system-ui` plugin trong `app.json`
|
||||||
|
|
||||||
|
### Colors không đúng
|
||||||
|
|
||||||
|
- Đảm bảo app wrapped trong `<AppThemeProvider>`
|
||||||
|
- Check `colorScheme` trong console logs
|
||||||
|
- Verify Colors object trong `constants/theme.ts`
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
Nếu đang dùng old theme system:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// Cũ
|
// Old ❌
|
||||||
|
import { useColorScheme } from "react-native";
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const backgroundColor = colorScheme === "dark" ? "#000" : "#fff";
|
const backgroundColor = colorScheme === "dark" ? "#000" : "#fff";
|
||||||
|
|
||||||
// Mới
|
// New ✅
|
||||||
|
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const backgroundColor = colors.background;
|
const backgroundColor = colors.background;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
```tsx
|
||||||
|
// Old ❌
|
||||||
|
const [theme, setTheme] = useState("light");
|
||||||
|
|
||||||
1. **Theme không được lưu**: Kiểm tra AsyncStorage permissions
|
// New ✅
|
||||||
2. **Flash khi khởi động**: ThemeProvider sẽ chờ load theme trước khi render
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
3. **Colors không đúng**: Đảm bảo component được wrap trong ThemeProvider
|
const { themeMode, setThemeMode } = useThemeContext();
|
||||||
|
```
|
||||||
|
|
||||||
## Examples
|
## Debug Logs
|
||||||
|
|
||||||
Xem `components/theme-example.tsx` để biết các cách sử dụng theme khác nhau.
|
Enable logs để debug theme issues:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Trong use-theme-context.tsx, uncomment các dòng:
|
||||||
|
console.log("[Theme] Appearance.getColorScheme():", scheme);
|
||||||
|
console.log("[Theme] System theme changed to:", newScheme);
|
||||||
|
console.log("[Theme] Mode:", themeMode);
|
||||||
|
console.log("[Theme] Derived colorScheme:", colorScheme);
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Trong use-theme-color.ts:
|
||||||
|
console.log("Detected theme:", theme); // Đã có sẵn
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Trong _layout.tsx:
|
||||||
|
console.log("Color Scheme: ", colorScheme); // Đã có sẵn
|
||||||
|
```
|
||||||
|
|||||||
8
app.json
8
app.json
@@ -12,8 +12,6 @@
|
|||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"CFBundleLocalizations": [
|
"CFBundleLocalizations": [
|
||||||
"en",
|
|
||||||
"vi",
|
|
||||||
"en",
|
"en",
|
||||||
"vi"
|
"vi"
|
||||||
]
|
]
|
||||||
@@ -80,6 +78,12 @@
|
|||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
"reactCompiler": true
|
"reactCompiler": true
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"router": {},
|
||||||
|
"eas": {
|
||||||
|
"projectId": "d4ef1318-5427-4c96-ad88-97ec117829cc"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Tabs, useSegments } from "expo-router";
|
|||||||
import { HapticTab } from "@/components/haptic-tab";
|
import { HapticTab } from "@/components/haptic-tab";
|
||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
import { useColorScheme } from "@/hooks/use-theme-context";
|
||||||
import { startEvents, stopEvents } from "@/services/device_events";
|
import { startEvents, stopEvents } from "@/services/device_events";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { StyleSheet, View, ScrollView } from "react-native";
|
import { ScrollView, StyleSheet, View } from "react-native";
|
||||||
|
|
||||||
import EnIcon from "@/assets/icons/en_icon.png";
|
import EnIcon from "@/assets/icons/en_icon.png";
|
||||||
import VnIcon from "@/assets/icons/vi_icon.png";
|
import VnIcon from "@/assets/icons/vi_icon.png";
|
||||||
import RotateSwitch from "@/components/rotate-switch";
|
import RotateSwitch from "@/components/rotate-switch";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { ThemedText } from "@/components/themed-text";
|
import { ThemedText } from "@/components/themed-text";
|
||||||
import { ThemedView } from "@/components/themed-view";
|
import { ThemedView } from "@/components/themed-view";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
|
||||||
import { DOMAIN, TOKEN } from "@/constants";
|
import { DOMAIN, TOKEN } from "@/constants";
|
||||||
|
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
||||||
import { removeStorageItem } from "@/utils/storage";
|
import { removeStorageItem } from "@/utils/storage";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
type Todo = {
|
type Todo = {
|
||||||
@@ -24,7 +24,7 @@ export default function SettingScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [data, setData] = useState<Todo | null>(null);
|
const [data, setData] = useState<Todo | null>(null);
|
||||||
const { t, locale, setLocale } = useI18n();
|
const { t, locale, setLocale } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useAppTheme();
|
||||||
const [isEnabled, setIsEnabled] = useState(locale === "vi");
|
const [isEnabled, setIsEnabled] = useState(locale === "vi");
|
||||||
|
|
||||||
// Sync isEnabled state khi locale thay đổi
|
// Sync isEnabled state khi locale thay đổi
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ import CrewListTable from "@/components/tripInfo/CrewListTable";
|
|||||||
import FishingToolsTable from "@/components/tripInfo/FishingToolsList";
|
import FishingToolsTable from "@/components/tripInfo/FishingToolsList";
|
||||||
import NetListTable from "@/components/tripInfo/NetListTable";
|
import NetListTable from "@/components/tripInfo/NetListTable";
|
||||||
import TripCostTable from "@/components/tripInfo/TripCostTable";
|
import TripCostTable from "@/components/tripInfo/TripCostTable";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function TripInfoScreen() {
|
export default function TripInfoScreen() {
|
||||||
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={[styles.titleText, { color: colors.text }]}>
|
<Text style={[styles.titleText, { color: colors.text }]}>
|
||||||
Thông Tin Chuyến Đi
|
{t("trip.infoTrip")}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.buttonWrapper}>
|
<View style={styles.buttonWrapper}>
|
||||||
<ButtonCreateNewHaulOrTrip />
|
<ButtonCreateNewHaulOrTrip />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DarkTheme,
|
DarkTheme,
|
||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
ThemeProvider as NavigationThemeProvider,
|
ThemeProvider,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack, useRouter } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
@@ -9,65 +9,62 @@ import { useEffect } from "react";
|
|||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
// import Toast from "react-native-toast-message";
|
// 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 { toastConfig } from "@/config";
|
||||||
import { setRouterInstance } from "@/config/auth";
|
import { setRouterInstance } from "@/config/auth";
|
||||||
import "@/global.css";
|
import "@/global.css";
|
||||||
import { ThemeProvider, useThemeContext } from "@/hooks/use-theme-context";
|
|
||||||
import { I18nProvider } from "@/hooks/use-i18n";
|
import { I18nProvider } from "@/hooks/use-i18n";
|
||||||
|
import { ThemeProvider as AppThemeProvider, useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const { colorScheme } = useThemeContext();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { colorScheme } = useThemeContext();
|
||||||
|
console.log("Color Scheme: ", colorScheme);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRouterInstance(router);
|
setRouterInstance(router);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GluestackUIProvider mode={colorScheme}>
|
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||||
<NavigationThemeProvider
|
<Stack
|
||||||
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
screenOptions={{ headerShown: false }}
|
||||||
|
initialRouteName="auth/login"
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack.Screen
|
||||||
screenOptions={{ headerShown: false }}
|
name="auth/login"
|
||||||
initialRouteName="auth/login"
|
options={{
|
||||||
>
|
title: "Login",
|
||||||
<Stack.Screen
|
headerShown: false,
|
||||||
name="auth/login"
|
}}
|
||||||
options={{
|
/>
|
||||||
title: "Login",
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="(tabs)"
|
name="(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Home",
|
title: "Home",
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="modal"
|
name="modal"
|
||||||
options={{ presentation: "formSheet", title: "Modal" }}
|
options={{ presentation: "formSheet", title: "Modal" }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
||||||
</NavigationThemeProvider>
|
</ThemeProvider>
|
||||||
</GluestackUIProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ThemeProvider>
|
<AppThemeProvider>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
</ThemeProvider>
|
</AppThemeProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ import { ThemedText } from "@/components/themed-text";
|
|||||||
import { ThemedView } from "@/components/themed-view";
|
import { ThemedView } from "@/components/themed-view";
|
||||||
import SliceSwitch from "@/components/ui/slice-switch";
|
import SliceSwitch from "@/components/ui/slice-switch";
|
||||||
import { DOMAIN, TOKEN } from "@/constants";
|
import { DOMAIN, TOKEN } from "@/constants";
|
||||||
|
import { Colors } from "@/constants/theme";
|
||||||
import { login } from "@/controller/AuthController";
|
import { login } from "@/controller/AuthController";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
import {
|
||||||
|
ColorScheme as ThemeColorScheme,
|
||||||
|
useTheme,
|
||||||
|
useThemeContext,
|
||||||
|
} from "@/hooks/use-theme-context";
|
||||||
import { showErrorToast, showWarningToast } from "@/services/toast_service";
|
import { showErrorToast, showWarningToast } from "@/services/toast_service";
|
||||||
import {
|
import {
|
||||||
getStorageItem,
|
getStorageItem,
|
||||||
@@ -17,7 +23,7 @@ import {
|
|||||||
import { parseJwtToken } from "@/utils/token";
|
import { parseJwtToken } from "@/utils/token";
|
||||||
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Image,
|
Image,
|
||||||
@@ -38,6 +44,14 @@ export default function LoginScreen() {
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
|
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
|
||||||
const { t, setLocale, locale } = useI18n();
|
const { t, setLocale, locale } = useI18n();
|
||||||
|
const { colors, colorScheme } = useTheme();
|
||||||
|
const { setThemeMode } = useThemeContext();
|
||||||
|
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 [isVNLang, setIsVNLang] = useState(false);
|
||||||
|
|
||||||
const checkLogin = useCallback(async () => {
|
const checkLogin = useCallback(async () => {
|
||||||
@@ -154,7 +168,10 @@ export default function LoginScreen() {
|
|||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
<ScrollView
|
||||||
|
style={{ flex: 1, backgroundColor: colors.background }}
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
>
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
@@ -177,7 +194,7 @@ export default function LoginScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder={t("auth.username_placeholder")}
|
placeholder={t("auth.username_placeholder")}
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor={placeholderColor}
|
||||||
value={username}
|
value={username}
|
||||||
onChangeText={setUsername}
|
onChangeText={setUsername}
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
@@ -192,7 +209,7 @@ export default function LoginScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder={t("auth.password_placeholder")}
|
placeholder={t("auth.password_placeholder")}
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor={placeholderColor}
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
secureTextEntry={!showPassword}
|
secureTextEntry={!showPassword}
|
||||||
@@ -217,7 +234,7 @@ export default function LoginScreen() {
|
|||||||
<Ionicons
|
<Ionicons
|
||||||
name={showPassword ? "eye-off" : "eye"}
|
name={showPassword ? "eye-off" : "eye"}
|
||||||
size={22}
|
size={22}
|
||||||
color="#666"
|
color={colors.icon}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -241,9 +258,13 @@ export default function LoginScreen() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator color="#fff" size="small" />
|
<ActivityIndicator color={buttonTextColor} size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.loginButtonText}>{t("auth.login")}</Text>
|
<Text
|
||||||
|
style={[styles.loginButtonText, { color: buttonTextColor }]}
|
||||||
|
>
|
||||||
|
{t("auth.login")}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -252,10 +273,10 @@ export default function LoginScreen() {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderColor: "#ddd",
|
borderColor: colors.border,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: "transparent",
|
backgroundColor: colors.surface,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
@@ -265,7 +286,7 @@ export default function LoginScreen() {
|
|||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="qr-code-scanner"
|
name="qr-code-scanner"
|
||||||
size={28}
|
size={28}
|
||||||
color="#007AFF"
|
color={colors.primary}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -282,13 +303,21 @@ export default function LoginScreen() {
|
|||||||
<SliceSwitch
|
<SliceSwitch
|
||||||
size="sm"
|
size="sm"
|
||||||
leftIcon="moon"
|
leftIcon="moon"
|
||||||
leftIconColor="white"
|
leftIconColor={
|
||||||
|
colorScheme === "dark" ? colors.background : colors.surface
|
||||||
|
}
|
||||||
rightIcon="sunny"
|
rightIcon="sunny"
|
||||||
rightIconColor="orange"
|
rightIconColor={
|
||||||
activeBackgroundColor="black"
|
colorScheme === "dark" ? colors.warning : "orange"
|
||||||
inactiveBackgroundColor="white"
|
}
|
||||||
inactiveOverlayColor="black"
|
activeBackgroundColor={colors.text}
|
||||||
activeOverlayColor="white"
|
inactiveBackgroundColor={colors.surface}
|
||||||
|
inactiveOverlayColor={colors.textSecondary}
|
||||||
|
activeOverlayColor={colors.background}
|
||||||
|
value={colorScheme === "light"}
|
||||||
|
onChange={(val) => {
|
||||||
|
setThemeMode(val ? "light" : "dark");
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -310,129 +339,130 @@ export default function LoginScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const createStyles = (colors: typeof Colors.light, scheme: ThemeColorScheme) =>
|
||||||
scrollContainer: {
|
StyleSheet.create({
|
||||||
flexGrow: 1,
|
scrollContainer: {
|
||||||
justifyContent: "center",
|
flexGrow: 1,
|
||||||
},
|
justifyContent: "center",
|
||||||
container: {
|
backgroundColor: colors.background,
|
||||||
flex: 1,
|
},
|
||||||
paddingHorizontal: 20,
|
container: {
|
||||||
justifyContent: "center",
|
flex: 1,
|
||||||
},
|
paddingHorizontal: 20,
|
||||||
headerContainer: {
|
justifyContent: "center",
|
||||||
marginBottom: 40,
|
},
|
||||||
alignItems: "center",
|
headerContainer: {
|
||||||
},
|
marginBottom: 40,
|
||||||
logo: {
|
alignItems: "center",
|
||||||
width: 120,
|
},
|
||||||
height: 120,
|
logo: {
|
||||||
marginBottom: 20,
|
width: 120,
|
||||||
},
|
height: 120,
|
||||||
title: {
|
marginBottom: 20,
|
||||||
fontSize: 28,
|
},
|
||||||
fontWeight: "bold",
|
title: {
|
||||||
marginBottom: 8,
|
fontSize: 28,
|
||||||
},
|
fontWeight: "bold",
|
||||||
subtitle: {
|
marginBottom: 8,
|
||||||
fontSize: 16,
|
},
|
||||||
opacity: 0.7,
|
subtitle: {
|
||||||
},
|
fontSize: 16,
|
||||||
formContainer: {
|
opacity: 0.7,
|
||||||
gap: 16,
|
},
|
||||||
},
|
formContainer: {
|
||||||
inputGroup: {
|
gap: 16,
|
||||||
gap: 8,
|
},
|
||||||
},
|
inputGroup: {
|
||||||
label: {
|
gap: 8,
|
||||||
fontSize: 14,
|
},
|
||||||
fontWeight: "600",
|
label: {
|
||||||
},
|
fontSize: 14,
|
||||||
input: {
|
fontWeight: "600",
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: "#ddd",
|
input: {
|
||||||
borderRadius: 8,
|
borderWidth: 1,
|
||||||
paddingHorizontal: 12,
|
borderColor: colors.border,
|
||||||
paddingVertical: 12,
|
borderRadius: 8,
|
||||||
fontSize: 16,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: "#f5f5f5",
|
paddingVertical: 12,
|
||||||
color: "#000",
|
fontSize: 16,
|
||||||
},
|
backgroundColor: colors.surface,
|
||||||
loginButton: {
|
color: colors.text,
|
||||||
backgroundColor: "#007AFF",
|
},
|
||||||
paddingVertical: 14,
|
loginButton: {
|
||||||
borderRadius: 8,
|
backgroundColor: colors.primary,
|
||||||
alignItems: "center",
|
paddingVertical: 14,
|
||||||
marginTop: 16,
|
borderRadius: 8,
|
||||||
},
|
alignItems: "center",
|
||||||
loginButtonDisabled: {
|
marginTop: 16,
|
||||||
opacity: 0.6,
|
},
|
||||||
},
|
loginButtonDisabled: {
|
||||||
loginButtonText: {
|
opacity: 0.6,
|
||||||
color: "#fff",
|
},
|
||||||
fontSize: 16,
|
loginButtonText: {
|
||||||
fontWeight: "600",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
footerContainer: {
|
},
|
||||||
marginTop: 16,
|
footerContainer: {
|
||||||
alignItems: "center",
|
marginTop: 16,
|
||||||
},
|
alignItems: "center",
|
||||||
footerText: {
|
},
|
||||||
fontSize: 14,
|
footerText: {
|
||||||
},
|
fontSize: 14,
|
||||||
linkText: {
|
},
|
||||||
color: "#007AFF",
|
linkText: {
|
||||||
fontWeight: "600",
|
color: colors.primary,
|
||||||
},
|
fontWeight: "600",
|
||||||
copyrightContainer: {
|
},
|
||||||
marginTop: 20,
|
copyrightContainer: {
|
||||||
alignItems: "center",
|
marginTop: 20,
|
||||||
},
|
alignItems: "center",
|
||||||
copyrightText: {
|
},
|
||||||
fontSize: 12,
|
copyrightText: {
|
||||||
opacity: 0.6,
|
fontSize: 12,
|
||||||
textAlign: "center",
|
opacity: 0.6,
|
||||||
},
|
textAlign: "center",
|
||||||
languageSwitcherContainer: {
|
color: colors.textSecondary,
|
||||||
display: "flex",
|
},
|
||||||
flexDirection: "row",
|
languageSwitcherContainer: {
|
||||||
justifyContent: "center",
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
marginTop: 24,
|
alignItems: "center",
|
||||||
gap: 20,
|
marginTop: 24,
|
||||||
},
|
gap: 20,
|
||||||
languageSwitcherLabel: {
|
},
|
||||||
fontSize: 12,
|
languageSwitcherLabel: {
|
||||||
fontWeight: "600",
|
fontSize: 12,
|
||||||
textAlign: "center",
|
fontWeight: "600",
|
||||||
opacity: 0.8,
|
textAlign: "center",
|
||||||
},
|
opacity: 0.8,
|
||||||
languageButtonsContainer: {
|
},
|
||||||
flexDirection: "row",
|
languageButtonsContainer: {
|
||||||
gap: 10,
|
flexDirection: "row",
|
||||||
},
|
gap: 10,
|
||||||
languageButton: {
|
},
|
||||||
flex: 1,
|
languageButton: {
|
||||||
paddingVertical: 10,
|
flex: 1,
|
||||||
paddingHorizontal: 12,
|
paddingVertical: 10,
|
||||||
borderWidth: 1,
|
paddingHorizontal: 12,
|
||||||
borderColor: "#ddd",
|
borderWidth: 1,
|
||||||
borderRadius: 8,
|
borderColor: colors.border,
|
||||||
alignItems: "center",
|
borderRadius: 8,
|
||||||
backgroundColor: "transparent",
|
alignItems: "center",
|
||||||
},
|
backgroundColor: colors.surface,
|
||||||
languageButtonActive: {
|
},
|
||||||
backgroundColor: "#007AFF",
|
languageButtonActive: {
|
||||||
borderColor: "#007AFF",
|
backgroundColor: colors.primary,
|
||||||
},
|
borderColor: colors.primary,
|
||||||
languageButtonText: {
|
},
|
||||||
fontSize: 14,
|
languageButtonText: {
|
||||||
fontWeight: "500",
|
fontSize: 14,
|
||||||
color: "#666",
|
fontWeight: "500",
|
||||||
},
|
color: colors.textSecondary,
|
||||||
languageButtonTextActive: {
|
},
|
||||||
color: "#fff",
|
languageButtonTextActive: {
|
||||||
fontWeight: "600",
|
color: scheme === "dark" ? colors.text : colors.surface,
|
||||||
},
|
fontWeight: "600",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -93,6 +94,12 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
|
|
||||||
const sz = sizeMap[size];
|
const sz = sizeMap[size];
|
||||||
|
|
||||||
|
// Theme colors from context (consistent with other components)
|
||||||
|
const { colors } = useThemeContext();
|
||||||
|
const selectBackgroundColor = disabled
|
||||||
|
? colors.backgroundSecondary
|
||||||
|
: colors.surface;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.wrapper}>
|
<View style={styles.wrapper}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -101,7 +108,8 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
{
|
{
|
||||||
height: sz.height,
|
height: sz.height,
|
||||||
paddingHorizontal: sz.paddingHorizontal,
|
paddingHorizontal: sz.paddingHorizontal,
|
||||||
opacity: disabled ? 0.6 : 1,
|
backgroundColor: selectBackgroundColor,
|
||||||
|
borderColor: disabled ? colors.border : colors.primary,
|
||||||
},
|
},
|
||||||
style,
|
style,
|
||||||
]}
|
]}
|
||||||
@@ -112,14 +120,18 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
>
|
>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator size="small" color="#4ecdc4" />
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.text,
|
styles.text,
|
||||||
{
|
{
|
||||||
fontSize: sz.fontSize,
|
fontSize: sz.fontSize,
|
||||||
color: selectedValue ? "#111" : "#999",
|
color: disabled
|
||||||
|
? colors.textSecondary
|
||||||
|
: selectedValue
|
||||||
|
? colors.text
|
||||||
|
: colors.textSecondary,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
@@ -131,24 +143,41 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
<View style={styles.suffix}>
|
<View style={styles.suffix}>
|
||||||
{allowClear && selectedValue && !loading ? (
|
{allowClear && selectedValue && !loading ? (
|
||||||
<TouchableOpacity onPress={handleClear} style={styles.icon}>
|
<TouchableOpacity onPress={handleClear} style={styles.icon}>
|
||||||
<AntDesign name="close" size={16} color="#999" />
|
<AntDesign name="close" size={16} color={colors.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : null}
|
) : null}
|
||||||
<AntDesign
|
<AntDesign
|
||||||
name={isOpen ? "up" : "down"}
|
name={isOpen ? "up" : "down"}
|
||||||
size={14}
|
size={14}
|
||||||
color="#999"
|
color={colors.textSecondary}
|
||||||
style={styles.arrow}
|
style={styles.arrow}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<View style={[styles.dropdown, { top: containerHeight }]}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.dropdown,
|
||||||
|
{
|
||||||
|
top: containerHeight,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderColor: colors.border,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.searchInput}
|
style={[
|
||||||
|
styles.searchInput,
|
||||||
|
{
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderColor: colors.border,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
]}
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChangeText={setSearchText}
|
onChangeText={setSearchText}
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -160,8 +189,13 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
key={item.value}
|
key={item.value}
|
||||||
style={[
|
style={[
|
||||||
styles.option,
|
styles.option,
|
||||||
|
{
|
||||||
|
borderBottomColor: colors.separator,
|
||||||
|
},
|
||||||
item.disabled && styles.optionDisabled,
|
item.disabled && styles.optionDisabled,
|
||||||
selectedValue === item.value && styles.optionSelected,
|
selectedValue === item.value && {
|
||||||
|
backgroundColor: colors.primary + "20", // Add transparency to primary color
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => !item.disabled && handleSelect(item.value)}
|
onPress={() => !item.disabled && handleSelect(item.value)}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
@@ -169,14 +203,22 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.optionText,
|
styles.optionText,
|
||||||
item.disabled && styles.optionTextDisabled,
|
{
|
||||||
selectedValue === item.value && styles.optionTextSelected,
|
color: colors.text,
|
||||||
|
},
|
||||||
|
item.disabled && {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
selectedValue === item.value && {
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Text>
|
</Text>
|
||||||
{selectedValue === item.value && (
|
{selectedValue === item.value && (
|
||||||
<AntDesign name="check" size={16} color="#4ecdc4" />
|
<AntDesign name="check" size={16} color={colors.primary} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
@@ -193,9 +235,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#e6e6e6",
|
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: "#fff",
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
@@ -204,7 +244,7 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
color: "#111",
|
// Color is set dynamically via theme
|
||||||
},
|
},
|
||||||
suffix: {
|
suffix: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -220,9 +260,7 @@ const styles = StyleSheet.create({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
backgroundColor: "#fff",
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#e6e6e6",
|
|
||||||
borderTopWidth: 0,
|
borderTopWidth: 0,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
borderBottomLeftRadius: 8,
|
borderBottomLeftRadius: 8,
|
||||||
@@ -236,7 +274,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
searchInput: {
|
searchInput: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#e6e6e6",
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
padding: 8,
|
padding: 8,
|
||||||
margin: 8,
|
margin: 8,
|
||||||
@@ -247,7 +284,6 @@ const styles = StyleSheet.create({
|
|||||||
option: {
|
option: {
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -255,20 +291,11 @@ const styles = StyleSheet.create({
|
|||||||
optionDisabled: {
|
optionDisabled: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
optionSelected: {
|
// optionSelected is handled dynamically via inline styles
|
||||||
backgroundColor: "#f6ffed",
|
|
||||||
},
|
|
||||||
optionText: {
|
optionText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: "#111",
|
|
||||||
},
|
|
||||||
optionTextDisabled: {
|
|
||||||
color: "#999",
|
|
||||||
},
|
|
||||||
optionTextSelected: {
|
|
||||||
color: "#4ecdc4",
|
|
||||||
fontWeight: "600",
|
|
||||||
},
|
},
|
||||||
|
// optionTextDisabled and optionTextSelected are handled dynamically via inline styles
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Select;
|
export default Select;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Text, View } from "react-native";
|
import { ThemedText } from "@/components/themed-text";
|
||||||
|
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
interface DescriptionProps {
|
interface DescriptionProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -8,10 +10,15 @@ export const Description = ({
|
|||||||
title = "",
|
title = "",
|
||||||
description = "",
|
description = "",
|
||||||
}: DescriptionProps) => {
|
}: DescriptionProps) => {
|
||||||
|
const { colors } = useAppTheme();
|
||||||
return (
|
return (
|
||||||
<View className="flex-row gap-2 ">
|
<View className="flex-row gap-2 ">
|
||||||
<Text className="opacity-50 text-lg">{title}:</Text>
|
<ThemedText
|
||||||
<Text className="text-lg">{description}</Text>
|
style={{ color: colors.textSecondary, fontSize: 16 }}
|
||||||
|
>
|
||||||
|
{title}:
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={{ fontSize: 16 }}>{description}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
import { convertToDMS, kmhToKnot } from "@/utils/geom";
|
import { convertToDMS, kmhToKnot } from "@/utils/geom";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
@@ -15,6 +16,8 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
|||||||
const translateY = useRef(new Animated.Value(0)).current;
|
const translateY = useRef(new Animated.Value(0)).current;
|
||||||
const blockBottom = useRef(new Animated.Value(0)).current;
|
const blockBottom = useRef(new Animated.Value(0)).current;
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { colors, styles } = useAppTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Animated.timing(translateY, {
|
Animated.timing(translateY, {
|
||||||
toValue: isExpanded ? 0 : 200, // Dịch chuyển xuống 200px khi thu gọn
|
toValue: isExpanded ? 0 : 200, // Dịch chuyển xuống 200px khi thu gọn
|
||||||
@@ -44,45 +47,35 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: blockBottom,
|
bottom: blockBottom,
|
||||||
left: 5,
|
left: 5,
|
||||||
// width: 48,
|
|
||||||
// height: 48,
|
|
||||||
// backgroundColor: "blue",
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
zIndex: 30,
|
zIndex: 30,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ButtonCreateNewHaulOrTrip gpsData={gpsData} />
|
<ButtonCreateNewHaulOrTrip gpsData={gpsData} />
|
||||||
{/* <TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
// showInfoToast("oad");
|
|
||||||
showWarningToast("This is a warning toast!");
|
|
||||||
}}
|
|
||||||
className="absolute top-2 right-2 z-10 bg-white rounded-full p-1"
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name={isExpanded ? "close" : "close"}
|
|
||||||
size={20}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity> */}
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={[
|
||||||
transform: [{ translateY }],
|
styles.card,
|
||||||
}}
|
{
|
||||||
className="absolute bottom-0 gap-3 right-0 p-3 left-0 h-auto w-full rounded-t-xl bg-white shadow-md"
|
transform: [{ translateY }],
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderRadius: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
className="absolute bottom-0 gap-5 right-0 px-4 pt-12 pb-2 left-0 h-auto w-full rounded-t-xl shadow-md"
|
||||||
onLayout={(event) => setPanelHeight(event.nativeEvent.layout.height)}
|
onLayout={(event) => setPanelHeight(event.nativeEvent.layout.height)}
|
||||||
>
|
>
|
||||||
{/* Nút toggle ở top-right */}
|
{/* Nút toggle ở top-right */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={togglePanel}
|
onPress={togglePanel}
|
||||||
className="absolute top-2 right-2 z-10 bg-white rounded-full p-1"
|
className="absolute top-2 right-2 z-10 rounded-full p-1"
|
||||||
|
style={{ backgroundColor: colors.card }}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={isExpanded ? "close" : "close"}
|
name={isExpanded ? "close" : "close"}
|
||||||
size={20}
|
size={20}
|
||||||
color="#666"
|
color={colors.icon}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -120,9 +113,10 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
|||||||
{!isExpanded && (
|
{!isExpanded && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={togglePanel}
|
onPress={togglePanel}
|
||||||
className="absolute bottom-5 right-2 z-20 bg-white rounded-full p-2 shadow-lg"
|
className="absolute bottom-5 right-2 z-20 rounded-full p-2 shadow-lg"
|
||||||
|
style={{ backgroundColor: colors.card }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="info-outline" size={24} />
|
<MaterialIcons name="info-outline" size={24} color={colors.icon} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,24 +8,11 @@ import { showErrorToast } from "@/services/toast_service";
|
|||||||
import { sosMessage } from "@/utils/sosUtils";
|
import { sosMessage } from "@/utils/sosUtils";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import { StyleSheet, Text, TextInput, View } from "react-native";
|
||||||
FlatList,
|
import IconButton from "../IconButton";
|
||||||
ScrollView,
|
import Select from "../Select";
|
||||||
StyleSheet,
|
import Modal from "../ui/modal";
|
||||||
Text,
|
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||||
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";
|
|
||||||
|
|
||||||
const SosButton = () => {
|
const SosButton = () => {
|
||||||
const [sosData, setSosData] = useState<Model.SosResponse | null>();
|
const [sosData, setSosData] = useState<Model.SosResponse | null>();
|
||||||
@@ -34,17 +21,33 @@ const SosButton = () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [customMessage, setCustomMessage] = useState("");
|
const [customMessage, setCustomMessage] = useState("");
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// Theme colors
|
||||||
|
const textColor = useThemeColor({}, 'text');
|
||||||
|
const borderColor = useThemeColor({}, 'border');
|
||||||
|
const errorColor = useThemeColor({}, 'error');
|
||||||
|
const backgroundColor = useThemeColor({}, 'background');
|
||||||
|
|
||||||
|
// Dynamic styles
|
||||||
|
const styles = SosButtonStyles(textColor, borderColor, errorColor, backgroundColor);
|
||||||
|
|
||||||
const sosOptions = [
|
const sosOptions = [
|
||||||
...sosMessage.map((msg) => ({ ma: msg.ma, moTa: msg.moTa })),
|
...sosMessage.map((msg) => ({
|
||||||
{ ma: 999, moTa: "Khác" },
|
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 () => {
|
const getSosData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await queryGetSos();
|
const response = await queryGetSos();
|
||||||
|
// console.log("SoS ResponseL: ", response);
|
||||||
|
|
||||||
setSosData(response.data);
|
setSosData(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch SOS data:", error);
|
console.error("Failed to fetch SOS data:", error);
|
||||||
@@ -58,8 +61,6 @@ const SosButton = () => {
|
|||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: { [key: string]: string } = {};
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
|
||||||
// Không cần validate sosMessage vì luôn có default value (11)
|
|
||||||
|
|
||||||
if (selectedSosMessage === 999 && customMessage.trim() === "") {
|
if (selectedSosMessage === 999 && customMessage.trim() === "") {
|
||||||
newErrors.customMessage = t("home.sos.statusRequired");
|
newErrors.customMessage = t("home.sos.statusRequired");
|
||||||
}
|
}
|
||||||
@@ -69,27 +70,34 @@ const SosButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmSos = async () => {
|
const handleConfirmSos = async () => {
|
||||||
if (validateForm()) {
|
if (!validateForm()) {
|
||||||
let messageToSend = "";
|
console.log("Form chưa validate");
|
||||||
if (selectedSosMessage === 999) {
|
return; // Không đóng modal nếu validate fail
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
const handleClickButton = async (isActive: boolean) => {
|
||||||
|
console.log("Is Active: ", isActive);
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
const resp = await queryDeleteSos();
|
const resp = await queryDeleteSos();
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
@@ -115,172 +123,96 @@ const SosButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<IconButton
|
||||||
className="shadow-md rounded-full"
|
icon={<MaterialIcons name="warning" size={20} color="white" />}
|
||||||
size="lg"
|
type="danger"
|
||||||
action="negative"
|
size="middle"
|
||||||
onPress={() => handleClickButton(sosData?.active || false)}
|
onPress={() => handleClickButton(sosData?.active || false)}
|
||||||
|
style={{ borderRadius: 20 }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="warning" size={15} color="white" />
|
{sosData?.active ? t("home.sos.active") : t("home.sos.inactive")}
|
||||||
<ButtonText className="text-center">
|
</IconButton>
|
||||||
{sosData?.active ? t("home.sos.active") : t("home.sos.inactive")}
|
|
||||||
</ButtonText>
|
|
||||||
{/* <ButtonSpinner /> */}
|
|
||||||
{/* <ButtonIcon /> */}
|
|
||||||
</Button>
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showConfirmSosDialog}
|
open={showConfirmSosDialog}
|
||||||
onClose={() => {
|
onCancel={() => {
|
||||||
setShowConfirmSosDialog(false);
|
setShowConfirmSosDialog(false);
|
||||||
setSelectedSosMessage(null);
|
setSelectedSosMessage(null);
|
||||||
setCustomMessage("");
|
setCustomMessage("");
|
||||||
setErrors({});
|
setErrors({});
|
||||||
}}
|
}}
|
||||||
|
okText={t("home.sos.confirm")}
|
||||||
|
cancelText={t("home.sos.cancel")}
|
||||||
|
title={t("home.sos.title")}
|
||||||
|
centered
|
||||||
|
onOk={handleConfirmSos}
|
||||||
>
|
>
|
||||||
<ModalBackdrop />
|
{/* Select Nội dung SOS */}
|
||||||
<ModalContent>
|
<View style={styles.formGroup}>
|
||||||
<ModalHeader className="flex-col gap-0.5 items-center">
|
<Text style={styles.label}>{t("home.sos.content")}</Text>
|
||||||
<Text
|
|
||||||
style={{ fontSize: 18, fontWeight: "bold", textAlign: "center" }}
|
|
||||||
>
|
|
||||||
{t("home.sos.title")}
|
|
||||||
</Text>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody className="mb-4">
|
|
||||||
<ScrollView style={{ maxHeight: 400 }}>
|
|
||||||
{/* Dropdown Nội dung SOS */}
|
|
||||||
<View style={styles.formGroup}>
|
|
||||||
<Text style={styles.label}>{t("home.sos.content")}</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.dropdownButton,
|
|
||||||
errors.sosMessage ? styles.errorBorder : {},
|
|
||||||
]}
|
|
||||||
onPress={() => setShowDropdown(!showDropdown)}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.dropdownButtonText,
|
|
||||||
!selectedSosMessage && styles.placeholderText,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{selectedSosMessage !== null
|
|
||||||
? sosOptions.find((opt) => opt.ma === selectedSosMessage)
|
|
||||||
?.moTa || t("home.sos.selectReason")
|
|
||||||
: t("home.sos.selectReason")}
|
|
||||||
</Text>
|
|
||||||
<MaterialIcons
|
|
||||||
name={showDropdown ? "expand-less" : "expand-more"}
|
|
||||||
size={20}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
{errors.sosMessage && (
|
|
||||||
<Text style={styles.errorText}>{errors.sosMessage}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Input Custom Message nếu chọn "Khác" */}
|
<Select
|
||||||
{selectedSosMessage === 999 && (
|
value={selectedSosMessage ?? undefined}
|
||||||
<View style={styles.formGroup}>
|
options={sosOptions}
|
||||||
<Text style={styles.label}>{t("home.sos.statusInput")}</Text>
|
placeholder={t("home.sos.selectReason")}
|
||||||
<TextInput
|
onChange={(value) => {
|
||||||
style={[
|
setSelectedSosMessage(value as number);
|
||||||
styles.input,
|
// Clear custom message nếu chọn khác lý do
|
||||||
errors.customMessage ? styles.errorInput : {},
|
if (value !== 999) {
|
||||||
]}
|
|
||||||
placeholder={t("home.sos.enterStatus")}
|
|
||||||
placeholderTextColor="#999"
|
|
||||||
value={customMessage}
|
|
||||||
onChangeText={(text) => {
|
|
||||||
setCustomMessage(text);
|
|
||||||
if (text.trim() !== "") {
|
|
||||||
setErrors((prev) => {
|
|
||||||
const newErrors = { ...prev };
|
|
||||||
delete newErrors.customMessage;
|
|
||||||
return newErrors;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
multiline
|
|
||||||
numberOfLines={4}
|
|
||||||
/>
|
|
||||||
{errors.customMessage && (
|
|
||||||
<Text style={styles.errorText}>{errors.customMessage}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter className="flex-row items-start gap-2">
|
|
||||||
<Button
|
|
||||||
onPress={handleConfirmSos}
|
|
||||||
// className="w-1/3"
|
|
||||||
action="negative"
|
|
||||||
>
|
|
||||||
<ButtonText>{t("home.sos.confirm")}</ButtonText>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
setShowConfirmSosDialog(false);
|
|
||||||
setSelectedSosMessage(null);
|
|
||||||
setCustomMessage("");
|
setCustomMessage("");
|
||||||
setErrors({});
|
}
|
||||||
}}
|
// Clear error if exists
|
||||||
// className="w-1/3"
|
if (errors.sosMessage) {
|
||||||
action="secondary"
|
setErrors((prev) => {
|
||||||
>
|
const newErrors = { ...prev };
|
||||||
<ButtonText>{t("home.sos.cancel")}</ButtonText>
|
delete newErrors.sosMessage;
|
||||||
</Button>
|
return newErrors;
|
||||||
</ModalFooter>
|
});
|
||||||
</ModalContent>
|
}
|
||||||
</Modal>
|
}}
|
||||||
|
showSearch={false}
|
||||||
|
style={[errors.sosMessage ? styles.errorBorder : undefined]}
|
||||||
|
/>
|
||||||
|
{errors.sosMessage && (
|
||||||
|
<Text style={styles.errorText}>{errors.sosMessage}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Dropdown Modal - Nổi lên */}
|
{/* Input Custom Message nếu chọn "Khác" */}
|
||||||
{showDropdown && showConfirmSosDialog && (
|
{selectedSosMessage === 999 && (
|
||||||
<Modal isOpen={showDropdown} onClose={() => setShowDropdown(false)}>
|
<View style={styles.formGroup}>
|
||||||
<TouchableOpacity
|
<Text style={styles.label}>{t("home.sos.statusInput")}</Text>
|
||||||
style={styles.dropdownOverlay}
|
<TextInput
|
||||||
activeOpacity={1}
|
style={[
|
||||||
onPress={() => setShowDropdown(false)}
|
styles.input,
|
||||||
>
|
errors.customMessage ? styles.errorInput : {},
|
||||||
<View style={styles.dropdownModalContainer}>
|
]}
|
||||||
<FlatList
|
placeholder={t("home.sos.enterStatus")}
|
||||||
data={sosOptions}
|
placeholderTextColor={textColor + '99'} // Add transparency
|
||||||
keyExtractor={(item) => item.ma.toString()}
|
value={customMessage}
|
||||||
renderItem={({ item }) => (
|
onChangeText={(text) => {
|
||||||
<TouchableOpacity
|
setCustomMessage(text);
|
||||||
style={styles.dropdownModalItem}
|
if (text.trim() !== "") {
|
||||||
onPress={() => {
|
setErrors((prev) => {
|
||||||
setSelectedSosMessage(item.ma);
|
const newErrors = { ...prev };
|
||||||
setShowDropdown(false);
|
delete newErrors.customMessage;
|
||||||
// Clear custom message nếu chọn khác lý do
|
return newErrors;
|
||||||
if (item.ma !== 999) {
|
});
|
||||||
setCustomMessage("");
|
}
|
||||||
}
|
}}
|
||||||
}}
|
multiline
|
||||||
>
|
numberOfLines={4}
|
||||||
<Text
|
/>
|
||||||
style={[
|
{errors.customMessage && (
|
||||||
styles.dropdownModalItemText,
|
<Text style={styles.errorText}>{errors.customMessage}</Text>
|
||||||
selectedSosMessage === item.ma &&
|
)}
|
||||||
styles.selectedItemText,
|
</View>
|
||||||
]}
|
)}
|
||||||
>
|
</Modal>
|
||||||
{item.moTa}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const SosButtonStyles = (textColor: string, borderColor: string, errorColor: string, backgroundColor: string) => StyleSheet.create({
|
||||||
formGroup: {
|
formGroup: {
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
@@ -288,93 +220,27 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
color: "#333",
|
color: textColor,
|
||||||
},
|
|
||||||
dropdownButton: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ddd",
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 12,
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
},
|
},
|
||||||
errorBorder: {
|
errorBorder: {
|
||||||
borderColor: "#ff4444",
|
borderColor: errorColor,
|
||||||
},
|
|
||||||
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: {
|
input: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#ddd",
|
borderColor: borderColor,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: "#333",
|
color: textColor,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
textAlignVertical: "top",
|
textAlignVertical: "top",
|
||||||
},
|
},
|
||||||
errorInput: {
|
errorInput: {
|
||||||
borderColor: "#ff4444",
|
borderColor: errorColor,
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
color: "#ff4444",
|
color: errorColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
interpolate,
|
interpolate,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useScrollOffset,
|
useScrollOffset,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
import { ThemedView } from '@/components/themed-view';
|
import { ThemedView } from '@/components/themed-view';
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
|
||||||
import { useThemeColor } from '@/hooks/use-theme-color';
|
import { useThemeColor } from '@/hooks/use-theme-color';
|
||||||
|
import { useColorScheme } from '@/hooks/use-theme-context';
|
||||||
|
|
||||||
const HEADER_HEIGHT = 250;
|
const HEADER_HEIGHT = 250;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export default function ParallaxScrollView({
|
|||||||
headerBackgroundColor,
|
headerBackgroundColor,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const backgroundColor = useThemeColor({}, 'background');
|
const backgroundColor = useThemeColor({}, 'background');
|
||||||
const colorScheme = useColorScheme() ?? 'light';
|
const colorScheme = useColorScheme();
|
||||||
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||||
const scrollOffset = useScrollOffset(scrollRef);
|
const scrollOffset = useScrollOffset(scrollRef);
|
||||||
const headerAnimatedStyle = useAnimatedStyle(() => {
|
const headerAnimatedStyle = useAnimatedStyle(() => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { IconSymbol } from "@/components/ui/icon-symbol";
|
|||||||
import { queryGpsData } from "@/controller/DeviceController";
|
import { queryGpsData } from "@/controller/DeviceController";
|
||||||
import { queryUpdateFishingLogs } from "@/controller/TripController";
|
import { queryUpdateFishingLogs } from "@/controller/TripController";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import { showErrorToast, showSuccessToast } from "@/services/toast_service";
|
import { showErrorToast, showSuccessToast } from "@/services/toast_service";
|
||||||
import { useFishes } from "@/state/use-fish";
|
import { useFishes } from "@/state/use-fish";
|
||||||
import { useTrip } from "@/state/use-trip";
|
import { useTrip } from "@/state/use-trip";
|
||||||
@@ -20,8 +21,8 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { InfoSection } from "./NetDetailModal/components";
|
import { InfoSection } from "./components/InfoSection";
|
||||||
import styles from "./style/CreateOrUpdateHaulModal.styles";
|
import { createStyles } from "./style/CreateOrUpdateHaulModal.styles";
|
||||||
|
|
||||||
interface CreateOrUpdateHaulModalProps {
|
interface CreateOrUpdateHaulModalProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
@@ -74,6 +75,8 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
|||||||
fishingLog,
|
fishingLog,
|
||||||
fishingLogIndex,
|
fishingLogIndex,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { colors } = useThemeContext();
|
||||||
|
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [isCreateMode, setIsCreateMode] = React.useState(!fishingLog?.info);
|
const [isCreateMode, setIsCreateMode] = React.useState(!fishingLog?.info);
|
||||||
const [isEditing, setIsEditing] = React.useState(false);
|
const [isEditing, setIsEditing] = React.useState(false);
|
||||||
@@ -256,7 +259,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => remove(index)}
|
onPress={() => remove(index)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#FF3B30",
|
backgroundColor: colors.error,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -277,7 +280,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleToggleExpanded(index)}
|
onPress={() => handleToggleExpanded(index)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#007AFF",
|
backgroundColor: colors.primary,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Modal, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
import { Modal, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||||
import styles from "./style/CrewDetailModal.styles";
|
import { createStyles } from "./style/CrewDetailModal.styles";
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// 🧩 Interface
|
// 🧩 Interface
|
||||||
@@ -23,6 +24,8 @@ const CrewDetailModal: React.FC<CrewDetailModalProps> = ({
|
|||||||
crewData,
|
crewData,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { colors } = useThemeContext();
|
||||||
|
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||||
|
|
||||||
if (!crewData) return null;
|
if (!crewData) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,362 +0,0 @@
|
|||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Modal,
|
|
||||||
ScrollView,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import styles from "../style/NetDetailModal.styles";
|
|
||||||
import { CatchSectionHeader } from "./components/CatchSectionHeader";
|
|
||||||
import { FishCardList } from "./components/FishCardList";
|
|
||||||
import { NotesSection } from "./components/NotesSection";
|
|
||||||
|
|
||||||
interface NetDetailModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
netData: Model.FishingLog | null;
|
|
||||||
stt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// 🧵 Component Modal
|
|
||||||
// ---------------------------
|
|
||||||
const NetDetailModal: React.FC<NetDetailModalProps> = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
netData,
|
|
||||||
stt,
|
|
||||||
}) => {
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [editableCatchList, setEditableCatchList] = useState<
|
|
||||||
Model.FishingLogInfo[]
|
|
||||||
>([]);
|
|
||||||
const [selectedFishIndex, setSelectedFishIndex] = useState<number | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [selectedUnitIndex, setSelectedUnitIndex] = useState<number | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
// const [selectedConditionIndex, setSelectedConditionIndex] = useState<
|
|
||||||
// number | null
|
|
||||||
// >(null);
|
|
||||||
// const [selectedGearIndex, setSelectedGearIndex] = useState<number | null>(
|
|
||||||
// null
|
|
||||||
// );
|
|
||||||
const [expandedFishIndices, setExpandedFishIndices] = useState<number[]>([]);
|
|
||||||
|
|
||||||
// Khởi tạo dữ liệu khi netData thay đổi
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (netData?.info) {
|
|
||||||
setEditableCatchList(netData.info);
|
|
||||||
}
|
|
||||||
}, [netData]);
|
|
||||||
|
|
||||||
// Reset state khi modal đóng
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!visible) {
|
|
||||||
setExpandedFishIndices([]);
|
|
||||||
setSelectedFishIndex(null);
|
|
||||||
setSelectedUnitIndex(null);
|
|
||||||
// setSelectedConditionIndex(null);
|
|
||||||
// setSelectedGearIndex(null);
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
// if (!netData) return null;
|
|
||||||
|
|
||||||
const isCompleted = netData?.status === 2; // ví dụ: status=2 là hoàn thành
|
|
||||||
|
|
||||||
// Danh sách tên cá có sẵn
|
|
||||||
const fishNameOptions = [
|
|
||||||
"Cá chim trắng",
|
|
||||||
"Cá song đỏ",
|
|
||||||
"Cá hồng",
|
|
||||||
"Cá nục",
|
|
||||||
"Cá ngừ đại dương",
|
|
||||||
"Cá mú trắng",
|
|
||||||
"Cá hồng phớn",
|
|
||||||
"Cá hổ Napoleon",
|
|
||||||
"Cá nược",
|
|
||||||
"Cá đuối quạt",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Danh sách đơn vị
|
|
||||||
const unitOptions = ["kg", "con", "tấn"];
|
|
||||||
|
|
||||||
// Danh sách tình trạng
|
|
||||||
// const conditionOptions = ["Còn sống", "Chết", "Bị thương"];
|
|
||||||
|
|
||||||
// Danh sách ngư cụ
|
|
||||||
// const gearOptions = [
|
|
||||||
// "Lưới kéo",
|
|
||||||
// "Lưới vây",
|
|
||||||
// "Lưới rê",
|
|
||||||
// "Lưới cào",
|
|
||||||
// "Lưới lồng",
|
|
||||||
// "Câu cần",
|
|
||||||
// "Câu dây",
|
|
||||||
// "Chài cá",
|
|
||||||
// "Lồng bẫy",
|
|
||||||
// "Đăng",
|
|
||||||
// ];
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
setIsEditing(!isEditing);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
// Validate từng cá trong danh sách và thu thập tất cả lỗi
|
|
||||||
const allErrors: { index: number; errors: string[] }[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < editableCatchList.length; i++) {
|
|
||||||
const fish = editableCatchList[i];
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
if (!fish.fish_name || fish.fish_name.trim() === "") {
|
|
||||||
errors.push("- Tên loài cá");
|
|
||||||
}
|
|
||||||
if (!fish.catch_number || fish.catch_number <= 0) {
|
|
||||||
errors.push("- Số lượng bắt được");
|
|
||||||
}
|
|
||||||
if (!fish.catch_unit || fish.catch_unit.trim() === "") {
|
|
||||||
errors.push("- Đơn vị");
|
|
||||||
}
|
|
||||||
if (!fish.fish_size || fish.fish_size <= 0) {
|
|
||||||
errors.push("- Kích thước cá");
|
|
||||||
}
|
|
||||||
// if (!fish.fish_condition || fish.fish_condition.trim() === "") {
|
|
||||||
// errors.push("- Tình trạng cá");
|
|
||||||
// }
|
|
||||||
// if (!fish.gear_usage || fish.gear_usage.trim() === "") {
|
|
||||||
// errors.push("- Dụng cụ sử dụng");
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
allErrors.push({ index: i, errors });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nếu có lỗi, hiển thị tất cả
|
|
||||||
if (allErrors.length > 0) {
|
|
||||||
const errorMessage = allErrors
|
|
||||||
.map((item) => {
|
|
||||||
return `Cá số ${item.index + 1}:\n${item.errors.join("\n")}`;
|
|
||||||
})
|
|
||||||
.join("\n\n");
|
|
||||||
|
|
||||||
Alert.alert(
|
|
||||||
"Thông tin không đầy đủ",
|
|
||||||
errorMessage,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: "Tiếp tục chỉnh sửa",
|
|
||||||
onPress: () => {
|
|
||||||
// Mở rộng tất cả các card bị lỗi
|
|
||||||
setExpandedFishIndices((prev) => {
|
|
||||||
const errorIndices = allErrors.map((item) => item.index);
|
|
||||||
const newIndices = [...prev];
|
|
||||||
errorIndices.forEach((idx) => {
|
|
||||||
if (!newIndices.includes(idx)) {
|
|
||||||
newIndices.push(idx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return newIndices;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Hủy",
|
|
||||||
onPress: () => {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ cancelable: false }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nếu validation pass, lưu dữ liệu
|
|
||||||
setIsEditing(false);
|
|
||||||
console.log("Saved catch list:", editableCatchList);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsEditing(false);
|
|
||||||
setEditableCatchList(netData?.info || []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleExpanded = (index: number) => {
|
|
||||||
setExpandedFishIndices((prev) =>
|
|
||||||
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCatchItem = (
|
|
||||||
index: number,
|
|
||||||
field: keyof Model.FishingLogInfo,
|
|
||||||
value: string | number
|
|
||||||
) => {
|
|
||||||
setEditableCatchList((prev) =>
|
|
||||||
prev.map((item, i) => {
|
|
||||||
if (i === index) {
|
|
||||||
const updatedItem = { ...item };
|
|
||||||
if (
|
|
||||||
field === "catch_number" ||
|
|
||||||
field === "fish_size" ||
|
|
||||||
field === "fish_rarity"
|
|
||||||
) {
|
|
||||||
updatedItem[field] = Number(value) || 0;
|
|
||||||
} else {
|
|
||||||
updatedItem[field] = value as never;
|
|
||||||
}
|
|
||||||
return updatedItem;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddNewFish = () => {
|
|
||||||
const newFish: Model.FishingLogInfo = {
|
|
||||||
fish_species_id: 0,
|
|
||||||
fish_name: "",
|
|
||||||
catch_number: 0,
|
|
||||||
catch_unit: "kg",
|
|
||||||
fish_size: 0,
|
|
||||||
fish_rarity: 0,
|
|
||||||
fish_condition: "",
|
|
||||||
gear_usage: "",
|
|
||||||
};
|
|
||||||
setEditableCatchList((prev) => [...prev, newFish]);
|
|
||||||
// Tự động expand card mới
|
|
||||||
setExpandedFishIndices((prev) => [...prev, editableCatchList.length]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteFish = (index: number) => {
|
|
||||||
Alert.alert(
|
|
||||||
"Xác nhận xóa",
|
|
||||||
`Bạn có chắc muốn xóa loài cá này?`,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: "Hủy",
|
|
||||||
style: "cancel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Xóa",
|
|
||||||
style: "destructive",
|
|
||||||
onPress: () => {
|
|
||||||
setEditableCatchList((prev) => prev.filter((_, i) => i !== index));
|
|
||||||
// Cập nhật lại expandedFishIndices sau khi xóa
|
|
||||||
setExpandedFishIndices((prev) =>
|
|
||||||
prev
|
|
||||||
.filter((i) => i !== index)
|
|
||||||
.map((i) => (i > index ? i - 1 : i))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ cancelable: true }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chỉ tính tổng số lượng cá có đơn vị là 'kg'
|
|
||||||
const totalCatch = editableCatchList.reduce(
|
|
||||||
(sum, item) =>
|
|
||||||
item.catch_unit === "kg" ? sum + (item.catch_number ?? 0) : sum,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
animationType="slide"
|
|
||||||
presentationStyle="pageSheet"
|
|
||||||
onRequestClose={onClose}
|
|
||||||
>
|
|
||||||
<View style={styles.container}>
|
|
||||||
{/* Header */}
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={styles.title}>Chi tiết mẻ lưới</Text>
|
|
||||||
<View style={styles.headerButtons}>
|
|
||||||
{isEditing ? (
|
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleCancel}
|
|
||||||
style={styles.cancelButton}
|
|
||||||
>
|
|
||||||
<Text style={styles.cancelButtonText}>Hủy</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleSave}
|
|
||||||
style={styles.saveButton}
|
|
||||||
>
|
|
||||||
<Text style={styles.saveButtonText}>Lưu</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity onPress={handleEdit} style={styles.editButton}>
|
|
||||||
<View style={styles.editIconButton}>
|
|
||||||
<IconSymbol
|
|
||||||
name="pencil"
|
|
||||||
size={28}
|
|
||||||
color="#fff"
|
|
||||||
weight="heavy"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
|
||||||
<View style={styles.closeIconButton}>
|
|
||||||
<IconSymbol name="xmark" size={28} color="#fff" />
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<ScrollView style={styles.content}>
|
|
||||||
{/* Thông tin chung */}
|
|
||||||
{/* <InfoSection
|
|
||||||
netData={netData ?? undefined}
|
|
||||||
isCompleted={isCompleted}
|
|
||||||
stt={stt}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{/* Danh sách cá bắt được */}
|
|
||||||
<CatchSectionHeader totalCatch={totalCatch} />
|
|
||||||
|
|
||||||
{/* Fish cards */}
|
|
||||||
<FishCardList
|
|
||||||
catchList={editableCatchList}
|
|
||||||
isEditing={isEditing}
|
|
||||||
expandedFishIndex={expandedFishIndices}
|
|
||||||
selectedFishIndex={selectedFishIndex}
|
|
||||||
selectedUnitIndex={selectedUnitIndex}
|
|
||||||
// selectedConditionIndex={selectedConditionIndex}
|
|
||||||
// selectedGearIndex={selectedGearIndex}
|
|
||||||
fishNameOptions={fishNameOptions}
|
|
||||||
unitOptions={unitOptions}
|
|
||||||
// conditionOptions={conditionOptions}
|
|
||||||
// gearOptions={gearOptions}
|
|
||||||
onToggleExpanded={handleToggleExpanded}
|
|
||||||
onUpdateCatchItem={updateCatchItem}
|
|
||||||
setSelectedFishIndex={setSelectedFishIndex}
|
|
||||||
setSelectedUnitIndex={setSelectedUnitIndex}
|
|
||||||
// setSelectedConditionIndex={setSelectedConditionIndex}
|
|
||||||
// setSelectedGearIndex={setSelectedGearIndex}
|
|
||||||
onAddNewFish={handleAddNewFish}
|
|
||||||
onDeleteFish={handleDeleteFish}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Ghi chú */}
|
|
||||||
<NotesSection ghiChu={netData?.weather_description} />
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetDetailModal;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Text, View } from "react-native";
|
|
||||||
import styles from "../../style/NetDetailModal.styles";
|
|
||||||
|
|
||||||
interface CatchSectionHeaderProps {
|
|
||||||
totalCatch: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CatchSectionHeader: React.FC<CatchSectionHeaderProps> = ({
|
|
||||||
totalCatch,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<View style={styles.sectionHeader}>
|
|
||||||
<Text style={styles.sectionTitle}>Danh sách cá bắt được</Text>
|
|
||||||
<Text style={styles.totalCatchText}>
|
|
||||||
Tổng: {totalCatch.toLocaleString()} kg
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import { useFishes } from "@/state/use-fish";
|
|
||||||
import React from "react";
|
|
||||||
import { Text, TextInput, View } from "react-native";
|
|
||||||
import styles from "../../style/NetDetailModal.styles";
|
|
||||||
import { FishSelectDropdown } from "./FishSelectDropdown";
|
|
||||||
|
|
||||||
interface FishCardFormProps {
|
|
||||||
fish: Model.FishingLogInfo;
|
|
||||||
index: number;
|
|
||||||
isEditing: boolean;
|
|
||||||
fishNameOptions: string[]; // Bỏ gọi API cá
|
|
||||||
unitOptions: string[]; // Bỏ render ở trong này
|
|
||||||
// conditionOptions: string[];
|
|
||||||
// gearOptions: string[];
|
|
||||||
selectedFishIndex: number | null;
|
|
||||||
selectedUnitIndex: number | null;
|
|
||||||
// selectedConditionIndex: number | null;
|
|
||||||
// selectedGearIndex: number | null;
|
|
||||||
setSelectedFishIndex: (index: number | null) => void;
|
|
||||||
setSelectedUnitIndex: (index: number | null) => void;
|
|
||||||
// setSelectedConditionIndex: (index: number | null) => void;
|
|
||||||
// setSelectedGearIndex: (index: number | null) => void;
|
|
||||||
onUpdateCatchItem: (
|
|
||||||
index: number,
|
|
||||||
field: keyof Model.FishingLogInfo,
|
|
||||||
value: string | number
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FishCardForm: React.FC<FishCardFormProps> = ({
|
|
||||||
fish,
|
|
||||||
index,
|
|
||||||
isEditing,
|
|
||||||
unitOptions,
|
|
||||||
// conditionOptions,
|
|
||||||
// gearOptions,
|
|
||||||
selectedFishIndex,
|
|
||||||
selectedUnitIndex,
|
|
||||||
// selectedConditionIndex,
|
|
||||||
// selectedGearIndex,
|
|
||||||
setSelectedFishIndex,
|
|
||||||
setSelectedUnitIndex,
|
|
||||||
// setSelectedConditionIndex,
|
|
||||||
// setSelectedGearIndex,
|
|
||||||
onUpdateCatchItem,
|
|
||||||
}) => {
|
|
||||||
const { fishSpecies } = useFishes();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Tên cá - Select */}
|
|
||||||
<View
|
|
||||||
style={[styles.fieldGroup, { zIndex: 1000 - index }, { marginTop: 15 }]}
|
|
||||||
>
|
|
||||||
<Text style={styles.label}>Tên cá</Text>
|
|
||||||
{isEditing ? (
|
|
||||||
<FishSelectDropdown
|
|
||||||
options={fishSpecies || []}
|
|
||||||
selectedFishId={selectedFishIndex}
|
|
||||||
isOpen={selectedFishIndex === index}
|
|
||||||
onToggle={() =>
|
|
||||||
setSelectedFishIndex(selectedFishIndex === index ? null : index)
|
|
||||||
}
|
|
||||||
onSelect={(value: Model.FishSpeciesResponse) => {
|
|
||||||
onUpdateCatchItem(index, "fish_name", value.name);
|
|
||||||
setSelectedFishIndex(value.id);
|
|
||||||
console.log("Fish Selected: ", fish);
|
|
||||||
}}
|
|
||||||
zIndex={1000 - index}
|
|
||||||
styleOverride={styles.fishNameDropdown}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.infoValue}>{fish.fish_name}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Số lượng & Đơn vị */}
|
|
||||||
<View style={styles.rowGroup}>
|
|
||||||
<View style={[styles.fieldGroup, { flex: 1, marginRight: 8 }]}>
|
|
||||||
<Text style={styles.label}>Số lượng</Text>
|
|
||||||
{isEditing ? (
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={String(fish.catch_number)}
|
|
||||||
onChangeText={(value) =>
|
|
||||||
onUpdateCatchItem(index, "catch_number", value)
|
|
||||||
}
|
|
||||||
keyboardType="numeric"
|
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.infoValue}>{fish.catch_number}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.fieldGroup,
|
|
||||||
{ flex: 1, marginLeft: 8, zIndex: 900 - index },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={styles.label}>Đơn vị</Text>
|
|
||||||
{/* {isEditing ? (
|
|
||||||
<FishSelectDropdown
|
|
||||||
options={unitOptions}
|
|
||||||
selectedValue={fish.catch_unit ?? ""}
|
|
||||||
isOpen={selectedUnitIndex === index}
|
|
||||||
onToggle={() =>
|
|
||||||
setSelectedUnitIndex(selectedUnitIndex === index ? null : index)
|
|
||||||
}
|
|
||||||
onSelect={(value: string) => {
|
|
||||||
onUpdateCatchItem(index, "catch_unit", value);
|
|
||||||
setSelectedUnitIndex(null);
|
|
||||||
}}
|
|
||||||
zIndex={900 - index}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.infoValue}>{fish.catch_unit}</Text>
|
|
||||||
)} */}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Kích thước & Độ hiếm */}
|
|
||||||
<View style={styles.rowGroup}>
|
|
||||||
<View style={[styles.fieldGroup, { flex: 1, marginRight: 8 }]}>
|
|
||||||
<Text style={styles.label}>Kích thước (cm)</Text>
|
|
||||||
{isEditing ? (
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={String(fish.fish_size)}
|
|
||||||
onChangeText={(value) =>
|
|
||||||
onUpdateCatchItem(index, "fish_size", value)
|
|
||||||
}
|
|
||||||
keyboardType="numeric"
|
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.infoValue}>{fish.fish_size} cm</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={[styles.fieldGroup, { flex: 1, marginLeft: 8 }]}>
|
|
||||||
<Text style={styles.label}>Độ hiếm</Text>
|
|
||||||
{isEditing ? (
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={String(fish.fish_rarity)}
|
|
||||||
onChangeText={(value) =>
|
|
||||||
onUpdateCatchItem(index, "fish_rarity", value)
|
|
||||||
}
|
|
||||||
keyboardType="numeric"
|
|
||||||
placeholder="1-5"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.infoValue}>{fish.fish_rarity}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Tình trạng */}
|
|
||||||
{/* <View style={[styles.fieldGroup, { zIndex: 800 - index }]}>
|
|
||||||
<Text style={styles.label}>Tình trạng</Text>
|
|
||||||
{isEditing ? (
|
|
||||||
<FishSelectDropdown
|
|
||||||
options={conditionOptions}
|
|
||||||
selectedValue={fish.fish_condition}
|
|
||||||
isOpen={selectedConditionIndex === index}
|
|
||||||
onToggle={() =>
|
|
||||||
setSelectedConditionIndex(
|
|
||||||
selectedConditionIndex === index ? null : index
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onSelect={(value: string) => {
|
|
||||||
onUpdateCatchItem(index, "fish_condition", value);
|
|
||||||
setSelectedConditionIndex(null);
|
|
||||||
}}
|
|
||||||
zIndex={800 - index}
|
|
||||||
styleOverride={styles.optionsStatusFishList}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.infoValue}>{fish.fish_condition}</Text>
|
|
||||||
)}
|
|
||||||
</View> */}
|
|
||||||
|
|
||||||
{/* Ngư cụ sử dụng */}
|
|
||||||
{/* <View style={[styles.fieldGroup, { zIndex: 700 - index }]}>
|
|
||||||
<Text style={styles.label}>Ngư cụ sử dụng</Text>
|
|
||||||
{isEditing ? (
|
|
||||||
<FishSelectDropdown
|
|
||||||
options={gearOptions}
|
|
||||||
selectedValue={fish.gear_usage}
|
|
||||||
isOpen={selectedGearIndex === index}
|
|
||||||
onToggle={() =>
|
|
||||||
setSelectedGearIndex(selectedGearIndex === index ? null : index)
|
|
||||||
}
|
|
||||||
onSelect={(value: string) => {
|
|
||||||
onUpdateCatchItem(index, "gear_usage", value);
|
|
||||||
setSelectedGearIndex(null);
|
|
||||||
}}
|
|
||||||
zIndex={700 - index}
|
|
||||||
styleOverride={styles.optionsStatusFishList}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.infoValue}>{fish.gear_usage || "Không có"}</Text>
|
|
||||||
)}
|
|
||||||
</View> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Text, View } from "react-native";
|
|
||||||
import styles from "../../style/NetDetailModal.styles";
|
|
||||||
|
|
||||||
interface FishCardHeaderProps {
|
|
||||||
fish: Model.FishingLogInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FishCardHeader: React.FC<FishCardHeaderProps> = ({ fish }) => {
|
|
||||||
return (
|
|
||||||
<View style={styles.fishCardHeaderContent}>
|
|
||||||
<Text style={styles.fishCardTitle}>{fish.fish_name}:</Text>
|
|
||||||
<Text style={styles.fishCardSubtitle}>
|
|
||||||
{fish.catch_number} {fish.catch_unit}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
|
||||||
import React from "react";
|
|
||||||
import { Text, TouchableOpacity, View } from "react-native";
|
|
||||||
import styles from "../../style/NetDetailModal.styles";
|
|
||||||
import { FishCardForm } from "./FishCardForm";
|
|
||||||
import { FishCardHeader } from "./FishCardHeader";
|
|
||||||
|
|
||||||
interface FishCardListProps {
|
|
||||||
catchList: Model.FishingLogInfo[];
|
|
||||||
isEditing: boolean;
|
|
||||||
expandedFishIndex: number[];
|
|
||||||
selectedFishIndex: number | null;
|
|
||||||
selectedUnitIndex: number | null;
|
|
||||||
// selectedConditionIndex: number | null;
|
|
||||||
// selectedGearIndex: number | null;
|
|
||||||
fishNameOptions: string[];
|
|
||||||
unitOptions: string[];
|
|
||||||
// conditionOptions: string[];
|
|
||||||
// gearOptions: string[];
|
|
||||||
onToggleExpanded: (index: number) => void;
|
|
||||||
onUpdateCatchItem: (
|
|
||||||
index: number,
|
|
||||||
field: keyof Model.FishingLogInfo,
|
|
||||||
value: string | number
|
|
||||||
) => void;
|
|
||||||
setSelectedFishIndex: (index: number | null) => void;
|
|
||||||
setSelectedUnitIndex: (index: number | null) => void;
|
|
||||||
// setSelectedConditionIndex: (index: number | null) => void;
|
|
||||||
// setSelectedGearIndex: (index: number | null) => void;
|
|
||||||
onAddNewFish?: () => void;
|
|
||||||
onDeleteFish?: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FishCardList: React.FC<FishCardListProps> = ({
|
|
||||||
catchList,
|
|
||||||
isEditing,
|
|
||||||
expandedFishIndex,
|
|
||||||
selectedFishIndex,
|
|
||||||
selectedUnitIndex,
|
|
||||||
// selectedConditionIndex,
|
|
||||||
// selectedGearIndex,
|
|
||||||
fishNameOptions,
|
|
||||||
unitOptions,
|
|
||||||
// conditionOptions,
|
|
||||||
// gearOptions,
|
|
||||||
onToggleExpanded,
|
|
||||||
onUpdateCatchItem,
|
|
||||||
setSelectedFishIndex,
|
|
||||||
setSelectedUnitIndex,
|
|
||||||
// setSelectedConditionIndex,
|
|
||||||
// setSelectedGearIndex,
|
|
||||||
onAddNewFish,
|
|
||||||
onDeleteFish,
|
|
||||||
}) => {
|
|
||||||
// Chuyển về logic đơn giản, không animation
|
|
||||||
const handleToggleCard = (index: number) => {
|
|
||||||
onToggleExpanded(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{catchList.map((fish, index) => (
|
|
||||||
<View key={index} style={styles.fishCard}>
|
|
||||||
{/* Delete + Chevron buttons - always on top, right side, horizontal row */}
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 9999,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: 8,
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
pointerEvents="box-none"
|
|
||||||
>
|
|
||||||
{isEditing && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => onDeleteFish?.(index)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#FF3B30",
|
|
||||||
borderRadius: 8,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOpacity: 0.08,
|
|
||||||
shadowRadius: 2,
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
|
||||||
elevation: 2,
|
|
||||||
}}
|
|
||||||
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<IconSymbol name="trash" size={24} color="#fff" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => handleToggleCard(index)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#007AFF",
|
|
||||||
borderRadius: 8,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOpacity: 0.08,
|
|
||||||
shadowRadius: 2,
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
|
||||||
elevation: 2,
|
|
||||||
}}
|
|
||||||
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<IconSymbol
|
|
||||||
name={
|
|
||||||
expandedFishIndex.includes(index)
|
|
||||||
? "chevron.up"
|
|
||||||
: "chevron.down"
|
|
||||||
}
|
|
||||||
size={24}
|
|
||||||
color="#fff"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Header - Only visible when collapsed */}
|
|
||||||
{!expandedFishIndex.includes(index) && <FishCardHeader fish={fish} />}
|
|
||||||
|
|
||||||
{/* Form - Only show when expanded */}
|
|
||||||
{expandedFishIndex.includes(index) && (
|
|
||||||
<FishCardForm
|
|
||||||
fish={fish}
|
|
||||||
index={index}
|
|
||||||
isEditing={isEditing}
|
|
||||||
fishNameOptions={fishNameOptions}
|
|
||||||
unitOptions={unitOptions}
|
|
||||||
// conditionOptions={conditionOptions}
|
|
||||||
// gearOptions={gearOptions}
|
|
||||||
selectedFishIndex={selectedFishIndex}
|
|
||||||
selectedUnitIndex={selectedUnitIndex}
|
|
||||||
// selectedConditionIndex={selectedConditionIndex}
|
|
||||||
// selectedGearIndex={selectedGearIndex}
|
|
||||||
setSelectedFishIndex={setSelectedFishIndex}
|
|
||||||
setSelectedUnitIndex={setSelectedUnitIndex}
|
|
||||||
// setSelectedConditionIndex={setSelectedConditionIndex}
|
|
||||||
// setSelectedGearIndex={setSelectedGearIndex}
|
|
||||||
onUpdateCatchItem={onUpdateCatchItem}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Nút thêm loài cá mới - hiển thị khi đang chỉnh sửa */}
|
|
||||||
{isEditing && (
|
|
||||||
<TouchableOpacity onPress={onAddNewFish} style={styles.addFishButton}>
|
|
||||||
<View style={styles.addFishButtonContent}>
|
|
||||||
<IconSymbol name="plus" size={24} color="#fff" />
|
|
||||||
<Text style={styles.addFishButtonText}>Thêm loài cá</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
|
||||||
import React from "react";
|
|
||||||
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
|
||||||
import styles from "../../style/NetDetailModal.styles";
|
|
||||||
|
|
||||||
interface FishSelectDropdownProps {
|
|
||||||
options: Model.FishSpeciesResponse[];
|
|
||||||
selectedFishId: number | null;
|
|
||||||
isOpen: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
onSelect: (value: Model.FishSpeciesResponse) => void;
|
|
||||||
zIndex: number;
|
|
||||||
styleOverride?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FishSelectDropdown: React.FC<FishSelectDropdownProps> = ({
|
|
||||||
options,
|
|
||||||
selectedFishId,
|
|
||||||
isOpen,
|
|
||||||
onToggle,
|
|
||||||
onSelect,
|
|
||||||
zIndex,
|
|
||||||
styleOverride,
|
|
||||||
}) => {
|
|
||||||
const dropdownStyle = styleOverride || styles.optionsList;
|
|
||||||
const findFishNameById = (id: number | null) => {
|
|
||||||
const fish = options.find((item) => item.id === id);
|
|
||||||
return fish?.name || "Chọn cá";
|
|
||||||
};
|
|
||||||
const [selectedFish, setSelectedFish] =
|
|
||||||
React.useState<Model.FishSpeciesResponse | null>(null);
|
|
||||||
return (
|
|
||||||
<View style={{ zIndex }}>
|
|
||||||
<TouchableOpacity style={styles.selectButton} onPress={onToggle}>
|
|
||||||
<Text style={styles.selectButtonText}>
|
|
||||||
{findFishNameById(selectedFishId)}
|
|
||||||
</Text>
|
|
||||||
<IconSymbol
|
|
||||||
name={isOpen ? "chevron.up" : "chevron.down"}
|
|
||||||
size={16}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
{isOpen && (
|
|
||||||
<ScrollView style={dropdownStyle} nestedScrollEnabled={true}>
|
|
||||||
{options.map((option, optIndex) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={option.id || optIndex}
|
|
||||||
style={styles.optionItem}
|
|
||||||
onPress={() => onSelect(option)}
|
|
||||||
>
|
|
||||||
<Text style={styles.optionText}>
|
|
||||||
{findFishNameById(option.id)}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Text, View } from "react-native";
|
|
||||||
import styles from "../../style/NetDetailModal.styles";
|
|
||||||
|
|
||||||
interface NotesSectionProps {
|
|
||||||
ghiChu?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotesSection: React.FC<NotesSectionProps> = ({ ghiChu }) => {
|
|
||||||
if (!ghiChu) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.infoCard}>
|
|
||||||
<View style={styles.infoRow}>
|
|
||||||
<Text style={styles.infoLabel}>Ghi chú</Text>
|
|
||||||
<Text style={styles.infoValue}>{ghiChu}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export { CatchSectionHeader } from "./CatchSectionHeader";
|
|
||||||
export { FishCardForm } from "./FishCardForm";
|
|
||||||
export { FishCardHeader } from "./FishCardHeader";
|
|
||||||
export { FishCardList } from "./FishCardList";
|
|
||||||
export { FishSelectDropdown } from "./FishSelectDropdown";
|
|
||||||
export { InfoSection } from "./InfoSection";
|
|
||||||
export { NotesSection } from "./NotesSection";
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import { StyleSheet } from "react-native";
|
|
||||||
|
|
||||||
export default StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingTop: 16,
|
|
||||||
paddingBottom: 8,
|
|
||||||
backgroundColor: "#f8f9fa",
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: "#e9ecef",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: "#333",
|
|
||||||
},
|
|
||||||
closeButton: {
|
|
||||||
padding: 8,
|
|
||||||
},
|
|
||||||
closeButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: "#007bff",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
padding: 16,
|
|
||||||
},
|
|
||||||
fieldGroup: {
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "#333",
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ccc",
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: 8,
|
|
||||||
fontSize: 16,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
},
|
|
||||||
infoValue: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: "#555",
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
rowGroup: {
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
},
|
|
||||||
fishNameDropdown: {
|
|
||||||
// Custom styles if needed
|
|
||||||
},
|
|
||||||
optionsStatusFishList: {
|
|
||||||
// Custom styles if needed
|
|
||||||
},
|
|
||||||
optionsList: {
|
|
||||||
maxHeight: 150,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ccc",
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
position: "absolute",
|
|
||||||
top: 40,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 1000,
|
|
||||||
},
|
|
||||||
selectButton: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ccc",
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: 8,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
selectButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: "#333",
|
|
||||||
},
|
|
||||||
optionItem: {
|
|
||||||
padding: 10,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
|
||||||
optionText: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: "#333",
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ddd",
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 12,
|
|
||||||
marginBottom: 12,
|
|
||||||
backgroundColor: "#f9f9f9",
|
|
||||||
},
|
|
||||||
removeButton: {
|
|
||||||
backgroundColor: "#dc3545",
|
|
||||||
padding: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
alignSelf: "flex-end",
|
|
||||||
marginTop: 8,
|
|
||||||
},
|
|
||||||
removeButtonText: {
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
errorText: {
|
|
||||||
color: "#dc3545",
|
|
||||||
fontSize: 12,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
buttonGroup: {
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-around",
|
|
||||||
marginTop: 16,
|
|
||||||
},
|
|
||||||
editButton: {
|
|
||||||
backgroundColor: "#007bff",
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
editButtonText: {
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
addButton: {
|
|
||||||
backgroundColor: "#28a745",
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
addButtonText: {
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
backgroundColor: "#007bff",
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
saveButtonText: {
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
cancelButton: {
|
|
||||||
backgroundColor: "#6c757d",
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
cancelButtonText: {
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
addFishButton: {
|
|
||||||
backgroundColor: "#17a2b8",
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 4,
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
addFishButtonText: {
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import styles from "./style/TripCostDetailModal.styles";
|
import { createStyles } from "./style/TripCostDetailModal.styles";
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// 🧩 Interface
|
// 🧩 Interface
|
||||||
@@ -31,6 +32,8 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
|
|||||||
data,
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { colors } = useThemeContext();
|
||||||
|
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editableData, setEditableData] = useState<Model.TripCost[]>(data);
|
const [editableData, setEditableData] = useState<Model.TripCost[]>(data);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
import styles from "../../style/NetDetailModal.styles";
|
|
||||||
|
|
||||||
interface InfoSectionProps {
|
interface InfoSectionProps {
|
||||||
fishingLog?: Model.FishingLog;
|
fishingLog?: Model.FishingLog;
|
||||||
@@ -13,6 +13,9 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
|||||||
stt,
|
stt,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { colors } = useThemeContext();
|
||||||
|
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||||
|
|
||||||
if (!fishingLog) {
|
if (!fishingLog) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -42,22 +45,6 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
|||||||
? new Date(fishingLog.end_at).toLocaleString()
|
? new Date(fishingLog.end_at).toLocaleString()
|
||||||
: "-",
|
: "-",
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// label: "Vị trí hạ thu",
|
|
||||||
// value: fishingLog.viTriHaThu || "Chưa cập nhật",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: "Vị trí thu lưới",
|
|
||||||
// value: fishingLog.viTriThuLuoi || "Chưa cập nhật",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: "Độ sâu hạ thu",
|
|
||||||
// value: fishingLog.doSauHaThu || "Chưa cập nhật",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: "Độ sâu thu lưới",
|
|
||||||
// value: fishingLog.doSauThuLuoi || "Chưa cập nhật",
|
|
||||||
// },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -69,21 +56,12 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.statusBadge,
|
styles.statusBadge,
|
||||||
item.value === "Đã hoàn thành"
|
item.value === t("trip.infoSection.statusCompleted")
|
||||||
? styles.statusBadgeCompleted
|
? styles.statusBadgeCompleted
|
||||||
: styles.statusBadgeInProgress,
|
: styles.statusBadgeInProgress,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text
|
<Text style={styles.statusBadgeText}>{item.value}</Text>
|
||||||
style={[
|
|
||||||
styles.statusBadgeText,
|
|
||||||
item.value === "Đã hoàn thành"
|
|
||||||
? styles.statusBadgeTextCompleted
|
|
||||||
: styles.statusBadgeTextInProgress,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{item.value}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.infoValue}>{item.value}</Text>
|
<Text style={styles.infoValue}>{item.value}</Text>
|
||||||
@@ -93,3 +71,49 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
infoCard: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
backgroundColor: colors.surfaceSecondary,
|
||||||
|
},
|
||||||
|
infoRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
infoLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
infoValue: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.text,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingVertical: 4,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
statusBadgeCompleted: {
|
||||||
|
backgroundColor: colors.success,
|
||||||
|
},
|
||||||
|
statusBadgeInProgress: {
|
||||||
|
backgroundColor: colors.warning,
|
||||||
|
},
|
||||||
|
statusBadgeText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,179 +1,179 @@
|
|||||||
|
import { Colors } from "@/constants/theme";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
export const createStyles = (colors: typeof Colors.light) =>
|
||||||
container: {
|
StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: "#f5f5f5",
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.backgroundSecondary,
|
||||||
header: {
|
},
|
||||||
flexDirection: "row",
|
header: {
|
||||||
justifyContent: "space-between",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: 20,
|
alignItems: "center",
|
||||||
paddingTop: 30,
|
paddingHorizontal: 20,
|
||||||
paddingBottom: 16,
|
paddingTop: 30,
|
||||||
backgroundColor: "#fff",
|
paddingBottom: 16,
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.surface,
|
||||||
borderBottomColor: "#eee",
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.separator,
|
||||||
title: {
|
},
|
||||||
fontSize: 22,
|
title: {
|
||||||
fontWeight: "700",
|
fontSize: 22,
|
||||||
color: "#000",
|
fontWeight: "700",
|
||||||
flex: 1,
|
color: colors.text,
|
||||||
},
|
flex: 1,
|
||||||
headerButtons: {
|
},
|
||||||
flexDirection: "row",
|
headerButtons: {
|
||||||
gap: 12,
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
gap: 12,
|
||||||
},
|
alignItems: "center",
|
||||||
closeButton: {
|
},
|
||||||
padding: 4,
|
closeButton: {
|
||||||
},
|
padding: 4,
|
||||||
closeIconButton: {
|
},
|
||||||
backgroundColor: "#FF3B30",
|
closeIconButton: {
|
||||||
borderRadius: 10,
|
backgroundColor: colors.error,
|
||||||
padding: 10,
|
borderRadius: 10,
|
||||||
justifyContent: "center",
|
padding: 10,
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
},
|
alignItems: "center",
|
||||||
saveButton: {
|
},
|
||||||
backgroundColor: "#007bff",
|
saveButton: {
|
||||||
borderRadius: 8,
|
backgroundColor: colors.primary,
|
||||||
paddingHorizontal: 20,
|
borderRadius: 8,
|
||||||
paddingVertical: 10,
|
paddingHorizontal: 20,
|
||||||
justifyContent: "center",
|
paddingVertical: 10,
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
},
|
alignItems: "center",
|
||||||
saveButtonText: {
|
},
|
||||||
color: "#fff",
|
saveButtonText: {
|
||||||
fontSize: 16,
|
color: "#fff",
|
||||||
fontWeight: "600",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
content: {
|
},
|
||||||
flex: 1,
|
content: {
|
||||||
padding: 16,
|
flex: 1,
|
||||||
marginBottom: 15,
|
padding: 16,
|
||||||
},
|
marginBottom: 15,
|
||||||
fishCard: {
|
},
|
||||||
backgroundColor: "#fff",
|
fishCard: {
|
||||||
borderRadius: 12,
|
backgroundColor: colors.surfaceSecondary,
|
||||||
padding: 16,
|
borderRadius: 12,
|
||||||
marginBottom: 16,
|
padding: 16,
|
||||||
shadowColor: "#000",
|
marginBottom: 16,
|
||||||
shadowOpacity: 0.05,
|
shadowColor: "#000",
|
||||||
shadowRadius: 4,
|
shadowOpacity: 0.05,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
shadowOffset: { width: 0, height: 2 },
|
||||||
},
|
elevation: 2,
|
||||||
|
},
|
||||||
|
|
||||||
fishCardHeaderContent: {
|
fishCardHeaderContent: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
fishCardTitle: {
|
fishCardTitle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: "#000",
|
color: colors.text,
|
||||||
},
|
},
|
||||||
fishCardSubtitle: {
|
fishCardSubtitle: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: "#ff6600",
|
color: colors.warning,
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
fieldGroup: {
|
fieldGroup: {
|
||||||
marginBottom: 14,
|
marginBottom: 14,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: "#333",
|
color: colors.textSecondary,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#ddd",
|
borderColor: colors.primary,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
backgroundColor: "#fff",
|
backgroundColor: colors.surface,
|
||||||
color: "#000",
|
color: colors.text,
|
||||||
},
|
},
|
||||||
inputDisabled: {
|
inputDisabled: {
|
||||||
backgroundColor: "#f5f5f5",
|
backgroundColor: colors.backgroundSecondary,
|
||||||
color: "#999",
|
color: colors.textSecondary,
|
||||||
borderColor: "#eee",
|
borderColor: colors.border,
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
color: "#dc3545",
|
color: colors.error,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
buttonRow: {
|
buttonRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
},
|
},
|
||||||
removeButton: {
|
removeButton: {
|
||||||
backgroundColor: "#dc3545",
|
backgroundColor: colors.error,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
removeButtonText: {
|
removeButtonText: {
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
addButton: {
|
addButton: {
|
||||||
backgroundColor: "#007AFF",
|
backgroundColor: colors.primary,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOpacity: 0.05,
|
shadowOpacity: 0.05,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
addButtonText: {
|
addButtonText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
},
|
},
|
||||||
footerSection: {
|
footerSection: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
backgroundColor: "#fff",
|
backgroundColor: colors.surface,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: "#eee",
|
borderTopColor: colors.separator,
|
||||||
},
|
},
|
||||||
saveButtonLarge: {
|
saveButtonLarge: {
|
||||||
backgroundColor: "#007bff",
|
backgroundColor: colors.primary,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
saveButtonLargeText: {
|
saveButtonLargeText: {
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
},
|
},
|
||||||
emptyStateText: {
|
emptyStateText: {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
color: "#999",
|
color: colors.textSecondary,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default styles;
|
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
|
import { Colors } from "@/constants/theme";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
export const createStyles = (colors: typeof Colors.light) =>
|
||||||
container: {
|
StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: "#f5f5f5",
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.backgroundSecondary,
|
||||||
header: {
|
},
|
||||||
flexDirection: "row",
|
header: {
|
||||||
justifyContent: "space-between",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: 20,
|
alignItems: "center",
|
||||||
paddingTop: 30,
|
paddingHorizontal: 20,
|
||||||
paddingBottom: 16,
|
paddingTop: 30,
|
||||||
backgroundColor: "#fff",
|
paddingBottom: 16,
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.surface,
|
||||||
borderBottomColor: "#eee",
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.separator,
|
||||||
title: {
|
},
|
||||||
fontSize: 22,
|
title: {
|
||||||
fontWeight: "700",
|
fontSize: 22,
|
||||||
color: "#000",
|
fontWeight: "700",
|
||||||
flex: 1,
|
color: colors.text,
|
||||||
},
|
flex: 1,
|
||||||
closeButton: {
|
},
|
||||||
padding: 4,
|
closeButton: {
|
||||||
},
|
padding: 4,
|
||||||
closeIconButton: {
|
},
|
||||||
backgroundColor: "#FF3B30",
|
closeIconButton: {
|
||||||
borderRadius: 10,
|
backgroundColor: colors.error,
|
||||||
padding: 10,
|
borderRadius: 10,
|
||||||
justifyContent: "center",
|
padding: 10,
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
},
|
alignItems: "center",
|
||||||
content: {
|
},
|
||||||
flex: 1,
|
content: {
|
||||||
padding: 16,
|
flex: 1,
|
||||||
marginBottom: 15,
|
padding: 16,
|
||||||
},
|
marginBottom: 15,
|
||||||
infoCard: {
|
},
|
||||||
backgroundColor: "#fff",
|
infoCard: {
|
||||||
borderRadius: 12,
|
backgroundColor: colors.card,
|
||||||
padding: 16,
|
borderRadius: 12,
|
||||||
marginBottom: 35,
|
padding: 16,
|
||||||
shadowColor: "#000",
|
marginBottom: 35,
|
||||||
shadowOpacity: 0.05,
|
shadowColor: "#000",
|
||||||
shadowRadius: 4,
|
shadowOpacity: 0.05,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
shadowOffset: { width: 0, height: 2 },
|
||||||
},
|
elevation: 2,
|
||||||
infoRow: {
|
},
|
||||||
paddingVertical: 12,
|
infoRow: {
|
||||||
borderBottomWidth: 1,
|
paddingVertical: 12,
|
||||||
borderBottomColor: "#f0f0f0",
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.separator,
|
||||||
infoLabel: {
|
},
|
||||||
fontSize: 13,
|
infoLabel: {
|
||||||
fontWeight: "600",
|
fontSize: 13,
|
||||||
color: "#666",
|
fontWeight: "600",
|
||||||
marginBottom: 6,
|
color: colors.textSecondary,
|
||||||
},
|
marginBottom: 6,
|
||||||
infoValue: {
|
},
|
||||||
fontSize: 16,
|
infoValue: {
|
||||||
color: "#000",
|
fontSize: 16,
|
||||||
fontWeight: "500",
|
color: colors.text,
|
||||||
},
|
fontWeight: "500",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
export default styles;
|
|
||||||
|
|||||||
@@ -1,292 +1,293 @@
|
|||||||
|
import { Colors } from "@/constants/theme";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
export const createStyles = (colors: typeof Colors.light) =>
|
||||||
container: {
|
StyleSheet.create({
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: "#f5f5f5",
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.backgroundSecondary,
|
||||||
header: {
|
},
|
||||||
flexDirection: "row",
|
header: {
|
||||||
justifyContent: "space-between",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: 20,
|
alignItems: "center",
|
||||||
paddingTop: 30,
|
paddingHorizontal: 20,
|
||||||
paddingBottom: 16,
|
paddingTop: 30,
|
||||||
backgroundColor: "#fff",
|
paddingBottom: 16,
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.surface,
|
||||||
borderBottomColor: "#eee",
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.separator,
|
||||||
title: {
|
},
|
||||||
fontSize: 22,
|
title: {
|
||||||
fontWeight: "700",
|
fontSize: 22,
|
||||||
color: "#000",
|
fontWeight: "700",
|
||||||
flex: 1,
|
color: colors.text,
|
||||||
},
|
flex: 1,
|
||||||
closeButton: {
|
},
|
||||||
padding: 4,
|
closeButton: {
|
||||||
},
|
padding: 4,
|
||||||
closeIconButton: {
|
},
|
||||||
backgroundColor: "#FF3B30",
|
closeIconButton: {
|
||||||
borderRadius: 10,
|
backgroundColor: colors.error,
|
||||||
padding: 10,
|
borderRadius: 10,
|
||||||
justifyContent: "center",
|
padding: 10,
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
},
|
alignItems: "center",
|
||||||
content: {
|
},
|
||||||
flex: 1,
|
content: {
|
||||||
padding: 16,
|
flex: 1,
|
||||||
marginBottom: 15,
|
padding: 16,
|
||||||
},
|
marginBottom: 15,
|
||||||
infoCard: {
|
},
|
||||||
backgroundColor: "#fff",
|
infoCard: {
|
||||||
borderRadius: 12,
|
backgroundColor: colors.card,
|
||||||
padding: 16,
|
borderRadius: 12,
|
||||||
marginBottom: 35,
|
padding: 16,
|
||||||
shadowColor: "#000",
|
marginBottom: 35,
|
||||||
shadowOpacity: 0.05,
|
shadowColor: "#000",
|
||||||
shadowRadius: 4,
|
shadowOpacity: 0.05,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
shadowOffset: { width: 0, height: 2 },
|
||||||
},
|
elevation: 2,
|
||||||
infoRow: {
|
},
|
||||||
paddingVertical: 12,
|
infoRow: {
|
||||||
borderBottomWidth: 1,
|
paddingVertical: 12,
|
||||||
borderBottomColor: "#f0f0f0",
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.separator,
|
||||||
infoLabel: {
|
},
|
||||||
fontSize: 13,
|
infoLabel: {
|
||||||
fontWeight: "600",
|
fontSize: 13,
|
||||||
color: "#666",
|
fontWeight: "600",
|
||||||
marginBottom: 6,
|
color: colors.textSecondary,
|
||||||
},
|
marginBottom: 6,
|
||||||
infoValue: {
|
},
|
||||||
fontSize: 16,
|
infoValue: {
|
||||||
color: "#000",
|
fontSize: 16,
|
||||||
fontWeight: "500",
|
color: colors.text,
|
||||||
},
|
fontWeight: "500",
|
||||||
statusBadge: {
|
},
|
||||||
paddingHorizontal: 12,
|
statusBadge: {
|
||||||
paddingVertical: 6,
|
paddingHorizontal: 12,
|
||||||
borderRadius: 8,
|
paddingVertical: 6,
|
||||||
alignSelf: "flex-start",
|
borderRadius: 8,
|
||||||
},
|
alignSelf: "flex-start",
|
||||||
statusBadgeCompleted: {
|
},
|
||||||
backgroundColor: "#e8f5e9",
|
statusBadgeCompleted: {
|
||||||
},
|
backgroundColor: colors.success,
|
||||||
statusBadgeInProgress: {
|
},
|
||||||
backgroundColor: "#fff3e0",
|
statusBadgeInProgress: {
|
||||||
},
|
backgroundColor: colors.warning,
|
||||||
statusBadgeText: {
|
},
|
||||||
fontSize: 14,
|
statusBadgeText: {
|
||||||
fontWeight: "600",
|
fontSize: 14,
|
||||||
},
|
fontWeight: "600",
|
||||||
statusBadgeTextCompleted: {
|
color: "#fff",
|
||||||
color: "#2e7d32",
|
},
|
||||||
},
|
statusBadgeTextCompleted: {
|
||||||
statusBadgeTextInProgress: {
|
color: "#fff",
|
||||||
color: "#f57c00",
|
},
|
||||||
},
|
statusBadgeTextInProgress: {
|
||||||
headerButtons: {
|
color: "#fff",
|
||||||
flexDirection: "row",
|
},
|
||||||
alignItems: "center",
|
headerButtons: {
|
||||||
gap: 12,
|
flexDirection: "row",
|
||||||
},
|
alignItems: "center",
|
||||||
editButton: {
|
gap: 12,
|
||||||
padding: 4,
|
},
|
||||||
},
|
editButton: {
|
||||||
editIconButton: {
|
padding: 4,
|
||||||
backgroundColor: "#007AFF",
|
},
|
||||||
borderRadius: 10,
|
editIconButton: {
|
||||||
padding: 10,
|
backgroundColor: colors.primary,
|
||||||
justifyContent: "center",
|
borderRadius: 10,
|
||||||
alignItems: "center",
|
padding: 10,
|
||||||
},
|
justifyContent: "center",
|
||||||
cancelButton: {
|
alignItems: "center",
|
||||||
paddingHorizontal: 12,
|
},
|
||||||
paddingVertical: 6,
|
cancelButton: {
|
||||||
},
|
paddingHorizontal: 12,
|
||||||
cancelButtonText: {
|
paddingVertical: 6,
|
||||||
color: "#007AFF",
|
},
|
||||||
fontSize: 16,
|
cancelButtonText: {
|
||||||
fontWeight: "600",
|
color: colors.primary,
|
||||||
},
|
fontSize: 16,
|
||||||
saveButton: {
|
fontWeight: "600",
|
||||||
backgroundColor: "#007AFF",
|
},
|
||||||
paddingHorizontal: 16,
|
saveButton: {
|
||||||
paddingVertical: 6,
|
backgroundColor: colors.primary,
|
||||||
borderRadius: 6,
|
paddingHorizontal: 16,
|
||||||
},
|
paddingVertical: 6,
|
||||||
saveButtonText: {
|
borderRadius: 6,
|
||||||
color: "#fff",
|
},
|
||||||
fontSize: 16,
|
saveButtonText: {
|
||||||
fontWeight: "600",
|
color: "#fff",
|
||||||
},
|
fontSize: 16,
|
||||||
sectionHeader: {
|
fontWeight: "600",
|
||||||
flexDirection: "row",
|
},
|
||||||
justifyContent: "space-between",
|
sectionHeader: {
|
||||||
alignItems: "center",
|
flexDirection: "row",
|
||||||
marginTop: 16,
|
justifyContent: "space-between",
|
||||||
marginBottom: 12,
|
alignItems: "center",
|
||||||
paddingHorizontal: 4,
|
marginTop: 16,
|
||||||
},
|
marginBottom: 12,
|
||||||
sectionTitle: {
|
paddingHorizontal: 4,
|
||||||
fontSize: 18,
|
},
|
||||||
fontWeight: "700",
|
sectionTitle: {
|
||||||
color: "#000",
|
fontSize: 18,
|
||||||
},
|
fontWeight: "700",
|
||||||
totalCatchText: {
|
color: colors.text,
|
||||||
fontSize: 16,
|
},
|
||||||
fontWeight: "600",
|
totalCatchText: {
|
||||||
color: "#007AFF",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
fishCard: {
|
color: colors.primary,
|
||||||
position: "relative",
|
},
|
||||||
backgroundColor: "#fff",
|
fishCard: {
|
||||||
borderRadius: 12,
|
position: "relative",
|
||||||
padding: 16,
|
backgroundColor: colors.card,
|
||||||
marginBottom: 12,
|
borderRadius: 12,
|
||||||
shadowColor: "#000",
|
padding: 16,
|
||||||
shadowOpacity: 0.05,
|
marginBottom: 12,
|
||||||
shadowRadius: 4,
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOpacity: 0.05,
|
||||||
elevation: 2,
|
shadowRadius: 4,
|
||||||
},
|
shadowOffset: { width: 0, height: 2 },
|
||||||
fieldGroup: {
|
elevation: 2,
|
||||||
marginBottom: 12,
|
},
|
||||||
marginTop: 0,
|
fieldGroup: {
|
||||||
},
|
marginBottom: 12,
|
||||||
rowGroup: {
|
marginTop: 0,
|
||||||
flexDirection: "row",
|
},
|
||||||
marginBottom: 12,
|
rowGroup: {
|
||||||
},
|
flexDirection: "row",
|
||||||
label: {
|
marginBottom: 12,
|
||||||
fontSize: 13,
|
},
|
||||||
fontWeight: "600",
|
label: {
|
||||||
color: "#666",
|
fontSize: 13,
|
||||||
marginBottom: 6,
|
fontWeight: "600",
|
||||||
},
|
color: colors.textSecondary,
|
||||||
input: {
|
marginBottom: 6,
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: "#007AFF",
|
input: {
|
||||||
borderRadius: 8,
|
borderWidth: 1,
|
||||||
paddingHorizontal: 12,
|
borderColor: colors.primary,
|
||||||
paddingVertical: 10,
|
borderRadius: 8,
|
||||||
fontSize: 15,
|
paddingHorizontal: 12,
|
||||||
color: "#000",
|
paddingVertical: 10,
|
||||||
backgroundColor: "#fff",
|
fontSize: 15,
|
||||||
},
|
color: colors.text,
|
||||||
selectButton: {
|
backgroundColor: colors.surface,
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: "#007AFF",
|
selectButton: {
|
||||||
borderRadius: 8,
|
borderWidth: 1,
|
||||||
paddingHorizontal: 12,
|
borderColor: colors.primary,
|
||||||
paddingVertical: 10,
|
borderRadius: 8,
|
||||||
flexDirection: "row",
|
paddingHorizontal: 12,
|
||||||
justifyContent: "space-between",
|
paddingVertical: 10,
|
||||||
alignItems: "center",
|
flexDirection: "row",
|
||||||
backgroundColor: "#fff",
|
justifyContent: "space-between",
|
||||||
},
|
alignItems: "center",
|
||||||
selectButtonText: {
|
backgroundColor: colors.surface,
|
||||||
fontSize: 15,
|
},
|
||||||
color: "#000",
|
selectButtonText: {
|
||||||
},
|
fontSize: 15,
|
||||||
optionsList: {
|
color: colors.text,
|
||||||
position: "absolute",
|
},
|
||||||
top: 46,
|
optionsList: {
|
||||||
left: 0,
|
position: "absolute",
|
||||||
right: 0,
|
top: 46,
|
||||||
borderWidth: 1,
|
left: 0,
|
||||||
borderColor: "#007AFF",
|
right: 0,
|
||||||
borderRadius: 8,
|
borderWidth: 1,
|
||||||
marginTop: 4,
|
borderColor: colors.primary,
|
||||||
backgroundColor: "#fff",
|
borderRadius: 8,
|
||||||
maxHeight: 100,
|
marginTop: 4,
|
||||||
zIndex: 1000,
|
backgroundColor: colors.surface,
|
||||||
elevation: 5,
|
maxHeight: 100,
|
||||||
shadowColor: "#000",
|
zIndex: 1000,
|
||||||
shadowOpacity: 0.15,
|
elevation: 5,
|
||||||
shadowRadius: 8,
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOpacity: 0.15,
|
||||||
},
|
shadowRadius: 8,
|
||||||
optionItem: {
|
shadowOffset: { width: 0, height: 4 },
|
||||||
paddingHorizontal: 12,
|
},
|
||||||
paddingVertical: 12,
|
optionItem: {
|
||||||
borderBottomWidth: 1,
|
paddingHorizontal: 12,
|
||||||
borderBottomColor: "#f0f0f0",
|
paddingVertical: 12,
|
||||||
},
|
borderBottomWidth: 1,
|
||||||
optionText: {
|
borderBottomColor: colors.separator,
|
||||||
fontSize: 15,
|
},
|
||||||
color: "#000",
|
optionText: {
|
||||||
},
|
fontSize: 15,
|
||||||
optionsStatusFishList: {
|
color: colors.text,
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: "#007AFF",
|
optionsStatusFishList: {
|
||||||
borderRadius: 8,
|
borderWidth: 1,
|
||||||
marginTop: 4,
|
borderColor: colors.primary,
|
||||||
backgroundColor: "#fff",
|
borderRadius: 8,
|
||||||
maxHeight: 120,
|
marginTop: 4,
|
||||||
zIndex: 1000,
|
backgroundColor: colors.surface,
|
||||||
elevation: 5,
|
maxHeight: 120,
|
||||||
shadowColor: "#000",
|
zIndex: 1000,
|
||||||
shadowOpacity: 0.15,
|
elevation: 5,
|
||||||
shadowRadius: 8,
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOpacity: 0.15,
|
||||||
},
|
shadowRadius: 8,
|
||||||
fishNameDropdown: {
|
shadowOffset: { width: 0, height: 4 },
|
||||||
position: "absolute",
|
},
|
||||||
top: 46,
|
fishNameDropdown: {
|
||||||
left: 0,
|
position: "absolute",
|
||||||
right: 0,
|
top: 46,
|
||||||
borderWidth: 1,
|
left: 0,
|
||||||
borderColor: "#007AFF",
|
right: 0,
|
||||||
borderRadius: 8,
|
borderWidth: 1,
|
||||||
marginTop: 4,
|
borderColor: colors.primary,
|
||||||
backgroundColor: "#fff",
|
borderRadius: 8,
|
||||||
maxHeight: 180,
|
marginTop: 4,
|
||||||
zIndex: 1000,
|
backgroundColor: colors.surface,
|
||||||
elevation: 5,
|
maxHeight: 180,
|
||||||
shadowColor: "#000",
|
zIndex: 1000,
|
||||||
shadowOpacity: 0.15,
|
elevation: 5,
|
||||||
shadowRadius: 8,
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOpacity: 0.15,
|
||||||
},
|
shadowRadius: 8,
|
||||||
fishCardHeaderContent: {
|
shadowOffset: { width: 0, height: 4 },
|
||||||
flexDirection: "row",
|
},
|
||||||
gap: 5,
|
fishCardHeaderContent: {
|
||||||
},
|
flexDirection: "row",
|
||||||
fishCardTitle: {
|
gap: 5,
|
||||||
fontSize: 16,
|
},
|
||||||
fontWeight: "600",
|
fishCardTitle: {
|
||||||
color: "#000",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
fishCardSubtitle: {
|
color: colors.text,
|
||||||
fontSize: 15,
|
},
|
||||||
color: "#ff6600",
|
fishCardSubtitle: {
|
||||||
marginTop: 0,
|
fontSize: 15,
|
||||||
},
|
color: colors.warning,
|
||||||
addFishButton: {
|
marginTop: 0,
|
||||||
backgroundColor: "#007AFF",
|
},
|
||||||
borderRadius: 12,
|
addFishButton: {
|
||||||
padding: 16,
|
backgroundColor: colors.primary,
|
||||||
marginBottom: 12,
|
borderRadius: 12,
|
||||||
justifyContent: "center",
|
padding: 16,
|
||||||
alignItems: "center",
|
marginBottom: 12,
|
||||||
shadowColor: "#000",
|
justifyContent: "center",
|
||||||
shadowOpacity: 0.05,
|
alignItems: "center",
|
||||||
shadowRadius: 4,
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOpacity: 0.05,
|
||||||
elevation: 2,
|
shadowRadius: 4,
|
||||||
},
|
shadowOffset: { width: 0, height: 2 },
|
||||||
addFishButtonContent: {
|
elevation: 2,
|
||||||
flexDirection: "row",
|
},
|
||||||
alignItems: "center",
|
addFishButtonContent: {
|
||||||
gap: 8,
|
flexDirection: "row",
|
||||||
},
|
alignItems: "center",
|
||||||
addFishButtonText: {
|
gap: 8,
|
||||||
fontSize: 16,
|
},
|
||||||
fontWeight: "600",
|
addFishButtonText: {
|
||||||
color: "#fff",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
});
|
color: "#fff",
|
||||||
|
},
|
||||||
export default styles;
|
});
|
||||||
|
|||||||
@@ -1,153 +1,153 @@
|
|||||||
|
import { Colors } from "@/constants/theme";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
export const createStyles = (colors: typeof Colors.light) =>
|
||||||
closeIconButton: {
|
StyleSheet.create({
|
||||||
backgroundColor: "#FF3B30",
|
closeIconButton: {
|
||||||
borderRadius: 10,
|
backgroundColor: colors.error,
|
||||||
padding: 10,
|
borderRadius: 10,
|
||||||
justifyContent: "center",
|
padding: 10,
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
},
|
alignItems: "center",
|
||||||
container: {
|
},
|
||||||
flex: 1,
|
container: {
|
||||||
backgroundColor: "#f5f5f5",
|
flex: 1,
|
||||||
},
|
backgroundColor: colors.backgroundSecondary,
|
||||||
header: {
|
},
|
||||||
flexDirection: "row",
|
header: {
|
||||||
justifyContent: "space-between",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: 20,
|
alignItems: "center",
|
||||||
paddingTop: 30,
|
paddingHorizontal: 20,
|
||||||
paddingBottom: 16,
|
paddingTop: 30,
|
||||||
backgroundColor: "#fff",
|
paddingBottom: 16,
|
||||||
borderBottomWidth: 1,
|
backgroundColor: colors.surface,
|
||||||
borderBottomColor: "#eee",
|
borderBottomWidth: 1,
|
||||||
},
|
borderBottomColor: colors.separator,
|
||||||
title: {
|
},
|
||||||
fontSize: 22,
|
title: {
|
||||||
fontWeight: "700",
|
fontSize: 22,
|
||||||
color: "#000",
|
fontWeight: "700",
|
||||||
flex: 1,
|
color: colors.text,
|
||||||
},
|
flex: 1,
|
||||||
headerButtons: {
|
},
|
||||||
flexDirection: "row",
|
headerButtons: {
|
||||||
alignItems: "center",
|
flexDirection: "row",
|
||||||
gap: 12,
|
alignItems: "center",
|
||||||
},
|
gap: 12,
|
||||||
editButton: {
|
},
|
||||||
padding: 4,
|
editButton: {
|
||||||
},
|
padding: 4,
|
||||||
editIconButton: {
|
},
|
||||||
backgroundColor: "#007AFF",
|
editIconButton: {
|
||||||
borderRadius: 10,
|
backgroundColor: colors.primary,
|
||||||
padding: 10,
|
borderRadius: 10,
|
||||||
justifyContent: "center",
|
padding: 10,
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
},
|
alignItems: "center",
|
||||||
cancelButton: {
|
},
|
||||||
paddingHorizontal: 12,
|
cancelButton: {
|
||||||
paddingVertical: 6,
|
paddingHorizontal: 12,
|
||||||
},
|
paddingVertical: 6,
|
||||||
cancelButtonText: {
|
},
|
||||||
color: "#007AFF",
|
cancelButtonText: {
|
||||||
fontSize: 16,
|
color: colors.primary,
|
||||||
fontWeight: "600",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
saveButton: {
|
},
|
||||||
backgroundColor: "#007AFF",
|
saveButton: {
|
||||||
paddingHorizontal: 16,
|
backgroundColor: colors.primary,
|
||||||
paddingVertical: 6,
|
paddingHorizontal: 16,
|
||||||
borderRadius: 6,
|
paddingVertical: 6,
|
||||||
},
|
borderRadius: 6,
|
||||||
saveButtonText: {
|
},
|
||||||
color: "#fff",
|
saveButtonText: {
|
||||||
fontSize: 16,
|
color: "#fff",
|
||||||
fontWeight: "600",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
closeButton: {
|
},
|
||||||
padding: 4,
|
closeButton: {
|
||||||
},
|
padding: 4,
|
||||||
content: {
|
},
|
||||||
flex: 1,
|
content: {
|
||||||
padding: 16,
|
flex: 1,
|
||||||
},
|
padding: 16,
|
||||||
itemCard: {
|
},
|
||||||
backgroundColor: "#fff",
|
itemCard: {
|
||||||
borderRadius: 12,
|
backgroundColor: colors.surfaceSecondary,
|
||||||
padding: 16,
|
borderRadius: 12,
|
||||||
marginBottom: 12,
|
padding: 16,
|
||||||
shadowColor: "#000",
|
marginBottom: 12,
|
||||||
shadowOpacity: 0.05,
|
shadowColor: "#000",
|
||||||
shadowRadius: 4,
|
shadowOpacity: 0.05,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
shadowOffset: { width: 0, height: 2 },
|
||||||
},
|
elevation: 2,
|
||||||
fieldGroup: {
|
},
|
||||||
marginBottom: 12,
|
fieldGroup: {
|
||||||
},
|
marginBottom: 12,
|
||||||
rowGroup: {
|
},
|
||||||
flexDirection: "row",
|
rowGroup: {
|
||||||
marginBottom: 12,
|
flexDirection: "row",
|
||||||
},
|
marginBottom: 12,
|
||||||
label: {
|
},
|
||||||
fontSize: 13,
|
label: {
|
||||||
fontWeight: "600",
|
fontSize: 13,
|
||||||
color: "#666",
|
fontWeight: "600",
|
||||||
marginBottom: 6,
|
color: colors.textSecondary,
|
||||||
},
|
marginBottom: 6,
|
||||||
input: {
|
},
|
||||||
borderWidth: 1,
|
input: {
|
||||||
borderColor: "#007AFF",
|
borderWidth: 1,
|
||||||
borderRadius: 8,
|
borderColor: colors.primary,
|
||||||
paddingHorizontal: 12,
|
borderRadius: 8,
|
||||||
paddingVertical: 10,
|
paddingHorizontal: 12,
|
||||||
fontSize: 15,
|
paddingVertical: 10,
|
||||||
color: "#000",
|
fontSize: 15,
|
||||||
backgroundColor: "#fff",
|
color: colors.text,
|
||||||
},
|
backgroundColor: colors.surface,
|
||||||
inputDisabled: {
|
},
|
||||||
borderColor: "#ddd",
|
inputDisabled: {
|
||||||
backgroundColor: "#f9f9f9",
|
borderColor: colors.border,
|
||||||
color: "#666",
|
backgroundColor: colors.backgroundSecondary,
|
||||||
},
|
color: colors.textSecondary,
|
||||||
totalContainer: {
|
},
|
||||||
backgroundColor: "#fff5e6",
|
totalContainer: {
|
||||||
borderRadius: 8,
|
backgroundColor: colors.backgroundSecondary,
|
||||||
paddingHorizontal: 12,
|
borderRadius: 8,
|
||||||
paddingVertical: 10,
|
paddingHorizontal: 12,
|
||||||
borderWidth: 1,
|
paddingVertical: 10,
|
||||||
borderColor: "#ffd699",
|
borderWidth: 1,
|
||||||
},
|
borderColor: colors.border,
|
||||||
totalText: {
|
},
|
||||||
fontSize: 16,
|
totalText: {
|
||||||
fontWeight: "700",
|
fontSize: 16,
|
||||||
color: "#ff6600",
|
fontWeight: "700",
|
||||||
},
|
color: colors.warning,
|
||||||
footerTotal: {
|
},
|
||||||
backgroundColor: "#fff",
|
footerTotal: {
|
||||||
borderRadius: 12,
|
backgroundColor: colors.card,
|
||||||
padding: 20,
|
borderRadius: 12,
|
||||||
marginTop: 8,
|
padding: 20,
|
||||||
marginBottom: 50,
|
marginTop: 8,
|
||||||
flexDirection: "row",
|
marginBottom: 50,
|
||||||
justifyContent: "space-between",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
shadowColor: "#000",
|
alignItems: "center",
|
||||||
shadowOpacity: 0.1,
|
shadowColor: "#000",
|
||||||
shadowRadius: 4,
|
shadowOpacity: 0.1,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowRadius: 4,
|
||||||
elevation: 3,
|
shadowOffset: { width: 0, height: 2 },
|
||||||
},
|
elevation: 3,
|
||||||
footerLabel: {
|
},
|
||||||
fontSize: 18,
|
footerLabel: {
|
||||||
fontWeight: "700",
|
fontSize: 18,
|
||||||
color: "#007bff",
|
fontWeight: "700",
|
||||||
},
|
color: colors.primary,
|
||||||
footerAmount: {
|
},
|
||||||
fontSize: 20,
|
footerAmount: {
|
||||||
fontWeight: "700",
|
fontSize: 20,
|
||||||
color: "#ff6600",
|
fontWeight: "700",
|
||||||
},
|
color: colors.warning,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
export default styles;
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { ThemedText } from '@/components/themed-text';
|
|||||||
import { ThemedView } from '@/components/themed-view';
|
import { ThemedView } from '@/components/themed-view';
|
||||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
import { IconSymbol } from '@/components/ui/icon-symbol';
|
||||||
import { Colors } from '@/constants/theme';
|
import { Colors } from '@/constants/theme';
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from '@/hooks/use-theme-context';
|
||||||
|
|
||||||
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
|
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const theme = useColorScheme() ?? 'light';
|
const theme = useColorScheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView>
|
<ThemedView>
|
||||||
|
|||||||
@@ -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<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
|
||||||
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const AnimatedPressable = createMotionAnimatedComponent(
|
|
||||||
Pressable
|
|
||||||
) as React.ComponentType<IAnimatedPressableProps>;
|
|
||||||
|
|
||||||
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<typeof alertDialogStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogContentProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Content
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogContentStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IAlertDialogCloseButtonProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.CloseButton
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogCloseButtonStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogHeaderProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Header
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogHeaderStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogFooterProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Footer
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogFooterStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogBodyProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Body
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogBodyStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogBackdropProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Backdrop
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogBackdropStyle> & { className?: string };
|
|
||||||
|
|
||||||
const AlertDialog = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog>,
|
|
||||||
IAlertDialogProps
|
|
||||||
>(function AlertDialog({ className, size = 'md', ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogStyle({ class: className })}
|
|
||||||
context={{ size }}
|
|
||||||
pointerEvents="box-none"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Content>,
|
|
||||||
IAlertDialogContentProps
|
|
||||||
>(function AlertDialogContent({ className, size, ...props }, ref) {
|
|
||||||
const { size: parentSize } = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Content
|
|
||||||
pointerEvents="auto"
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
scale: 0.9,
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
scale: 1,
|
|
||||||
opacity: 1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
scale: 0.9,
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogContentStyle({
|
|
||||||
parentVariants: {
|
|
||||||
size: parentSize,
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogCloseButton = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.CloseButton>,
|
|
||||||
IAlertDialogCloseButtonProps
|
|
||||||
>(function AlertDialogCloseButton({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.CloseButton
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogCloseButtonStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogHeader = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Header>,
|
|
||||||
IAlertDialogHeaderProps
|
|
||||||
>(function AlertDialogHeader({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Header
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogHeaderStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogFooter = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Footer>,
|
|
||||||
IAlertDialogFooterProps
|
|
||||||
>(function AlertDialogFooter({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Footer
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogFooterStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogBody = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Body>,
|
|
||||||
IAlertDialogBodyProps
|
|
||||||
>(function AlertDialogBody({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Body
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogBodyStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogBackdrop = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Backdrop>,
|
|
||||||
IAlertDialogBackdropProps
|
|
||||||
>(function AlertDialogBackdrop({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Backdrop
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 0.5,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogBackdropStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -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<typeof UIButton>,
|
|
||||||
'context'
|
|
||||||
> &
|
|
||||||
VariantProps<typeof buttonStyle> & { className?: string };
|
|
||||||
|
|
||||||
const Button = React.forwardRef<
|
|
||||||
React.ElementRef<typeof UIButton>,
|
|
||||||
IButtonProps
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<UIButton
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={buttonStyle({ variant, size, action, class: className })}
|
|
||||||
context={{ variant, size, action }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text> &
|
|
||||||
VariantProps<typeof buttonTextStyle> & { className?: string };
|
|
||||||
|
|
||||||
const ButtonText = React.forwardRef<
|
|
||||||
React.ElementRef<typeof UIButton.Text>,
|
|
||||||
IButtonTextProps
|
|
||||||
>(({ className, variant, size, action, ...props }, ref) => {
|
|
||||||
const {
|
|
||||||
variant: parentVariant,
|
|
||||||
size: parentSize,
|
|
||||||
action: parentAction,
|
|
||||||
} = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UIButton.Text
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={buttonTextStyle({
|
|
||||||
parentVariants: {
|
|
||||||
variant: parentVariant,
|
|
||||||
size: parentSize,
|
|
||||||
action: parentAction,
|
|
||||||
},
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
action,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ButtonSpinner = UIButton.Spinner;
|
|
||||||
|
|
||||||
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon> &
|
|
||||||
VariantProps<typeof buttonIconStyle> & {
|
|
||||||
className?: string | undefined;
|
|
||||||
as?: React.ElementType;
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ButtonIcon = React.forwardRef<
|
|
||||||
React.ElementRef<typeof UIButton.Icon>,
|
|
||||||
IButtonIcon
|
|
||||||
>(({ className, size, ...props }, ref) => {
|
|
||||||
const {
|
|
||||||
variant: parentVariant,
|
|
||||||
size: parentSize,
|
|
||||||
action: parentAction,
|
|
||||||
} = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
if (typeof size === 'number') {
|
|
||||||
return (
|
|
||||||
<UIButton.Icon
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={buttonIconStyle({ class: className })}
|
|
||||||
size={size}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
(props.height !== undefined || props.width !== undefined) &&
|
|
||||||
size === undefined
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<UIButton.Icon
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={buttonIconStyle({ class: className })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<UIButton.Icon
|
|
||||||
{...props}
|
|
||||||
className={buttonIconStyle({
|
|
||||||
parentVariants: {
|
|
||||||
size: parentSize,
|
|
||||||
variant: parentVariant,
|
|
||||||
action: parentAction,
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group> &
|
|
||||||
VariantProps<typeof buttonGroupStyle>;
|
|
||||||
|
|
||||||
const ButtonGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof UIButton.Group>,
|
|
||||||
IButtonGroupProps
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
className,
|
|
||||||
space = 'md',
|
|
||||||
isAttached = false,
|
|
||||||
flexDirection = 'column',
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<UIButton.Group
|
|
||||||
className={buttonGroupStyle({
|
|
||||||
class: className,
|
|
||||||
space,
|
|
||||||
isAttached,
|
|
||||||
flexDirection,
|
|
||||||
})}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Button.displayName = 'Button';
|
|
||||||
ButtonText.displayName = 'ButtonText';
|
|
||||||
ButtonSpinner.displayName = 'ButtonSpinner';
|
|
||||||
ButtonIcon.displayName = 'ButtonIcon';
|
|
||||||
ButtonGroup.displayName = 'ButtonGroup';
|
|
||||||
|
|
||||||
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };
|
|
||||||
@@ -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',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -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 (
|
|
||||||
<OverlayProvider>
|
|
||||||
<ToastProvider>{props.children}</ToastProvider>
|
|
||||||
</OverlayProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
config[colorScheme!],
|
|
||||||
{ flex: 1, height: '100%', width: '100%' },
|
|
||||||
props.style,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<OverlayProvider>
|
|
||||||
<ToastProvider>{props.children}</ToastProvider>
|
|
||||||
</OverlayProvider>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<>
|
|
||||||
<script
|
|
||||||
suppressHydrationWarning
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `(${script.toString()})('${mode}')`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<OverlayProvider>
|
|
||||||
<ToastProvider>{props.children}</ToastProvider>
|
|
||||||
</OverlayProvider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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<typeof Pressable> &
|
|
||||||
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const AnimatedPressable = createMotionAnimatedComponent(
|
|
||||||
Pressable
|
|
||||||
) as React.ComponentType<IAnimatedPressableProps>;
|
|
||||||
const SCOPE = 'MODAL';
|
|
||||||
|
|
||||||
type IMotionViewProps = React.ComponentProps<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
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<typeof UIModal> &
|
|
||||||
VariantProps<typeof modalStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalBackdropProps = React.ComponentProps<typeof UIModal.Backdrop> &
|
|
||||||
VariantProps<typeof modalBackdropStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalContentProps = React.ComponentProps<typeof UIModal.Content> &
|
|
||||||
VariantProps<typeof modalContentStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalHeaderProps = React.ComponentProps<typeof UIModal.Header> &
|
|
||||||
VariantProps<typeof modalHeaderStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalBodyProps = React.ComponentProps<typeof UIModal.Body> &
|
|
||||||
VariantProps<typeof modalBodyStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalFooterProps = React.ComponentProps<typeof UIModal.Footer> &
|
|
||||||
VariantProps<typeof modalFooterStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalCloseButtonProps = React.ComponentProps<typeof UIModal.CloseButton> &
|
|
||||||
VariantProps<typeof modalCloseButtonStyle> & { className?: string };
|
|
||||||
|
|
||||||
const Modal = React.forwardRef<React.ComponentRef<typeof UIModal>, IModalProps>(
|
|
||||||
({ className, size = 'md', ...props }, ref) => (
|
|
||||||
<UIModal
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
pointerEvents="box-none"
|
|
||||||
className={modalStyle({ size, class: className })}
|
|
||||||
context={{ size }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const ModalBackdrop = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Backdrop>,
|
|
||||||
IModalBackdropProps
|
|
||||||
>(function ModalBackdrop({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Backdrop
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 0.5,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={modalBackdropStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Content>,
|
|
||||||
IModalContentProps
|
|
||||||
>(function ModalContent({ className, size, ...props }, ref) {
|
|
||||||
const { size: parentSize } = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UIModal.Content
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
scale: 0.9,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={modalContentStyle({
|
|
||||||
parentVariants: {
|
|
||||||
size: parentSize,
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
pointerEvents="auto"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalHeader = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Header>,
|
|
||||||
IModalHeaderProps
|
|
||||||
>(function ModalHeader({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Header
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalHeaderStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalBody = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Body>,
|
|
||||||
IModalBodyProps
|
|
||||||
>(function ModalBody({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Body
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalBodyStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalFooter = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Footer>,
|
|
||||||
IModalFooterProps
|
|
||||||
>(function ModalFooter({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Footer
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalFooterStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalCloseButton = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.CloseButton>,
|
|
||||||
IModalCloseButtonProps
|
|
||||||
>(function ModalCloseButton({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.CloseButton
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalCloseButtonStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -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<typeof Pressable> &
|
|
||||||
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const AnimatedPressable = createMotionAnimatedComponent(
|
|
||||||
Pressable
|
|
||||||
) as React.ComponentType<IAnimatedPressableProps>;
|
|
||||||
|
|
||||||
const SCOPE = 'POPOVER';
|
|
||||||
|
|
||||||
type IMotionViewProps = React.ComponentProps<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
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<typeof UIPopover> &
|
|
||||||
VariantProps<typeof popoverStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverArrowProps = React.ComponentProps<typeof UIPopover.Arrow> &
|
|
||||||
VariantProps<typeof popoverArrowStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverContentProps = React.ComponentProps<typeof UIPopover.Content> &
|
|
||||||
VariantProps<typeof popoverContentStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverHeaderProps = React.ComponentProps<typeof UIPopover.Header> &
|
|
||||||
VariantProps<typeof popoverHeaderStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverFooterProps = React.ComponentProps<typeof UIPopover.Footer> &
|
|
||||||
VariantProps<typeof popoverFooterStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverBodyProps = React.ComponentProps<typeof UIPopover.Body> &
|
|
||||||
VariantProps<typeof popoverBodyStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverBackdropProps = React.ComponentProps<typeof UIPopover.Backdrop> &
|
|
||||||
VariantProps<typeof popoverBackdropStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverCloseButtonProps = React.ComponentProps<
|
|
||||||
typeof UIPopover.CloseButton
|
|
||||||
> &
|
|
||||||
VariantProps<typeof popoverCloseButtonStyle> & { className?: string };
|
|
||||||
|
|
||||||
const Popover = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover>,
|
|
||||||
IPopoverProps
|
|
||||||
>(function Popover(
|
|
||||||
{ className, size = 'md', placement = 'bottom', ...props },
|
|
||||||
ref
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<UIPopover
|
|
||||||
ref={ref}
|
|
||||||
placement={placement}
|
|
||||||
{...props}
|
|
||||||
className={popoverStyle({ size, class: className })}
|
|
||||||
context={{ size, placement }}
|
|
||||||
pointerEvents="box-none"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Content>,
|
|
||||||
IPopoverContentProps
|
|
||||||
>(function PopoverContent({ className, size, ...props }, ref) {
|
|
||||||
const { size: parentSize } = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UIPopover.Content
|
|
||||||
ref={ref}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
mass: 0.9,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 50,
|
|
||||||
delay: 50,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={popoverContentStyle({
|
|
||||||
parentVariants: {
|
|
||||||
size: parentSize,
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
pointerEvents="auto"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverArrow = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Arrow>,
|
|
||||||
IPopoverArrowProps
|
|
||||||
>(function PopoverArrow({ className, ...props }, ref) {
|
|
||||||
const { placement } = useStyleContext(SCOPE);
|
|
||||||
return (
|
|
||||||
<UIPopover.Arrow
|
|
||||||
ref={ref}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
mass: 0.9,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 50,
|
|
||||||
delay: 50,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={popoverArrowStyle({
|
|
||||||
class: className,
|
|
||||||
placement,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverBackdrop = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Backdrop>,
|
|
||||||
IPopoverBackdropProps
|
|
||||||
>(function PopoverBackdrop({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Backdrop
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 0.1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 450,
|
|
||||||
mass: 0.9,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 50,
|
|
||||||
delay: 50,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
className={popoverBackdropStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverBody = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Body>,
|
|
||||||
IPopoverBodyProps
|
|
||||||
>(function PopoverBody({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Body
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverBodyStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverCloseButton = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.CloseButton>,
|
|
||||||
IPopoverCloseButtonProps
|
|
||||||
>(function PopoverCloseButton({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.CloseButton
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverCloseButtonStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverFooter = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Footer>,
|
|
||||||
IPopoverFooterProps
|
|
||||||
>(function PopoverFooter({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Footer
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverFooterStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverHeader = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Header>,
|
|
||||||
IPopoverHeaderProps
|
|
||||||
>(function PopoverHeader({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Header
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverHeaderStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -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<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
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<typeof UITooltip> &
|
|
||||||
VariantProps<typeof tooltipStyle> & { className?: string };
|
|
||||||
type ITooltipContentProps = React.ComponentProps<typeof UITooltip.Content> &
|
|
||||||
VariantProps<typeof tooltipContentStyle> & { className?: string };
|
|
||||||
type ITooltipTextProps = React.ComponentProps<typeof UITooltip.Text> &
|
|
||||||
VariantProps<typeof tooltipTextStyle> & { className?: string };
|
|
||||||
|
|
||||||
const Tooltip = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UITooltip>,
|
|
||||||
ITooltipProps
|
|
||||||
>(function Tooltip({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UITooltip
|
|
||||||
ref={ref}
|
|
||||||
className={tooltipStyle({ class: className })}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UITooltip.Content>,
|
|
||||||
ITooltipContentProps & { className?: string }
|
|
||||||
>(function TooltipContent({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UITooltip.Content
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={tooltipContentStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
pointerEvents="auto"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const TooltipText = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UITooltip.Text>,
|
|
||||||
ITooltipTextProps & { className?: string }
|
|
||||||
>(function TooltipText({ size, className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UITooltip.Text
|
|
||||||
ref={ref}
|
|
||||||
className={tooltipTextStyle({ size, class: className })}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Tooltip.displayName = 'Tooltip';
|
|
||||||
TooltipContent.displayName = 'TooltipContent';
|
|
||||||
TooltipText.displayName = 'TooltipText';
|
|
||||||
|
|
||||||
export { Tooltip, TooltipContent, TooltipText };
|
|
||||||
578
components/ui/modal.tsx
Normal file
578
components/ui/modal.tsx
Normal file
@@ -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<void>;
|
||||||
|
/** 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<ModalProps, "open"> {
|
||||||
|
/** Type of the confirm modal */
|
||||||
|
type?: "info" | "success" | "error" | "warning" | "confirm";
|
||||||
|
/** Content */
|
||||||
|
content?: ReactNode;
|
||||||
|
/** Custom icon */
|
||||||
|
icon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component
|
||||||
|
const Modal: React.FC<ModalProps> & {
|
||||||
|
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 <View style={styles.footer}>{footer}</View>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.cancelButton]}
|
||||||
|
onPress={handleCancel}
|
||||||
|
disabled={loading || confirmLoading}
|
||||||
|
{...cancelButtonProps}
|
||||||
|
>
|
||||||
|
<Text style={styles.cancelButtonText}>{cancelText}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
styles.okButton,
|
||||||
|
okType === "primary" && styles.primaryButton,
|
||||||
|
(loading || confirmLoading) && styles.disabledButton,
|
||||||
|
]}
|
||||||
|
onPress={handleOk}
|
||||||
|
disabled={loading || confirmLoading}
|
||||||
|
{...okButtonProps}
|
||||||
|
>
|
||||||
|
{loading || confirmLoading ? (
|
||||||
|
<ActivityIndicator color="#fff" size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.okButtonText}>{okText}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalWidth =
|
||||||
|
typeof width === "number" ? width : Dimensions.get("window").width * 0.9;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RNModal
|
||||||
|
visible={visible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={handleRequestClose}
|
||||||
|
statusBarTranslucent
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.overlay,
|
||||||
|
centered && styles.centered,
|
||||||
|
{ zIndex },
|
||||||
|
!mask && styles.noMask,
|
||||||
|
]}
|
||||||
|
onPress={handleMaskPress}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.modal, { width: modalWidth, maxWidth: "90%" }]}
|
||||||
|
onPress={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
{(title || closable) && (
|
||||||
|
<View style={styles.header}>
|
||||||
|
{title && (
|
||||||
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{closable && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.closeButton}
|
||||||
|
onPress={handleCancel}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
>
|
||||||
|
{closeIcon || (
|
||||||
|
<Ionicons name="close" size={24} color="#666" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<View style={styles.body}>
|
||||||
|
{(!destroyOnClose || visible) && children}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
{renderFooter()}
|
||||||
|
</Pressable>
|
||||||
|
</Pressable>
|
||||||
|
</RNModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<Ionicons name="information-circle" color="#1890ff" {...iconProps} />
|
||||||
|
);
|
||||||
|
case "success":
|
||||||
|
return (
|
||||||
|
<Ionicons name="checkmark-circle" color="#52c41a" {...iconProps} />
|
||||||
|
);
|
||||||
|
case "error":
|
||||||
|
return <Ionicons name="close-circle" color="#ff4d4f" {...iconProps} />;
|
||||||
|
case "warning":
|
||||||
|
return <Ionicons name="warning" color="#faad14" {...iconProps} />;
|
||||||
|
default:
|
||||||
|
return <Ionicons name="help-circle" color="#1890ff" {...iconProps} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Modal
|
||||||
|
open={visible}
|
||||||
|
title={title}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={type === "confirm" ? handleCancel : undefined}
|
||||||
|
okText={okText}
|
||||||
|
cancelText={cancelText}
|
||||||
|
confirmLoading={loading}
|
||||||
|
footer={
|
||||||
|
type === "confirm" ? undefined : (
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.okButton, styles.primaryButton]}
|
||||||
|
onPress={handleOk}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator color="#fff" size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.okButtonText}>{okText}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<View style={styles.confirmContent}>
|
||||||
|
{getIcon()}
|
||||||
|
<Text style={styles.confirmText}>{content}</Text>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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<ReactNode[]>([]);
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
<ConfirmModal
|
||||||
|
key={id}
|
||||||
|
visible={true}
|
||||||
|
onClose={destroy}
|
||||||
|
{...config}
|
||||||
|
{...newConfig}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
modal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalElement = (
|
||||||
|
<ConfirmModal
|
||||||
|
key={id}
|
||||||
|
visible={true}
|
||||||
|
onClose={destroy}
|
||||||
|
{...config}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
OpaqueColorValue,
|
OpaqueColorValue,
|
||||||
@@ -29,7 +29,7 @@ const PRESS_FEEDBACK_DURATION = 120;
|
|||||||
|
|
||||||
type IoniconName = ComponentProps<typeof Ionicons>["name"];
|
type IoniconName = ComponentProps<typeof Ionicons>["name"];
|
||||||
|
|
||||||
type RotateSwitchProps = {
|
type SliceSwitchProps = {
|
||||||
size?: SwitchSize;
|
size?: SwitchSize;
|
||||||
leftIcon?: IoniconName;
|
leftIcon?: IoniconName;
|
||||||
leftIconColor?: string | OpaqueColorValue | undefined;
|
leftIconColor?: string | OpaqueColorValue | undefined;
|
||||||
@@ -42,6 +42,7 @@ type RotateSwitchProps = {
|
|||||||
activeOverlayColor?: string;
|
activeOverlayColor?: string;
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
onChange?: (value: boolean) => void;
|
onChange?: (value: boolean) => void;
|
||||||
|
value?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SliceSwitch = ({
|
const SliceSwitch = ({
|
||||||
@@ -57,19 +58,28 @@ const SliceSwitch = ({
|
|||||||
activeOverlayColor = "#000",
|
activeOverlayColor = "#000",
|
||||||
style,
|
style,
|
||||||
onChange,
|
onChange,
|
||||||
}: RotateSwitchProps) => {
|
value,
|
||||||
|
}: SliceSwitchProps) => {
|
||||||
const { width: containerWidth, height: containerHeight } =
|
const { width: containerWidth, height: containerHeight } =
|
||||||
SIZE_PRESETS[size] ?? SIZE_PRESETS.md;
|
SIZE_PRESETS[size] ?? SIZE_PRESETS.md;
|
||||||
const animationDuration = Math.max(duration ?? DEFAULT_TOGGLE_DURATION, 0);
|
const animationDuration = Math.max(duration ?? DEFAULT_TOGGLE_DURATION, 0);
|
||||||
const [isOn, setIsOn] = useState(false);
|
const [isOn, setIsOn] = useState(value ?? false);
|
||||||
const [bgOn, setBgOn] = useState(false);
|
const [bgOn, setBgOn] = useState(value ?? false);
|
||||||
const progress = useRef(new Animated.Value(0)).current;
|
const progress = useRef(new Animated.Value(value ? 1 : 0)).current;
|
||||||
const pressScale = useRef(new Animated.Value(1)).current;
|
const pressScale = useRef(new Animated.Value(1)).current;
|
||||||
const overlayTranslateX = useRef(new Animated.Value(0)).current;
|
const overlayTranslateX = useRef(
|
||||||
|
new Animated.Value(value ? containerWidth / 2 : 0)
|
||||||
|
).current;
|
||||||
const listenerIdRef = useRef<string | number | null>(null);
|
const listenerIdRef = useRef<string | number | null>(null);
|
||||||
|
|
||||||
const handleToggle = () => {
|
// Sync with external value prop if provided
|
||||||
const next = !isOn;
|
useEffect(() => {
|
||||||
|
if (value !== undefined && value !== isOn) {
|
||||||
|
animateToValue(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const animateToValue = (next: boolean) => {
|
||||||
const targetValue = next ? 1 : 0;
|
const targetValue = next ? 1 : 0;
|
||||||
const overlayTarget = next ? containerWidth / 2 : 0;
|
const overlayTarget = next ? containerWidth / 2 : 0;
|
||||||
|
|
||||||
@@ -81,7 +91,6 @@ const SliceSwitch = ({
|
|||||||
overlayTranslateX.setValue(overlayTarget);
|
overlayTranslateX.setValue(overlayTarget);
|
||||||
setIsOn(next);
|
setIsOn(next);
|
||||||
setBgOn(next);
|
setBgOn(next);
|
||||||
onChange?.(next);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +109,6 @@ const SliceSwitch = ({
|
|||||||
}),
|
}),
|
||||||
]).start(() => {
|
]).start(() => {
|
||||||
setBgOn(next);
|
setBgOn(next);
|
||||||
onChange?.(next);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove any previous listener
|
// Remove any previous listener
|
||||||
@@ -132,6 +140,14 @@ const SliceSwitch = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
const next = !isOn;
|
||||||
|
if (value === undefined) {
|
||||||
|
animateToValue(next);
|
||||||
|
}
|
||||||
|
onChange?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePressIn = () => {
|
const handlePressIn = () => {
|
||||||
pressScale.stopAnimation();
|
pressScale.stopAnimation();
|
||||||
Animated.timing(pressScale, {
|
Animated.timing(pressScale, {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* Provides styled components and theme utilities
|
* Provides styled components and theme utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { StyleSheet, TextStyle, ViewStyle } from "react-native";
|
import { StyleSheet, TextStyle, ViewStyle } from "react-native";
|
||||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
||||||
|
|
||||||
export function useAppTheme() {
|
export function useAppTheme() {
|
||||||
const { colors, colorScheme, themeMode, setThemeMode, getColor } =
|
const { colors, colorScheme, themeMode, setThemeMode, getColor } =
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
* https://docs.expo.dev/guides/color-schemes/
|
* https://docs.expo.dev/guides/color-schemes/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ColorName } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
import { useColorScheme } from "@/hooks/use-theme-context";
|
||||||
|
|
||||||
export function useThemeColor(
|
export function useThemeColor(
|
||||||
props: { light?: string; dark?: string },
|
props: { light?: string; dark?: string },
|
||||||
colorName: ColorName
|
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||||
) {
|
) {
|
||||||
const { colorScheme, getColor } = useThemeContext();
|
const theme = useColorScheme();
|
||||||
const colorFromProps = props[colorScheme];
|
const colorFromProps = props[theme];
|
||||||
|
|
||||||
if (colorFromProps) {
|
if (colorFromProps) {
|
||||||
return colorFromProps;
|
return colorFromProps;
|
||||||
} else {
|
} else {
|
||||||
return getColor(colorName);
|
return Colors[theme][colorName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* Theme Context Hook for managing app-wide theme state
|
* Theme Context Hook for managing app-wide theme state.
|
||||||
* Supports Light, Dark, and System (automatic) modes
|
* 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 { getStorageItem, setStorageItem } from "@/utils/storage";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
ReactNode,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
useColorScheme as useSystemColorScheme,
|
|
||||||
Appearance,
|
Appearance,
|
||||||
|
AppState,
|
||||||
|
AppStateStatus,
|
||||||
|
useColorScheme as useRNColorScheme,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
|
||||||
type ThemeMode = "light" | "dark" | "system";
|
export type ThemeMode = "light" | "dark" | "system";
|
||||||
type ColorScheme = "light" | "dark";
|
export type ColorScheme = "light" | "dark";
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
@@ -28,146 +32,162 @@ interface ThemeContextType {
|
|||||||
colors: typeof Colors.light;
|
colors: typeof Colors.light;
|
||||||
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
||||||
getColor: (colorName: ColorName) => string;
|
getColor: (colorName: ColorName) => string;
|
||||||
|
isHydrated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
const THEME_STORAGE_KEY = "theme_mode";
|
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 }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
// State để force re-render khi system theme thay đổi
|
const [systemScheme, setSystemScheme] =
|
||||||
const [systemTheme, setSystemTheme] = useState<ColorScheme>(() => {
|
useState<ColorScheme>(getSystemScheme);
|
||||||
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 [themeMode, setThemeModeState] = useState<ThemeMode>("system");
|
const [themeMode, setThemeModeState] = useState<ThemeMode>("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(() => {
|
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 subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
||||||
const newScheme = colorScheme === "dark" ? "dark" : "light";
|
const next = colorScheme === "dark" ? "dark" : "light";
|
||||||
console.log(
|
// console.log("[Theme] Appearance listener fired with:", colorScheme);
|
||||||
"[Theme] System theme changed to:",
|
setSystemScheme((current) => (current === next ? current : next));
|
||||||
newScheme,
|
|
||||||
"at",
|
|
||||||
new Date().toLocaleTimeString()
|
|
||||||
);
|
|
||||||
setSystemTheme(newScheme);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Double check current theme khi mount
|
syncSystemScheme();
|
||||||
const currentScheme = Appearance.getColorScheme();
|
|
||||||
const current = currentScheme === "dark" ? "dark" : "light";
|
|
||||||
if (current !== systemTheme) {
|
|
||||||
console.log("[Theme] Syncing system theme on mount:", current);
|
|
||||||
setSystemTheme(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("[Theme] Removing appearance listener");
|
|
||||||
subscription.remove();
|
subscription.remove();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [syncSystemScheme]);
|
||||||
|
|
||||||
// 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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadThemeMode = async () => {
|
// console.log("[Theme] System scheme detected:", systemScheme);
|
||||||
try {
|
}, [systemScheme]);
|
||||||
const savedThemeMode = await getStorageItem(THEME_STORAGE_KEY);
|
|
||||||
if (
|
useEffect(() => {
|
||||||
savedThemeMode &&
|
const handleAppStateChange = (nextState: AppStateStatus) => {
|
||||||
["light", "dark", "system"].includes(savedThemeMode)
|
if (nextState === "active") {
|
||||||
) {
|
// console.log("[Theme] AppState active → scheduling system scheme sync");
|
||||||
setThemeModeState(savedThemeMode as ThemeMode);
|
setTimeout(() => {
|
||||||
}
|
// console.log("[Theme] AppState sync callback running");
|
||||||
} catch (error) {
|
syncSystemScheme();
|
||||||
console.warn("Failed to load theme mode:", error);
|
}, 100);
|
||||||
} finally {
|
|
||||||
setIsLoaded(true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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 {
|
try {
|
||||||
setThemeModeState(mode);
|
|
||||||
await setStorageItem(THEME_STORAGE_KEY, mode);
|
await setStorageItem(THEME_STORAGE_KEY, mode);
|
||||||
console.log("[Theme] Changed to:", mode);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to save theme mode:", error);
|
console.warn("[Theme] Failed to save theme mode:", error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getColor = (colorName: ColorName): string => {
|
useEffect(() => {
|
||||||
return colors[colorName] || colors.text;
|
// console.log("[Theme] window defined:", typeof window !== "undefined");
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// Chờ theme load xong trước khi render
|
const getColor = useCallback(
|
||||||
if (!isLoaded) {
|
(colorName: ColorName) => colors[colorName] ?? colors.text,
|
||||||
// Render với default theme (system) khi đang load
|
[colors]
|
||||||
return (
|
);
|
||||||
<ThemeContext.Provider
|
|
||||||
value={{
|
|
||||||
themeMode: "system",
|
|
||||||
colorScheme: systemTheme,
|
|
||||||
colors: Colors[systemTheme],
|
|
||||||
setThemeMode: async () => {},
|
|
||||||
getColor: (colorName: ColorName) =>
|
|
||||||
Colors[systemTheme][colorName] || Colors[systemTheme].text,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const value: ThemeContextType = {
|
useEffect(() => {
|
||||||
themeMode,
|
// console.log("[Theme] Mode:", themeMode);
|
||||||
colorScheme,
|
}, [themeMode]);
|
||||||
colors,
|
|
||||||
setThemeMode,
|
useEffect(() => {
|
||||||
getColor,
|
// console.log("[Theme] Derived colorScheme:", colorScheme);
|
||||||
};
|
}, [colorScheme]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
themeMode,
|
||||||
|
colorScheme,
|
||||||
|
colors,
|
||||||
|
setThemeMode,
|
||||||
|
getColor,
|
||||||
|
isHydrated,
|
||||||
|
}),
|
||||||
|
[themeMode, colorScheme, colors, setThemeMode, getColor, isHydrated]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useThemeContext(): ThemeContextType {
|
export function useTheme(): ThemeContextType {
|
||||||
const context = useContext(ThemeContext);
|
const context = useContext(ThemeContext);
|
||||||
if (context === undefined) {
|
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;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy hook cho backward compatibility
|
export const useThemeContext = useTheme;
|
||||||
|
|
||||||
export function useColorScheme(): ColorScheme {
|
export function useColorScheme(): ColorScheme {
|
||||||
const { colorScheme } = useThemeContext();
|
return useTheme().colorScheme;
|
||||||
return colorScheme;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trip": {
|
"trip": {
|
||||||
|
"infoTrip": "Trip Information",
|
||||||
"createNewTrip": "Create New Trip",
|
"createNewTrip": "Create New Trip",
|
||||||
"endTrip": "End Trip",
|
"endTrip": "End Trip",
|
||||||
"cancelTrip": "Cancel Trip",
|
"cancelTrip": "Cancel Trip",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trip": {
|
"trip": {
|
||||||
|
"infoTrip": "Thông Tin Chuyến Đi",
|
||||||
"createNewTrip": "Tạo chuyến mới",
|
"createNewTrip": "Tạo chuyến mới",
|
||||||
"endTrip": "Kết thúc chuyến",
|
"endTrip": "Kết thúc chuyến",
|
||||||
"cancelTrip": "Hủy chuyến",
|
"cancelTrip": "Hủy chuyến",
|
||||||
|
|||||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -10,8 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/html-elements": "^0.10.1",
|
"@expo/html-elements": "^0.10.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@gluestack-ui/core": "^3.0.12",
|
|
||||||
"@gluestack-ui/utils": "^3.0.11",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@islacel/react-native-custom-switch": "^1.0.10",
|
"@islacel/react-native-custom-switch": "^1.0.10",
|
||||||
"@legendapp/motion": "^2.5.3",
|
"@legendapp/motion": "^2.5.3",
|
||||||
@@ -2357,43 +2355,6 @@
|
|||||||
"tslib": "^2.8.0"
|
"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": {
|
"node_modules/@hookform/resolvers": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
||||||
@@ -7379,6 +7340,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-view-buffer": {
|
"node_modules/data-view-buffer": {
|
||||||
@@ -7625,16 +7587,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/html-elements": "^0.10.1",
|
"@expo/html-elements": "^0.10.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@gluestack-ui/core": "^3.0.12",
|
|
||||||
"@gluestack-ui/utils": "^3.0.11",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@islacel/react-native-custom-switch": "^1.0.10",
|
"@islacel/react-native-custom-switch": "^1.0.10",
|
||||||
"@legendapp/motion": "^2.5.3",
|
"@legendapp/motion": "^2.5.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user