Compare commits
4 Commits
7cb35efd30
...
MinhNN
| Author | SHA1 | Date | |
|---|---|---|---|
| 554289ee1e | |||
| 6975358a7f | |||
| 51327c7d01 | |||
| 1d5b29e4a7 |
460
THEME_GUIDE.md
460
THEME_GUIDE.md
@@ -2,11 +2,49 @@
|
||||
|
||||
## 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
|
||||
export const Colors = {
|
||||
@@ -17,11 +55,16 @@ export const Colors = {
|
||||
backgroundSecondary: "#f5f5f5",
|
||||
surface: "#ffffff",
|
||||
surfaceSecondary: "#f8f9fa",
|
||||
tint: "#0a7ea4",
|
||||
primary: "#007AFF",
|
||||
secondary: "#5AC8FA",
|
||||
success: "#34C759",
|
||||
warning: "#FF9500",
|
||||
warning: "#ff6600",
|
||||
error: "#FF3B30",
|
||||
icon: "#687076",
|
||||
border: "#C6C6C8",
|
||||
separator: "#E5E5E7",
|
||||
card: "#ffffff",
|
||||
// ... more colors
|
||||
},
|
||||
dark: {
|
||||
@@ -31,107 +74,110 @@ export const Colors = {
|
||||
backgroundSecondary: "#1C1C1E",
|
||||
surface: "#1C1C1E",
|
||||
surfaceSecondary: "#2C2C2E",
|
||||
tint: "#fff",
|
||||
primary: "#0A84FF",
|
||||
secondary: "#64D2FF",
|
||||
success: "#30D158",
|
||||
warning: "#FF9F0A",
|
||||
warning: "#ff6600",
|
||||
error: "#FF453A",
|
||||
icon: "#8E8E93",
|
||||
border: "#38383A",
|
||||
separator: "#38383A",
|
||||
card: "#1C1C1E",
|
||||
// ... 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
|
||||
interface ThemeContextType {
|
||||
themeMode: ThemeMode; // 'light' | 'dark' | 'system'
|
||||
colorScheme: ColorScheme; // 'light' | 'dark'
|
||||
colors: typeof Colors.light;
|
||||
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
||||
getColor: (colorName: ColorName) => string;
|
||||
```tsx
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<AppThemeProvider>
|
||||
<AppContent />
|
||||
</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
|
||||
|
||||
### 1. Sử dụng Themed Components
|
||||
### 1. useThemeContext (Core Hook)
|
||||
|
||||
```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="default">Regular Text</ThemedText>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Sử dụng Theme Hook
|
||||
Hook chính để access theme state:
|
||||
|
||||
```tsx
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
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 (
|
||||
<View style={{ backgroundColor: colors.background }}>
|
||||
<Text style={{ color: colors.text }}>Current theme: {colorScheme}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Sử dụng App Theme Hook (Recommended)
|
||||
|
||||
```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}>Button</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.surface,
|
||||
{
|
||||
backgroundColor: utils.getOpacityColor("primary", 0.1),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={{ color: colors.text }}>
|
||||
Theme is {utils.isDark ? "Dark" : "Light"}
|
||||
Mode: {themeMode}, Scheme: {colorScheme}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Sử dụng useThemeColor Hook
|
||||
### 2. useColorScheme Hook
|
||||
|
||||
Alias để lấy colorScheme nhanh:
|
||||
|
||||
```tsx
|
||||
import { useColorScheme } from "@/hooks/use-theme-context";
|
||||
|
||||
function MyComponent() {
|
||||
const colorScheme = useColorScheme(); // 'light' | 'dark'
|
||||
|
||||
return <Text>Current theme: {colorScheme}</Text>;
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Lưu ý:** `useColorScheme` từ `use-theme-context.tsx`, KHÔNG phải từ `react-native`.
|
||||
|
||||
### 3. useThemeColor Hook
|
||||
|
||||
Override colors cho specific themes:
|
||||
|
||||
```tsx
|
||||
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||
|
||||
function MyComponent() {
|
||||
// Override colors for specific themes
|
||||
// Với override
|
||||
const backgroundColor = useThemeColor(
|
||||
{ light: "#ffffff", dark: "#1C1C1E" },
|
||||
"surface"
|
||||
);
|
||||
|
||||
// Không override, dùng color từ theme
|
||||
const textColor = useThemeColor({}, "text");
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
```typescript
|
||||
@@ -165,25 +288,25 @@ const { styles } = useAppTheme();
|
||||
|
||||
// Container styles
|
||||
styles.container; // Flex 1 container với background
|
||||
styles.surface; // Card surface với padding
|
||||
styles.card; // Card với shadow và border radius
|
||||
styles.surface; // Surface với padding 16, borderRadius 12
|
||||
styles.card; // Card với shadow, elevation
|
||||
|
||||
// Button styles
|
||||
styles.primaryButton; // Primary button style
|
||||
styles.primaryButton; // Primary button với colors.primary
|
||||
styles.secondaryButton; // Secondary button với border
|
||||
styles.primaryButtonText; // White text cho primary button
|
||||
styles.secondaryButtonText; // Theme text cho secondary button
|
||||
|
||||
// Input styles
|
||||
styles.textInput; // Text input với border và padding
|
||||
styles.textInput; // Text input với border, padding
|
||||
|
||||
// Status styles
|
||||
styles.successContainer; // Success status container
|
||||
styles.warningContainer; // Warning status container
|
||||
styles.errorContainer; // Error status container
|
||||
styles.successContainer; // Success background với border
|
||||
styles.warningContainer; // Warning background với border
|
||||
styles.errorContainer; // Error background với border
|
||||
|
||||
// Utility
|
||||
styles.separator; // Line separator
|
||||
styles.separator; // 1px line separator
|
||||
```
|
||||
|
||||
## Theme Utilities
|
||||
@@ -191,44 +314,189 @@ styles.separator; // Line separator
|
||||
```typescript
|
||||
const { utils } = useAppTheme();
|
||||
|
||||
utils.isDark; // boolean - kiểm tra dark mode
|
||||
utils.isLight; // boolean - kiểm tra light mode
|
||||
utils.toggleTheme(); // function - toggle giữa light/dark
|
||||
utils.getOpacityColor(colorName, opacity); // Tạo màu với opacity
|
||||
// Check theme
|
||||
utils.isDark; // boolean - true nếu dark mode
|
||||
utils.isLight; // boolean - true nếu light mode
|
||||
|
||||
// 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
|
||||
|
||||
1. **Sử dụng `useAppTheme`** thay vì access colors trực tiếp
|
||||
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
|
||||
1. **Sử dụng hooks đúng context:**
|
||||
|
||||
## 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
|
||||
// Cũ
|
||||
const colorScheme = useColorScheme();
|
||||
const backgroundColor = colorScheme === "dark" ? "#000" : "#fff";
|
||||
// Good ✅
|
||||
<ThemedView>
|
||||
<ThemedText>Hello</ThemedText>
|
||||
</ThemedView>;
|
||||
|
||||
// Mới
|
||||
// Also good ✅
|
||||
const { colors } = useAppTheme();
|
||||
const backgroundColor = colors.background;
|
||||
<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
|
||||
|
||||
1. **Theme không được lưu**: Kiểm tra AsyncStorage permissions
|
||||
2. **Flash khi khởi động**: ThemeProvider sẽ chờ load theme trước khi render
|
||||
3. **Colors không đúng**: Đảm bảo component được wrap trong ThemeProvider
|
||||
### Theme không được lưu
|
||||
|
||||
## Examples
|
||||
- Kiểm tra AsyncStorage permissions
|
||||
- Check logs trong console: `[Theme] Failed to save theme mode:`
|
||||
|
||||
Xem `components/theme-example.tsx` để biết các cách sử dụng theme khác nhau.
|
||||
### 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
|
||||
// Old ❌
|
||||
import { useColorScheme } from "react-native";
|
||||
const colorScheme = useColorScheme();
|
||||
const backgroundColor = colorScheme === "dark" ? "#000" : "#fff";
|
||||
|
||||
// New ✅
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
const { colors } = useAppTheme();
|
||||
const backgroundColor = colors.background;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Old ❌
|
||||
const [theme, setTheme] = useState("light");
|
||||
|
||||
// New ✅
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
const { themeMode, setThemeMode } = useThemeContext();
|
||||
```
|
||||
|
||||
## Debug Logs
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
@@ -5,17 +5,19 @@ import CrewListTable from "@/components/tripInfo/CrewListTable";
|
||||
import FishingToolsTable from "@/components/tripInfo/FishingToolsList";
|
||||
import NetListTable from "@/components/tripInfo/NetListTable";
|
||||
import TripCostTable from "@/components/tripInfo/TripCostTable";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
export default function TripInfoScreen() {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.titleText, { color: colors.text }]}>
|
||||
Thông Tin Chuyến Đi
|
||||
{t("trip.infoTrip")}
|
||||
</Text>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<ButtonCreateNewHaulOrTrip />
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 {
|
||||
@@ -44,6 +45,7 @@ export default function LoginScreen() {
|
||||
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
|
||||
const { t, setLocale, locale } = useI18n();
|
||||
const { colors, colorScheme } = useTheme();
|
||||
const { setThemeMode } = useThemeContext();
|
||||
const styles = useMemo(
|
||||
() => createStyles(colors, colorScheme),
|
||||
[colors, colorScheme]
|
||||
@@ -312,6 +314,10 @@ export default function LoginScreen() {
|
||||
inactiveBackgroundColor={colors.surface}
|
||||
inactiveOverlayColor={colors.textSecondary}
|
||||
activeOverlayColor={colors.background}
|
||||
value={colorScheme === "light"}
|
||||
onChange={(val) => {
|
||||
setThemeMode(val ? "light" : "dark");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
@@ -93,6 +94,12 @@ const Select: React.FC<SelectProps> = ({
|
||||
|
||||
const sz = sizeMap[size];
|
||||
|
||||
// Theme colors from context (consistent with other components)
|
||||
const { colors } = useThemeContext();
|
||||
const selectBackgroundColor = disabled
|
||||
? colors.backgroundSecondary
|
||||
: colors.surface;
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<TouchableOpacity
|
||||
@@ -101,7 +108,8 @@ const Select: React.FC<SelectProps> = ({
|
||||
{
|
||||
height: sz.height,
|
||||
paddingHorizontal: sz.paddingHorizontal,
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
backgroundColor: selectBackgroundColor,
|
||||
borderColor: disabled ? colors.border : colors.primary,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
@@ -112,14 +120,18 @@ const Select: React.FC<SelectProps> = ({
|
||||
>
|
||||
<View style={styles.content}>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color="#4ecdc4" />
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
{
|
||||
fontSize: sz.fontSize,
|
||||
color: selectedValue ? "#111" : "#999",
|
||||
color: disabled
|
||||
? colors.textSecondary
|
||||
: selectedValue
|
||||
? colors.text
|
||||
: colors.textSecondary,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
@@ -131,24 +143,41 @@ const Select: React.FC<SelectProps> = ({
|
||||
<View style={styles.suffix}>
|
||||
{allowClear && selectedValue && !loading ? (
|
||||
<TouchableOpacity onPress={handleClear} style={styles.icon}>
|
||||
<AntDesign name="close" size={16} color="#999" />
|
||||
<AntDesign name="close" size={16} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
<AntDesign
|
||||
name={isOpen ? "up" : "down"}
|
||||
size={14}
|
||||
color="#999"
|
||||
color={colors.textSecondary}
|
||||
style={styles.arrow}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{isOpen && (
|
||||
<View style={[styles.dropdown, { top: containerHeight }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.dropdown,
|
||||
{
|
||||
top: containerHeight,
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{showSearch && (
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
style={[
|
||||
styles.searchInput,
|
||||
{
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
},
|
||||
]}
|
||||
placeholder="Search..."
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
autoFocus
|
||||
@@ -160,8 +189,13 @@ const Select: React.FC<SelectProps> = ({
|
||||
key={item.value}
|
||||
style={[
|
||||
styles.option,
|
||||
{
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
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)}
|
||||
disabled={item.disabled}
|
||||
@@ -169,14 +203,22 @@ const Select: React.FC<SelectProps> = ({
|
||||
<Text
|
||||
style={[
|
||||
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}
|
||||
</Text>
|
||||
{selectedValue === item.value && (
|
||||
<AntDesign name="check" size={16} color="#4ecdc4" />
|
||||
<AntDesign name="check" size={16} color={colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -193,9 +235,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
container: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#e6e6e6",
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#fff",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
@@ -204,7 +244,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
text: {
|
||||
color: "#111",
|
||||
// Color is set dynamically via theme
|
||||
},
|
||||
suffix: {
|
||||
flexDirection: "row",
|
||||
@@ -220,9 +260,7 @@ const styles = StyleSheet.create({
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: "#fff",
|
||||
borderWidth: 1,
|
||||
borderColor: "#e6e6e6",
|
||||
borderTopWidth: 0,
|
||||
borderRadius: 10,
|
||||
borderBottomLeftRadius: 8,
|
||||
@@ -236,7 +274,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
searchInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#e6e6e6",
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
margin: 8,
|
||||
@@ -247,7 +284,6 @@ const styles = StyleSheet.create({
|
||||
option: {
|
||||
padding: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
@@ -255,20 +291,11 @@ const styles = StyleSheet.create({
|
||||
optionDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
optionSelected: {
|
||||
backgroundColor: "#f6ffed",
|
||||
},
|
||||
// optionSelected is handled dynamically via inline styles
|
||||
optionText: {
|
||||
fontSize: 16,
|
||||
color: "#111",
|
||||
},
|
||||
optionTextDisabled: {
|
||||
color: "#999",
|
||||
},
|
||||
optionTextSelected: {
|
||||
color: "#4ecdc4",
|
||||
fontWeight: "600",
|
||||
},
|
||||
// optionTextDisabled and optionTextSelected are handled dynamically via inline styles
|
||||
});
|
||||
|
||||
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 {
|
||||
title?: string;
|
||||
@@ -8,10 +10,15 @@ export const Description = ({
|
||||
title = "",
|
||||
description = "",
|
||||
}: DescriptionProps) => {
|
||||
const { colors } = useAppTheme();
|
||||
return (
|
||||
<View className="flex-row gap-2 ">
|
||||
<Text className="opacity-50 text-lg">{title}:</Text>
|
||||
<Text className="text-lg">{description}</Text>
|
||||
<ThemedText
|
||||
style={{ color: colors.textSecondary, fontSize: 16 }}
|
||||
>
|
||||
{title}:
|
||||
</ThemedText>
|
||||
<ThemedText style={{ fontSize: 16 }}>{description}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { convertToDMS, kmhToKnot } from "@/utils/geom";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
@@ -15,6 +16,8 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
||||
const translateY = useRef(new Animated.Value(0)).current;
|
||||
const blockBottom = useRef(new Animated.Value(0)).current;
|
||||
const { t } = useI18n();
|
||||
const { colors, styles } = useAppTheme();
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(translateY, {
|
||||
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",
|
||||
bottom: blockBottom,
|
||||
left: 5,
|
||||
// width: 48,
|
||||
// height: 48,
|
||||
// backgroundColor: "blue",
|
||||
borderRadius: 4,
|
||||
zIndex: 30,
|
||||
}}
|
||||
>
|
||||
<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
|
||||
style={{
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
transform: [{ translateY }],
|
||||
}}
|
||||
className="absolute bottom-0 gap-3 right-0 p-3 left-0 h-auto w-full rounded-t-xl bg-white shadow-md"
|
||||
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)}
|
||||
>
|
||||
{/* Nút toggle ở top-right */}
|
||||
<TouchableOpacity
|
||||
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
|
||||
name={isExpanded ? "close" : "close"}
|
||||
size={20}
|
||||
color="#666"
|
||||
color={colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -120,9 +113,10 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
||||
{!isExpanded && (
|
||||
<TouchableOpacity
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { StyleSheet, Text, TextInput, View } from "react-native";
|
||||
import IconButton from "../IconButton";
|
||||
import Select from "../Select";
|
||||
import Modal from "../ui/modal";
|
||||
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||
|
||||
const SosButton = () => {
|
||||
const [sosData, setSosData] = useState<Model.SosResponse | null>();
|
||||
@@ -22,6 +23,16 @@ const SosButton = () => {
|
||||
const [customMessage, setCustomMessage] = useState("");
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
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 = [
|
||||
...sosMessage.map((msg) => ({
|
||||
ma: msg.ma,
|
||||
@@ -176,7 +187,7 @@ const SosButton = () => {
|
||||
errors.customMessage ? styles.errorInput : {},
|
||||
]}
|
||||
placeholder={t("home.sos.enterStatus")}
|
||||
placeholderTextColor="#999"
|
||||
placeholderTextColor={textColor + '99'} // Add transparency
|
||||
value={customMessage}
|
||||
onChangeText={(text) => {
|
||||
setCustomMessage(text);
|
||||
@@ -201,7 +212,7 @@ const SosButton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const SosButtonStyles = (textColor: string, borderColor: string, errorColor: string, backgroundColor: string) => StyleSheet.create({
|
||||
formGroup: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
@@ -209,26 +220,27 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
marginBottom: 8,
|
||||
color: "#333",
|
||||
color: textColor,
|
||||
},
|
||||
errorBorder: {
|
||||
borderColor: "#ff4444",
|
||||
borderColor: errorColor,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderColor: borderColor,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 14,
|
||||
color: "#333",
|
||||
color: textColor,
|
||||
backgroundColor: backgroundColor,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
errorInput: {
|
||||
borderColor: "#ff4444",
|
||||
borderColor: errorColor,
|
||||
},
|
||||
errorText: {
|
||||
color: "#ff4444",
|
||||
color: errorColor,
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { queryGpsData } from "@/controller/DeviceController";
|
||||
import { queryUpdateFishingLogs } from "@/controller/TripController";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { showErrorToast, showSuccessToast } from "@/services/toast_service";
|
||||
import { useFishes } from "@/state/use-fish";
|
||||
import { useTrip } from "@/state/use-trip";
|
||||
@@ -20,8 +21,8 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { z } from "zod";
|
||||
import { InfoSection } from "./NetDetailModal/components";
|
||||
import styles from "./style/CreateOrUpdateHaulModal.styles";
|
||||
import { InfoSection } from "./components/InfoSection";
|
||||
import { createStyles } from "./style/CreateOrUpdateHaulModal.styles";
|
||||
|
||||
interface CreateOrUpdateHaulModalProps {
|
||||
isVisible: boolean;
|
||||
@@ -74,6 +75,8 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
||||
fishingLog,
|
||||
fishingLogIndex,
|
||||
}) => {
|
||||
const { colors } = useThemeContext();
|
||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||
const { t } = useI18n();
|
||||
const [isCreateMode, setIsCreateMode] = React.useState(!fishingLog?.info);
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
@@ -256,7 +259,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
||||
<TouchableOpacity
|
||||
onPress={() => remove(index)}
|
||||
style={{
|
||||
backgroundColor: "#FF3B30",
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -277,7 +280,7 @@ const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
||||
<TouchableOpacity
|
||||
onPress={() => handleToggleExpanded(index)}
|
||||
style={{
|
||||
backgroundColor: "#007AFF",
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import React from "react";
|
||||
import { Modal, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import styles from "./style/CrewDetailModal.styles";
|
||||
import { createStyles } from "./style/CrewDetailModal.styles";
|
||||
|
||||
// ---------------------------
|
||||
// 🧩 Interface
|
||||
@@ -23,6 +24,8 @@ const CrewDetailModal: React.FC<CrewDetailModalProps> = ({
|
||||
crewData,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||
|
||||
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 { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import styles from "./style/TripCostDetailModal.styles";
|
||||
import { createStyles } from "./style/TripCostDetailModal.styles";
|
||||
|
||||
// ---------------------------
|
||||
// 🧩 Interface
|
||||
@@ -31,6 +32,8 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editableData, setEditableData] = useState<Model.TripCost[]>(data);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import styles from "../../style/NetDetailModal.styles";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
interface InfoSectionProps {
|
||||
fishingLog?: Model.FishingLog;
|
||||
@@ -13,6 +13,9 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
||||
stt,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||
|
||||
if (!fishingLog) {
|
||||
return null;
|
||||
}
|
||||
@@ -42,22 +45,6 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
||||
? 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 (
|
||||
@@ -69,21 +56,12 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
item.value === "Đã hoàn thành"
|
||||
item.value === t("trip.infoSection.statusCompleted")
|
||||
? styles.statusBadgeCompleted
|
||||
: styles.statusBadgeInProgress,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.statusBadgeText,
|
||||
item.value === "Đã hoàn thành"
|
||||
? styles.statusBadgeTextCompleted
|
||||
: styles.statusBadgeTextInProgress,
|
||||
]}
|
||||
>
|
||||
{item.value}
|
||||
</Text>
|
||||
<Text style={styles.statusBadgeText}>{item.value}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.infoValue}>{item.value}</Text>
|
||||
@@ -93,3 +71,49 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
||||
</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,9 +1,11 @@
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
export const createStyles = (colors: typeof Colors.light) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f5f5f5",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
@@ -12,14 +14,14 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 30,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#eee",
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
},
|
||||
headerButtons: {
|
||||
@@ -31,14 +33,14 @@ const styles = StyleSheet.create({
|
||||
padding: 4,
|
||||
},
|
||||
closeIconButton: {
|
||||
backgroundColor: "#FF3B30",
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: "#007bff",
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
@@ -56,7 +58,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 15,
|
||||
},
|
||||
fishCard: {
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surfaceSecondary,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
@@ -75,11 +77,11 @@ const styles = StyleSheet.create({
|
||||
fishCardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
},
|
||||
fishCardSubtitle: {
|
||||
fontSize: 15,
|
||||
color: "#ff6600",
|
||||
color: colors.warning,
|
||||
fontWeight: "500",
|
||||
},
|
||||
fieldGroup: {
|
||||
@@ -88,26 +90,26 @@ const styles = StyleSheet.create({
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#333",
|
||||
color: colors.textSecondary,
|
||||
marginBottom: 6,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
backgroundColor: "#fff",
|
||||
color: "#000",
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.text,
|
||||
},
|
||||
inputDisabled: {
|
||||
backgroundColor: "#f5f5f5",
|
||||
color: "#999",
|
||||
borderColor: "#eee",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
color: colors.textSecondary,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
errorText: {
|
||||
color: "#dc3545",
|
||||
color: colors.error,
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
fontWeight: "500",
|
||||
@@ -119,7 +121,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 12,
|
||||
},
|
||||
removeButton: {
|
||||
backgroundColor: "#dc3545",
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
@@ -132,7 +134,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: "600",
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
@@ -152,12 +154,12 @@ const styles = StyleSheet.create({
|
||||
footerSection: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "#eee",
|
||||
borderTopColor: colors.separator,
|
||||
},
|
||||
saveButtonLarge: {
|
||||
backgroundColor: "#007bff",
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 14,
|
||||
justifyContent: "center",
|
||||
@@ -170,10 +172,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
emptyStateText: {
|
||||
textAlign: "center",
|
||||
color: "#999",
|
||||
color: colors.textSecondary,
|
||||
fontSize: 14,
|
||||
marginTop: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
export const createStyles = (colors: typeof Colors.light) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f5f5f5",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
@@ -12,21 +14,21 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 30,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#eee",
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
},
|
||||
closeIconButton: {
|
||||
backgroundColor: "#FF3B30",
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
@@ -38,7 +40,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 15,
|
||||
},
|
||||
infoCard: {
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 35,
|
||||
@@ -51,19 +53,17 @@ const styles = StyleSheet.create({
|
||||
infoRow: {
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
color: "#666",
|
||||
color: colors.textSecondary,
|
||||
marginBottom: 6,
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 16,
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
export const createStyles = (colors: typeof Colors.light) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f5f5f5",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
@@ -12,21 +14,21 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 30,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#eee",
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
},
|
||||
closeIconButton: {
|
||||
backgroundColor: "#FF3B30",
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
@@ -38,7 +40,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 15,
|
||||
},
|
||||
infoCard: {
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 35,
|
||||
@@ -51,17 +53,17 @@ const styles = StyleSheet.create({
|
||||
infoRow: {
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
color: "#666",
|
||||
color: colors.textSecondary,
|
||||
marginBottom: 6,
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 16,
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
fontWeight: "500",
|
||||
},
|
||||
statusBadge: {
|
||||
@@ -71,20 +73,21 @@ const styles = StyleSheet.create({
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
statusBadgeCompleted: {
|
||||
backgroundColor: "#e8f5e9",
|
||||
backgroundColor: colors.success,
|
||||
},
|
||||
statusBadgeInProgress: {
|
||||
backgroundColor: "#fff3e0",
|
||||
backgroundColor: colors.warning,
|
||||
},
|
||||
statusBadgeText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#fff",
|
||||
},
|
||||
statusBadgeTextCompleted: {
|
||||
color: "#2e7d32",
|
||||
color: "#fff",
|
||||
},
|
||||
statusBadgeTextInProgress: {
|
||||
color: "#f57c00",
|
||||
color: "#fff",
|
||||
},
|
||||
headerButtons: {
|
||||
flexDirection: "row",
|
||||
@@ -95,7 +98,7 @@ const styles = StyleSheet.create({
|
||||
padding: 4,
|
||||
},
|
||||
editIconButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
@@ -106,12 +109,12 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 6,
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: "#007AFF",
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 6,
|
||||
@@ -132,16 +135,16 @@ const styles = StyleSheet.create({
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
},
|
||||
totalCatchText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#007AFF",
|
||||
color: colors.primary,
|
||||
},
|
||||
fishCard: {
|
||||
position: "relative",
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
@@ -162,33 +165,33 @@ const styles = StyleSheet.create({
|
||||
label: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
color: "#666",
|
||||
color: colors.textSecondary,
|
||||
marginBottom: 6,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#007AFF",
|
||||
borderColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 15,
|
||||
color: "#000",
|
||||
backgroundColor: "#fff",
|
||||
color: colors.text,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
selectButton: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#007AFF",
|
||||
borderColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
selectButtonText: {
|
||||
fontSize: 15,
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
},
|
||||
optionsList: {
|
||||
position: "absolute",
|
||||
@@ -196,10 +199,10 @@ const styles = StyleSheet.create({
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: "#007AFF",
|
||||
borderColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
maxHeight: 100,
|
||||
zIndex: 1000,
|
||||
elevation: 5,
|
||||
@@ -212,18 +215,18 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 15,
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
},
|
||||
optionsStatusFishList: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#007AFF",
|
||||
borderColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
maxHeight: 120,
|
||||
zIndex: 1000,
|
||||
elevation: 5,
|
||||
@@ -238,10 +241,10 @@ const styles = StyleSheet.create({
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: "#007AFF",
|
||||
borderColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
maxHeight: 180,
|
||||
zIndex: 1000,
|
||||
elevation: 5,
|
||||
@@ -257,15 +260,15 @@ const styles = StyleSheet.create({
|
||||
fishCardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
},
|
||||
fishCardSubtitle: {
|
||||
fontSize: 15,
|
||||
color: "#ff6600",
|
||||
color: colors.warning,
|
||||
marginTop: 0,
|
||||
},
|
||||
addFishButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
@@ -288,5 +291,3 @@ const styles = StyleSheet.create({
|
||||
color: "#fff",
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
export const createStyles = (colors: typeof Colors.light) =>
|
||||
StyleSheet.create({
|
||||
closeIconButton: {
|
||||
backgroundColor: "#FF3B30",
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
@@ -10,7 +12,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f5f5f5",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
@@ -19,14 +21,14 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 30,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#eee",
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
},
|
||||
headerButtons: {
|
||||
@@ -38,7 +40,7 @@ const styles = StyleSheet.create({
|
||||
padding: 4,
|
||||
},
|
||||
editIconButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
@@ -49,12 +51,12 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 6,
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: "#007AFF",
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 6,
|
||||
@@ -72,7 +74,7 @@ const styles = StyleSheet.create({
|
||||
padding: 16,
|
||||
},
|
||||
itemCard: {
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.surfaceSecondary,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
@@ -92,39 +94,39 @@ const styles = StyleSheet.create({
|
||||
label: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
color: "#666",
|
||||
color: colors.textSecondary,
|
||||
marginBottom: 6,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#007AFF",
|
||||
borderColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 15,
|
||||
color: "#000",
|
||||
backgroundColor: "#fff",
|
||||
color: colors.text,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
inputDisabled: {
|
||||
borderColor: "#ddd",
|
||||
backgroundColor: "#f9f9f9",
|
||||
color: "#666",
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
totalContainer: {
|
||||
backgroundColor: "#fff5e6",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: "#ffd699",
|
||||
borderColor: colors.border,
|
||||
},
|
||||
totalText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
color: "#ff6600",
|
||||
color: colors.warning,
|
||||
},
|
||||
footerTotal: {
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
marginTop: 8,
|
||||
@@ -141,13 +143,11 @@ const styles = StyleSheet.create({
|
||||
footerLabel: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
color: "#007bff",
|
||||
color: colors.primary,
|
||||
},
|
||||
footerAmount: {
|
||||
fontSize: 20,
|
||||
fontWeight: "700",
|
||||
color: "#ff6600",
|
||||
color: colors.warning,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
OpaqueColorValue,
|
||||
@@ -29,7 +29,7 @@ const PRESS_FEEDBACK_DURATION = 120;
|
||||
|
||||
type IoniconName = ComponentProps<typeof Ionicons>["name"];
|
||||
|
||||
type RotateSwitchProps = {
|
||||
type SliceSwitchProps = {
|
||||
size?: SwitchSize;
|
||||
leftIcon?: IoniconName;
|
||||
leftIconColor?: string | OpaqueColorValue | undefined;
|
||||
@@ -42,6 +42,7 @@ type RotateSwitchProps = {
|
||||
activeOverlayColor?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
onChange?: (value: boolean) => void;
|
||||
value?: boolean;
|
||||
};
|
||||
|
||||
const SliceSwitch = ({
|
||||
@@ -57,19 +58,28 @@ const SliceSwitch = ({
|
||||
activeOverlayColor = "#000",
|
||||
style,
|
||||
onChange,
|
||||
}: RotateSwitchProps) => {
|
||||
value,
|
||||
}: SliceSwitchProps) => {
|
||||
const { width: containerWidth, height: containerHeight } =
|
||||
SIZE_PRESETS[size] ?? SIZE_PRESETS.md;
|
||||
const animationDuration = Math.max(duration ?? DEFAULT_TOGGLE_DURATION, 0);
|
||||
const [isOn, setIsOn] = useState(false);
|
||||
const [bgOn, setBgOn] = useState(false);
|
||||
const progress = useRef(new Animated.Value(0)).current;
|
||||
const [isOn, setIsOn] = useState(value ?? false);
|
||||
const [bgOn, setBgOn] = useState(value ?? false);
|
||||
const progress = useRef(new Animated.Value(value ? 1 : 0)).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 handleToggle = () => {
|
||||
const next = !isOn;
|
||||
// Sync with external value prop if provided
|
||||
useEffect(() => {
|
||||
if (value !== undefined && value !== isOn) {
|
||||
animateToValue(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const animateToValue = (next: boolean) => {
|
||||
const targetValue = next ? 1 : 0;
|
||||
const overlayTarget = next ? containerWidth / 2 : 0;
|
||||
|
||||
@@ -81,7 +91,6 @@ const SliceSwitch = ({
|
||||
overlayTranslateX.setValue(overlayTarget);
|
||||
setIsOn(next);
|
||||
setBgOn(next);
|
||||
onChange?.(next);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +109,6 @@ const SliceSwitch = ({
|
||||
}),
|
||||
]).start(() => {
|
||||
setBgOn(next);
|
||||
onChange?.(next);
|
||||
});
|
||||
|
||||
// Remove any previous listener
|
||||
@@ -132,6 +140,14 @@ const SliceSwitch = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
const next = !isOn;
|
||||
if (value === undefined) {
|
||||
animateToValue(next);
|
||||
}
|
||||
onChange?.(next);
|
||||
};
|
||||
|
||||
const handlePressIn = () => {
|
||||
pressScale.stopAnimation();
|
||||
Animated.timing(pressScale, {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Provides styled components and theme utilities
|
||||
*/
|
||||
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { useMemo } from "react";
|
||||
import { StyleSheet, TextStyle, ViewStyle } from "react-native";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
export function useAppTheme() {
|
||||
const { colors, colorScheme, themeMode, setThemeMode, getColor } =
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
}
|
||||
},
|
||||
"trip": {
|
||||
"infoTrip": "Trip Information",
|
||||
"createNewTrip": "Create New Trip",
|
||||
"endTrip": "End Trip",
|
||||
"cancelTrip": "Cancel Trip",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
}
|
||||
},
|
||||
"trip": {
|
||||
"infoTrip": "Thông Tin Chuyến Đi",
|
||||
"createNewTrip": "Tạo chuyến mới",
|
||||
"endTrip": "Kết thúc chuyến",
|
||||
"cancelTrip": "Hủy chuyến",
|
||||
|
||||
Reference in New Issue
Block a user