fix themes modal, Add English to the trip information tab

This commit is contained in:
2025-11-21 18:46:51 +07:00
parent 6975358a7f
commit 554289ee1e
12 changed files with 148 additions and 82 deletions

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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>
)}
</>

View File

@@ -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,
},

View File

@@ -80,7 +80,7 @@ const createStyles = (colors: any) =>
borderRadius: 8,
padding: 12,
marginBottom: 12,
backgroundColor: colors.backgroundSecondary,
backgroundColor: colors.surfaceSecondary,
},
infoRow: {
flexDirection: "row",

View File

@@ -58,7 +58,7 @@ export const createStyles = (colors: typeof Colors.light) =>
marginBottom: 15,
},
fishCard: {
backgroundColor: colors.card,
backgroundColor: colors.surfaceSecondary,
borderRadius: 12,
padding: 16,
marginBottom: 16,
@@ -95,7 +95,7 @@ export const createStyles = (colors: typeof Colors.light) =>
},
input: {
borderWidth: 1,
borderColor: colors.border,
borderColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
@@ -106,7 +106,7 @@ export const createStyles = (colors: typeof Colors.light) =>
inputDisabled: {
backgroundColor: colors.backgroundSecondary,
color: colors.textSecondary,
borderColor: colors.separator,
borderColor: colors.border,
},
errorText: {
color: colors.error,

View File

@@ -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, {

View File

@@ -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 } =

View File

@@ -58,6 +58,7 @@
}
},
"trip": {
"infoTrip": "Trip Information",
"createNewTrip": "Create New Trip",
"endTrip": "End Trip",
"cancelTrip": "Cancel Trip",

View File

@@ -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",