Khởi tạo ban đầu
This commit is contained in:
24
components/map/Description.tsx
Normal file
24
components/map/Description.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { View } from "react-native";
|
||||
|
||||
interface DescriptionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
export const Description = ({
|
||||
title = "",
|
||||
description = "",
|
||||
}: DescriptionProps) => {
|
||||
const { colors } = useAppTheme();
|
||||
return (
|
||||
<View className="flex-row gap-2 ">
|
||||
<ThemedText
|
||||
style={{ color: colors.textSecondary, fontSize: 16 }}
|
||||
>
|
||||
{title}:
|
||||
</ThemedText>
|
||||
<ThemedText style={{ fontSize: 16 }}>{description}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
126
components/map/GPSInfoPanel.tsx
Normal file
126
components/map/GPSInfoPanel.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
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";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Animated, TouchableOpacity, View } from "react-native";
|
||||
import ButtonCreateNewHaulOrTrip from "../ButtonCreateNewHaulOrTrip";
|
||||
import { Description } from "./Description";
|
||||
type GPSInfoPanelProps = {
|
||||
gpsData: Model.GPSResponse | undefined;
|
||||
};
|
||||
|
||||
const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [panelHeight, setPanelHeight] = useState(0);
|
||||
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
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [isExpanded]);
|
||||
|
||||
useEffect(() => {
|
||||
const targetBottom = isExpanded ? panelHeight + 12 : 10;
|
||||
Animated.timing(blockBottom, {
|
||||
toValue: targetBottom,
|
||||
duration: 500,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [isExpanded, panelHeight, blockBottom]);
|
||||
|
||||
const togglePanel = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Khối hình vuông */}
|
||||
<Animated.View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: blockBottom,
|
||||
left: 5,
|
||||
borderRadius: 4,
|
||||
zIndex: 30,
|
||||
}}
|
||||
>
|
||||
<ButtonCreateNewHaulOrTrip gpsData={gpsData} />
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
transform: [{ translateY }],
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 0,
|
||||
},
|
||||
]}
|
||||
className="absolute bottom-0 gap-5 right-0 px-4 pt-12 pb-2 left-0 h-auto w-full rounded-t-xl shadow-md"
|
||||
onLayout={(event) => setPanelHeight(event.nativeEvent.layout.height)}
|
||||
>
|
||||
{/* Nút toggle ở top-right */}
|
||||
<TouchableOpacity
|
||||
onPress={togglePanel}
|
||||
className="absolute top-2 right-2 z-10 rounded-full p-1"
|
||||
style={{ backgroundColor: colors.card }}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={isExpanded ? "close" : "close"}
|
||||
size={20}
|
||||
color={colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View className="flex-row justify-between">
|
||||
<View className="flex-1">
|
||||
<Description
|
||||
title={t("home.latitude")}
|
||||
description={convertToDMS(gpsData?.lat ?? 0, true)}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Description
|
||||
title={t("home.longitude")}
|
||||
description={convertToDMS(gpsData?.lon ?? 0, false)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row justify-between">
|
||||
<View className="flex-1">
|
||||
<Description
|
||||
title={t("home.speed")}
|
||||
description={`${kmhToKnot(gpsData?.s ?? 0).toString()} knot`}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Description
|
||||
title={t("home.heading")}
|
||||
description={`${gpsData?.h ?? 0}°`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* Nút floating để mở lại panel khi thu gọn */}
|
||||
{!isExpanded && (
|
||||
<TouchableOpacity
|
||||
onPress={togglePanel}
|
||||
className="absolute bottom-5 right-2 z-20 rounded-full p-2 shadow-lg"
|
||||
style={{ backgroundColor: colors.card }}
|
||||
>
|
||||
<MaterialIcons name="info-outline" size={24} color={colors.icon} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GPSInfoPanel;
|
||||
148
components/map/PolygonWithLabel.tsx
Normal file
148
components/map/PolygonWithLabel.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { ANDROID_PLATFORM } from "@/constants";
|
||||
import { usePlatform } from "@/hooks/use-platform";
|
||||
import { getPolygonCenter } from "@/utils/polyline";
|
||||
import React, { useRef } from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { MapMarker, Marker, Polygon } from "react-native-maps";
|
||||
|
||||
export interface PolygonWithLabelProps {
|
||||
coordinates: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}[];
|
||||
label?: string;
|
||||
content?: string;
|
||||
fillColor?: string;
|
||||
strokeColor?: string;
|
||||
strokeWidth?: number;
|
||||
zIndex?: number;
|
||||
zoomLevel?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component render Polygon kèm Label/Text ở giữa
|
||||
*/
|
||||
export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
|
||||
coordinates,
|
||||
label,
|
||||
content,
|
||||
fillColor = "rgba(16, 85, 201, 0.6)",
|
||||
strokeColor = "rgba(16, 85, 201, 0.8)",
|
||||
strokeWidth = 2,
|
||||
zIndex = 50,
|
||||
zoomLevel = 10,
|
||||
}) => {
|
||||
if (!coordinates || coordinates.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const platform = usePlatform();
|
||||
const markerRef = useRef<MapMarker>(null);
|
||||
|
||||
const centerPoint = getPolygonCenter(coordinates);
|
||||
|
||||
// Tính font size dựa trên zoom level
|
||||
// Zoom càng thấp (xa ra) thì font size càng nhỏ
|
||||
const calculateFontSize = (baseSize: number) => {
|
||||
const baseZoom = 10;
|
||||
// Giảm scale factor để text không quá to khi zoom out
|
||||
const scaleFactor = Math.pow(2, (zoomLevel - baseZoom) * 0.3);
|
||||
return Math.max(baseSize * scaleFactor, 5); // Tối thiểu 5px
|
||||
};
|
||||
|
||||
const labelFontSize = calculateFontSize(12);
|
||||
const contentFontSize = calculateFontSize(10);
|
||||
// console.log("zoom level: ", zoomLevel);
|
||||
|
||||
const paddingScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.2), 0.5);
|
||||
const minWidthScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.25), 0.9);
|
||||
return (
|
||||
<>
|
||||
<Polygon
|
||||
coordinates={coordinates}
|
||||
fillColor={fillColor}
|
||||
strokeColor={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
{label && (
|
||||
<Marker
|
||||
ref={markerRef}
|
||||
coordinate={centerPoint}
|
||||
zIndex={50}
|
||||
tracksViewChanges={platform === ANDROID_PLATFORM ? false : true}
|
||||
anchor={{ x: 0.5, y: 0.5 }}
|
||||
title={platform === ANDROID_PLATFORM ? label : undefined}
|
||||
description={platform === ANDROID_PLATFORM ? content : undefined}
|
||||
>
|
||||
<View style={styles.markerContainer}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
paddingHorizontal: 5 * paddingScale,
|
||||
paddingVertical: 5 * paddingScale,
|
||||
minWidth: 80,
|
||||
maxWidth: 150 * minWidthScale,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.labelText, { fontSize: labelFontSize }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{content && (
|
||||
<Text
|
||||
style={[
|
||||
styles.contentText,
|
||||
{ fontSize: contentFontSize, marginTop: 2 * paddingScale },
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Marker>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
markerContainer: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
labelContainer: {
|
||||
backgroundColor: "rgba(16, 85, 201, 0.95)",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 18,
|
||||
borderWidth: 2,
|
||||
borderColor: "#fff",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 5,
|
||||
elevation: 8,
|
||||
minWidth: 80,
|
||||
maxWidth: 250,
|
||||
},
|
||||
labelText: {
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.3,
|
||||
textAlign: "center",
|
||||
},
|
||||
contentText: {
|
||||
color: "#fff",
|
||||
fontSize: 11,
|
||||
fontWeight: "600",
|
||||
letterSpacing: 0.2,
|
||||
textAlign: "center",
|
||||
opacity: 0.95,
|
||||
},
|
||||
});
|
||||
112
components/map/PolylineWithLabel.tsx
Normal file
112
components/map/PolylineWithLabel.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { ANDROID_PLATFORM } from "@/constants";
|
||||
import { usePlatform } from "@/hooks/use-platform";
|
||||
import {
|
||||
calculateTotalDistance,
|
||||
getMiddlePointOfPolyline,
|
||||
} from "@/utils/polyline";
|
||||
import React, { useRef } from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { MapMarker, Marker, Polyline } from "react-native-maps";
|
||||
|
||||
export interface PolylineWithLabelProps {
|
||||
coordinates: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}[];
|
||||
label?: string;
|
||||
content?: string;
|
||||
strokeColor?: string;
|
||||
strokeWidth?: number;
|
||||
showDistance?: boolean;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component render Polyline kèm Label/Text ở giữa
|
||||
*/
|
||||
export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
|
||||
coordinates,
|
||||
label,
|
||||
content,
|
||||
strokeColor = "#FF5733",
|
||||
strokeWidth = 4,
|
||||
showDistance = false,
|
||||
zIndex = 50,
|
||||
}) => {
|
||||
if (!coordinates || coordinates.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const middlePoint = getMiddlePointOfPolyline(coordinates);
|
||||
const distance = calculateTotalDistance(coordinates);
|
||||
const platform = usePlatform();
|
||||
const markerRef = useRef<MapMarker>(null);
|
||||
let displayText = label || "";
|
||||
if (showDistance) {
|
||||
displayText += displayText
|
||||
? ` (${distance.toFixed(2)}km)`
|
||||
: `${distance.toFixed(2)}km`;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Polyline
|
||||
coordinates={coordinates}
|
||||
strokeColor={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
{displayText && (
|
||||
<Marker
|
||||
ref={markerRef}
|
||||
coordinate={middlePoint}
|
||||
zIndex={zIndex + 10}
|
||||
tracksViewChanges={platform === ANDROID_PLATFORM ? false : true}
|
||||
anchor={{ x: 0.5, y: 0.5 }}
|
||||
title={platform === ANDROID_PLATFORM ? label : undefined}
|
||||
description={platform === ANDROID_PLATFORM ? content : undefined}
|
||||
>
|
||||
<View style={styles.markerContainer}>
|
||||
<View style={styles.labelContainer}>
|
||||
<Text
|
||||
style={styles.labelText}
|
||||
numberOfLines={2}
|
||||
adjustsFontSizeToFit
|
||||
>
|
||||
{displayText}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Marker>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
markerContainer: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
labelContainer: {
|
||||
backgroundColor: "rgba(255, 87, 51, 0.95)",
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: "#fff",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 5,
|
||||
elevation: 8,
|
||||
minWidth: 80,
|
||||
maxWidth: 180,
|
||||
},
|
||||
labelText: {
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.3,
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
249
components/map/SosButton.tsx
Normal file
249
components/map/SosButton.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import {
|
||||
queryDeleteSos,
|
||||
queryGetSos,
|
||||
querySendSosMessage,
|
||||
} from "@/controller/DeviceController";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { showErrorToast } from "@/services/toast_service";
|
||||
import { sosMessage } from "@/utils/sosUtils";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { useEffect, useState } from "react";
|
||||
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>();
|
||||
const [showConfirmSosDialog, setShowConfirmSosDialog] = useState(false);
|
||||
const [selectedSosMessage, setSelectedSosMessage] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
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,
|
||||
moTa: msg.moTa,
|
||||
label: msg.moTa,
|
||||
value: msg.ma,
|
||||
})),
|
||||
{ ma: 999, moTa: "Khác", label: "Khác", value: 999 },
|
||||
];
|
||||
|
||||
const getSosData = async () => {
|
||||
try {
|
||||
const response = await queryGetSos();
|
||||
// console.log("SoS ResponseL: ", response);
|
||||
|
||||
setSosData(response.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch SOS data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSosData();
|
||||
}, []);
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { [key: string]: string } = {};
|
||||
|
||||
if (selectedSosMessage === 999 && customMessage.trim() === "") {
|
||||
newErrors.customMessage = t("home.sos.statusRequired");
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleConfirmSos = async () => {
|
||||
if (!validateForm()) {
|
||||
console.log("Form chưa validate");
|
||||
return; // Không đóng modal nếu validate fail
|
||||
}
|
||||
|
||||
let messageToSend = "";
|
||||
if (selectedSosMessage === 999) {
|
||||
messageToSend = customMessage.trim();
|
||||
} else {
|
||||
const selectedOption = sosOptions.find(
|
||||
(opt) => opt.ma === selectedSosMessage
|
||||
);
|
||||
messageToSend = selectedOption ? selectedOption.moTa : "";
|
||||
}
|
||||
|
||||
// Gửi dữ liệu đi
|
||||
await sendSosMessage(messageToSend);
|
||||
|
||||
// Đóng modal và reset form sau khi gửi thành công
|
||||
setShowConfirmSosDialog(false);
|
||||
setSelectedSosMessage(null);
|
||||
setCustomMessage("");
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
const handleClickButton = async (isActive: boolean) => {
|
||||
console.log("Is Active: ", isActive);
|
||||
|
||||
if (isActive) {
|
||||
const resp = await queryDeleteSos();
|
||||
if (resp.status === 200) {
|
||||
await getSosData();
|
||||
}
|
||||
} else {
|
||||
setSelectedSosMessage(11); // Mặc định chọn lý do ma: 11
|
||||
setShowConfirmSosDialog(true);
|
||||
}
|
||||
};
|
||||
|
||||
const sendSosMessage = async (message: string) => {
|
||||
try {
|
||||
const resp = await querySendSosMessage(message);
|
||||
if (resp.status === 200) {
|
||||
await getSosData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error when send sos: ", error);
|
||||
showErrorToast(t("home.sos.sendError"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
icon={<MaterialIcons name="warning" size={20} color="white" />}
|
||||
type="danger"
|
||||
size="middle"
|
||||
onPress={() => handleClickButton(sosData?.active || false)}
|
||||
style={{ borderRadius: 20 }}
|
||||
>
|
||||
{sosData?.active ? t("home.sos.active") : t("home.sos.inactive")}
|
||||
</IconButton>
|
||||
<Modal
|
||||
open={showConfirmSosDialog}
|
||||
onCancel={() => {
|
||||
setShowConfirmSosDialog(false);
|
||||
setSelectedSosMessage(null);
|
||||
setCustomMessage("");
|
||||
setErrors({});
|
||||
}}
|
||||
okText={t("home.sos.confirm")}
|
||||
cancelText={t("home.sos.cancel")}
|
||||
title={t("home.sos.title")}
|
||||
centered
|
||||
onOk={handleConfirmSos}
|
||||
>
|
||||
{/* Select Nội dung SOS */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>{t("home.sos.content")}</Text>
|
||||
|
||||
<Select
|
||||
value={selectedSosMessage ?? undefined}
|
||||
options={sosOptions}
|
||||
placeholder={t("home.sos.selectReason")}
|
||||
onChange={(value) => {
|
||||
setSelectedSosMessage(value as number);
|
||||
// Clear custom message nếu chọn khác lý do
|
||||
if (value !== 999) {
|
||||
setCustomMessage("");
|
||||
}
|
||||
// Clear error if exists
|
||||
if (errors.sosMessage) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors.sosMessage;
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
}}
|
||||
showSearch={false}
|
||||
style={[errors.sosMessage ? styles.errorBorder : undefined]}
|
||||
/>
|
||||
{errors.sosMessage && (
|
||||
<Text style={styles.errorText}>{errors.sosMessage}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Input Custom Message nếu chọn "Khác" */}
|
||||
{selectedSosMessage === 999 && (
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>{t("home.sos.statusInput")}</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
errors.customMessage ? styles.errorInput : {},
|
||||
]}
|
||||
placeholder={t("home.sos.enterStatus")}
|
||||
placeholderTextColor={textColor + '99'} // Add transparency
|
||||
value={customMessage}
|
||||
onChangeText={(text) => {
|
||||
setCustomMessage(text);
|
||||
if (text.trim() !== "") {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors.customMessage;
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
}}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
/>
|
||||
{errors.customMessage && (
|
||||
<Text style={styles.errorText}>{errors.customMessage}</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SosButtonStyles = (textColor: string, borderColor: string, errorColor: string, backgroundColor: string) => StyleSheet.create({
|
||||
formGroup: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
marginBottom: 8,
|
||||
color: textColor,
|
||||
},
|
||||
errorBorder: {
|
||||
borderColor: errorColor,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: borderColor,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 14,
|
||||
color: textColor,
|
||||
backgroundColor: backgroundColor,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
errorInput: {
|
||||
borderColor: errorColor,
|
||||
},
|
||||
errorText: {
|
||||
color: errorColor,
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default SosButton;
|
||||
Reference in New Issue
Block a user