cập nhật modal CRUD thuyền viên trong trip
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -12,10 +12,23 @@ import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Alert,
|
||||
Image,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import {
|
||||
searchCrew,
|
||||
newTripCrew,
|
||||
updateTripCrew,
|
||||
} from "@/controller/TripCrewController";
|
||||
import {
|
||||
newCrew,
|
||||
updateCrewInfo,
|
||||
queryCrewImage,
|
||||
} from "@/controller/CrewController";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
interface CrewFormData {
|
||||
personalId: string;
|
||||
@@ -33,9 +46,12 @@ interface AddEditCrewModalProps {
|
||||
onSave: (data: CrewFormData) => Promise<void>;
|
||||
mode: "add" | "edit";
|
||||
initialData?: Partial<CrewFormData>;
|
||||
tripId?: string; // Required for add mode to add crew to trip
|
||||
existingCrewIds?: string[]; // List of existing crew IDs in trip
|
||||
}
|
||||
|
||||
const ROLES = ["captain", "crew", "engineer", "cook"];
|
||||
const ROLES = ["captain", "crew"];
|
||||
const DEBOUNCE_DELAY = 1000; // 3 seconds debounce
|
||||
|
||||
export default function AddEditCrewModal({
|
||||
visible,
|
||||
@@ -43,13 +59,17 @@ export default function AddEditCrewModal({
|
||||
onSave,
|
||||
mode,
|
||||
initialData,
|
||||
tripId,
|
||||
existingCrewIds = [],
|
||||
}: AddEditCrewModalProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
// Animation values
|
||||
const fadeAnim = useState(new Animated.Value(0))[0];
|
||||
const slideAnim = useState(new Animated.Value(Dimensions.get("window").height))[0];
|
||||
const slideAnim = useState(
|
||||
new Animated.Value(Dimensions.get("window").height)
|
||||
)[0];
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState<CrewFormData>({
|
||||
@@ -62,6 +82,21 @@ export default function AddEditCrewModal({
|
||||
note: "",
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isWaitingDebounce, setIsWaitingDebounce] = useState(false);
|
||||
const [searchStatus, setSearchStatus] = useState<
|
||||
"idle" | "found" | "not_found" | "error"
|
||||
>("idle");
|
||||
const [foundPersonData, setFoundPersonData] =
|
||||
useState<Model.TripCrewPerson | null>(null);
|
||||
|
||||
// State quản lý ảnh thuyền viên
|
||||
const [imageUri, setImageUri] = useState<string | null>(null);
|
||||
const [newImageUri, setNewImageUri] = useState<string | null>(null);
|
||||
const [isLoadingImage, setIsLoadingImage] = useState(false);
|
||||
|
||||
// Debounce timer ref
|
||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Pre-fill form when editing
|
||||
useEffect(() => {
|
||||
@@ -75,6 +110,11 @@ export default function AddEditCrewModal({
|
||||
role: initialData.role || "crew",
|
||||
note: initialData.note || "",
|
||||
});
|
||||
setSearchStatus("idle");
|
||||
setFoundPersonData(null);
|
||||
// Reset ảnh khi mở modal edit
|
||||
setNewImageUri(null);
|
||||
setImageUri(null);
|
||||
} else if (visible && mode === "add") {
|
||||
// Reset form for add mode
|
||||
setFormData({
|
||||
@@ -86,9 +126,153 @@ export default function AddEditCrewModal({
|
||||
role: "crew",
|
||||
note: "",
|
||||
});
|
||||
setSearchStatus("idle");
|
||||
setFoundPersonData(null);
|
||||
// Reset ảnh
|
||||
setNewImageUri(null);
|
||||
setImageUri(null);
|
||||
}
|
||||
}, [visible, initialData, mode]);
|
||||
|
||||
// Load ảnh thuyền viên khi edit
|
||||
useEffect(() => {
|
||||
const loadCrewImage = async () => {
|
||||
if (visible && mode === "edit" && initialData?.personalId) {
|
||||
setIsLoadingImage(true);
|
||||
try {
|
||||
const response = await queryCrewImage(initialData.personalId);
|
||||
if (response.data) {
|
||||
const base64 = Buffer.from(response.data as ArrayBuffer).toString(
|
||||
"base64"
|
||||
);
|
||||
setImageUri(`data:image/jpeg;base64,${base64}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Không có ảnh hoặc lỗi - hiển thị placeholder
|
||||
setImageUri(null);
|
||||
} finally {
|
||||
setIsLoadingImage(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
loadCrewImage();
|
||||
}, [visible, mode, initialData?.personalId]);
|
||||
|
||||
// Cleanup debounce timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Search crew by personal ID with debounce
|
||||
const searchCrewByPersonalId = useCallback(async (personalId: string) => {
|
||||
if (!personalId.trim() || personalId.length < 5) {
|
||||
setSearchStatus("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
setSearchStatus("idle");
|
||||
|
||||
try {
|
||||
const response = await searchCrew(personalId.trim());
|
||||
if (response.data) {
|
||||
const person = response.data;
|
||||
setFoundPersonData(person);
|
||||
setSearchStatus("found");
|
||||
|
||||
// Auto-fill form with found data
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
name: person.name || prev.name,
|
||||
phone: person.phone || prev.phone,
|
||||
email: person.email || prev.email,
|
||||
address: person.address || prev.address,
|
||||
note: person.note || prev.note,
|
||||
}));
|
||||
} else {
|
||||
setSearchStatus("not_found");
|
||||
setFoundPersonData(null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 or 401 means not found or unauthorized - treat as not found silently
|
||||
const status = error?.response?.status;
|
||||
if (status === 404 || status === 401) {
|
||||
setSearchStatus("not_found");
|
||||
} else {
|
||||
// For other errors, just set to not_found without logging to console
|
||||
setSearchStatus("not_found");
|
||||
}
|
||||
setFoundPersonData(null);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle personal ID change with debounce
|
||||
const handlePersonalIdChange = (value: string) => {
|
||||
// If crew was previously found and user is changing personal ID, reset form
|
||||
if (foundPersonData && value !== formData.personalId) {
|
||||
setFormData({
|
||||
personalId: value,
|
||||
name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
role: "crew",
|
||||
note: "",
|
||||
});
|
||||
setFoundPersonData(null);
|
||||
} else {
|
||||
setFormData((prev) => ({ ...prev, personalId: value }));
|
||||
}
|
||||
|
||||
setSearchStatus("idle");
|
||||
setIsWaitingDebounce(false);
|
||||
|
||||
// Clear previous timer
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Only search in add mode and when value is long enough
|
||||
if (mode === "add" && value.trim().length >= 5) {
|
||||
// Show waiting indicator
|
||||
setIsWaitingDebounce(true);
|
||||
|
||||
// Set new timer
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
setIsWaitingDebounce(false);
|
||||
searchCrewByPersonalId(value);
|
||||
}, DEBOUNCE_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
// Chọn ảnh từ thư viện
|
||||
const pickImage = async () => {
|
||||
// Xin quyền truy cập thư viện ảnh
|
||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (status !== "granted") {
|
||||
Alert.alert(t("common.error"), "Cần cấp quyền truy cập thư viện ảnh");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mở image picker
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ["images"],
|
||||
allowsEditing: true,
|
||||
aspect: [1, 1],
|
||||
quality: 0.8,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
setNewImageUri(result.assets[0].uri);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle animation
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
@@ -108,6 +292,11 @@ export default function AddEditCrewModal({
|
||||
}, [visible, fadeAnim, slideAnim]);
|
||||
|
||||
const handleClose = () => {
|
||||
// Clear debounce timer
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
@@ -125,7 +314,7 @@ export default function AddEditCrewModal({
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validate required fields
|
||||
// === VALIDATE DỮ LIỆU ===
|
||||
if (!formData.personalId.trim()) {
|
||||
Alert.alert(t("common.error"), t("diary.crew.form.personalIdRequired"));
|
||||
return;
|
||||
@@ -135,11 +324,65 @@ export default function AddEditCrewModal({
|
||||
return;
|
||||
}
|
||||
|
||||
// Kiểm tra thuyền viên đã có trong chuyến đi chưa (chỉ với mode add)
|
||||
if (
|
||||
mode === "add" &&
|
||||
existingCrewIds.includes(formData.personalId.trim())
|
||||
) {
|
||||
Alert.alert(t("common.error"), t("diary.crew.form.crewAlreadyExists"));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (mode === "add" && tripId) {
|
||||
// === CHẾ ĐỘ THÊM MỚI ===
|
||||
|
||||
// Bước 1: Nếu không tìm thấy thuyền viên trong hệ thống -> tạo mới
|
||||
if (searchStatus === "not_found" || !foundPersonData) {
|
||||
await newCrew({
|
||||
personal_id: formData.personalId.trim(),
|
||||
name: formData.name.trim(),
|
||||
phone: formData.phone || "",
|
||||
email: formData.email || "",
|
||||
birth_date: new Date(),
|
||||
note: formData.note || "",
|
||||
address: formData.address || "",
|
||||
});
|
||||
}
|
||||
|
||||
// Bước 2: Thêm thuyền viên vào chuyến đi với role
|
||||
await newTripCrew({
|
||||
trip_id: tripId,
|
||||
personal_id: formData.personalId.trim(),
|
||||
role: formData.role as "captain" | "crew",
|
||||
});
|
||||
} else if (mode === "edit" && tripId) {
|
||||
// === CHẾ ĐỘ CHỈNH SỬA ===
|
||||
|
||||
// Bước 1: Cập nhật thông tin cá nhân của thuyền viên (không bao gồm note)
|
||||
await updateCrewInfo(formData.personalId.trim(), {
|
||||
name: formData.name.trim(),
|
||||
phone: formData.phone || "",
|
||||
email: formData.email || "",
|
||||
birth_date: new Date(),
|
||||
address: formData.address || "",
|
||||
});
|
||||
|
||||
// Bước 2: Cập nhật role và note của thuyền viên trong chuyến đi
|
||||
await updateTripCrew({
|
||||
trip_id: tripId,
|
||||
personal_id: formData.personalId.trim(),
|
||||
role: formData.role as "captain" | "crew",
|
||||
note: formData.note || "",
|
||||
});
|
||||
}
|
||||
|
||||
// Gọi callback để reload danh sách
|
||||
await onSave(formData);
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("Lỗi khi lưu thuyền viên:", error);
|
||||
Alert.alert(t("common.error"), t("diary.crew.form.saveError"));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -150,6 +393,65 @@ export default function AddEditCrewModal({
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// Render search status message
|
||||
const renderSearchStatus = () => {
|
||||
if (isSearching) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.statusContainer,
|
||||
{ backgroundColor: colors.backgroundSecondary },
|
||||
]}
|
||||
>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text style={[styles.statusText, { color: colors.textSecondary }]}>
|
||||
{t("diary.crew.form.searching")}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (searchStatus === "found") {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.statusContainer,
|
||||
styles.statusSuccess,
|
||||
{ backgroundColor: colors.success + "15" },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="checkmark-circle" size={18} color={colors.success} />
|
||||
<Text style={[styles.statusText, { color: colors.success }]}>
|
||||
{t("diary.crew.form.crewFound")}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (searchStatus === "not_found") {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.statusContainer,
|
||||
styles.statusWarning,
|
||||
{ backgroundColor: colors.warning + "15" },
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name="information-circle"
|
||||
size={18}
|
||||
color={colors.warning}
|
||||
/>
|
||||
<Text style={[styles.statusText, { color: colors.warning }]}>
|
||||
{t("diary.crew.form.crewNotFound")}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
modalContainer: { backgroundColor: colors.card },
|
||||
header: { borderBottomColor: colors.separator },
|
||||
@@ -194,26 +496,96 @@ export default function AddEditCrewModal({
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, themedStyles.title]}>
|
||||
{mode === "add" ? t("diary.crew.form.addTitle") : t("diary.crew.form.editTitle")}
|
||||
{mode === "add"
|
||||
? t("diary.crew.form.addTitle")
|
||||
: t("diary.crew.form.editTitle")}
|
||||
</Text>
|
||||
<View style={styles.headerPlaceholder} />
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Ảnh thuyền viên */}
|
||||
<View style={styles.photoSection}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.avatarContainer,
|
||||
{ borderColor: colors.separator },
|
||||
]}
|
||||
onPress={pickImage}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isLoadingImage ? (
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
) : newImageUri || imageUri ? (
|
||||
<Image
|
||||
source={{ uri: newImageUri || imageUri || "" }}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
styles.avatarPlaceholder,
|
||||
{ backgroundColor: colors.backgroundSecondary },
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name="person"
|
||||
size={50}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.photoHint, { color: colors.textSecondary }]}>
|
||||
Nhấn để chọn ảnh
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Personal ID */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.crew.personalId")} *
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, themedStyles.input]}
|
||||
value={formData.personalId}
|
||||
onChangeText={(v) => updateField("personalId", v)}
|
||||
placeholder={t("diary.crew.form.personalIdPlaceholder")}
|
||||
placeholderTextColor={themedStyles.placeholder.color}
|
||||
editable={mode === "add"}
|
||||
/>
|
||||
<View style={styles.inputWithIcon}>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
styles.inputWithIconInput,
|
||||
themedStyles.input,
|
||||
]}
|
||||
value={formData.personalId}
|
||||
onChangeText={handlePersonalIdChange}
|
||||
placeholder={t("diary.crew.form.personalIdPlaceholder")}
|
||||
placeholderTextColor={themedStyles.placeholder.color}
|
||||
editable={mode === "add"}
|
||||
/>
|
||||
{/* Show waiting icon during debounce, spinner during search */}
|
||||
{(isWaitingDebounce || isSearching) && (
|
||||
<View style={styles.inputIcon}>
|
||||
{isSearching ? (
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
) : (
|
||||
<Ionicons
|
||||
name="time-outline"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{mode === "add" && (
|
||||
<Text style={[styles.hint, { color: colors.textSecondary }]}>
|
||||
{isWaitingDebounce
|
||||
? t("diary.crew.form.waitingSearch")
|
||||
: t("diary.crew.form.searchHint")}
|
||||
</Text>
|
||||
)}
|
||||
{renderSearchStatus()}
|
||||
</View>
|
||||
|
||||
{/* Name */}
|
||||
@@ -309,10 +681,18 @@ export default function AddEditCrewModal({
|
||||
{/* Footer */}
|
||||
<View style={[styles.footer, { borderTopColor: colors.separator }]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.cancelButton, { backgroundColor: colors.backgroundSecondary }]}
|
||||
style={[
|
||||
styles.cancelButton,
|
||||
{ backgroundColor: colors.backgroundSecondary },
|
||||
]}
|
||||
onPress={handleClose}
|
||||
>
|
||||
<Text style={[styles.cancelButtonText, { color: colors.textSecondary }]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.cancelButtonText,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -363,7 +743,11 @@ const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
headerPlaceholder: {
|
||||
width: 32,
|
||||
@@ -378,7 +762,11 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
marginBottom: 8,
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
input: {
|
||||
height: 44,
|
||||
@@ -386,7 +774,52 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 14,
|
||||
fontSize: 15,
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
inputWithIcon: {
|
||||
position: "relative",
|
||||
},
|
||||
inputWithIconInput: {
|
||||
paddingRight: 44,
|
||||
},
|
||||
inputIcon: {
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
justifyContent: "center",
|
||||
},
|
||||
hint: {
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
statusContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
},
|
||||
statusSuccess: {},
|
||||
statusWarning: {},
|
||||
statusText: {
|
||||
fontSize: 13,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
textArea: {
|
||||
height: 80,
|
||||
@@ -407,7 +840,11 @@ const styles = StyleSheet.create({
|
||||
roleButtonText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "500",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
@@ -424,7 +861,11 @@ const styles = StyleSheet.create({
|
||||
cancelButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
saveButton: {
|
||||
flex: 1,
|
||||
@@ -436,9 +877,56 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#FFFFFF",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
// Styles cho phần ảnh thuyền viên
|
||||
photoSection: {
|
||||
alignItems: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
avatarContainer: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
borderWidth: 2,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
},
|
||||
avatar: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
},
|
||||
avatarPlaceholder: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
cameraIconOverlay: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 2,
|
||||
borderColor: "#FFFFFF",
|
||||
},
|
||||
photoHint: {
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user