thêm tab "Xem chi tiết chuyến đi", "Xem chi tiết thành viên chuyến đi", tái sử dụng lại components modal tripForm
This commit is contained in:
156
components/diary/TripDetailSections/AlertsSection.tsx
Normal file
156
components/diary/TripDetailSections/AlertsSection.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import SectionCard from "./SectionCard";
|
||||
|
||||
interface AlertsSectionProps {
|
||||
alerts?: Model.Alarm[];
|
||||
}
|
||||
|
||||
export default function AlertsSection({ alerts = [] }: AlertsSectionProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const getAlertLevelColor = (level?: number) => {
|
||||
switch (level) {
|
||||
case 0:
|
||||
return { bg: "#FEF3C7", text: "#92400E" }; // Warning - Yellow
|
||||
case 1:
|
||||
return { bg: "#FEE2E2", text: "#991B1B" }; // Error - Red
|
||||
case 2:
|
||||
return { bg: "#DBEAFE", text: "#1E40AF" }; // Info - Blue
|
||||
default:
|
||||
return { bg: "#F3F4F6", text: "#4B5563" }; // Default - Gray
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp?: number) => {
|
||||
if (!timestamp) return "--";
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString("vi-VN");
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("diary.tripDetail.alerts")}
|
||||
icon="warning-outline"
|
||||
count={alerts.length}
|
||||
collapsible
|
||||
defaultExpanded={alerts.length > 0}
|
||||
>
|
||||
{alerts.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="checkmark-circle-outline" size={40} color={colors.success || "#22C55E"} />
|
||||
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.noAlerts")}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.list}>
|
||||
{alerts.map((alert, index) => {
|
||||
const levelColor = getAlertLevelColor(alert.level);
|
||||
return (
|
||||
<View
|
||||
key={alert.id || index}
|
||||
style={[styles.alertItem, { backgroundColor: levelColor.bg }]}
|
||||
>
|
||||
<View style={styles.alertHeader}>
|
||||
<View style={styles.alertInfo}>
|
||||
<Ionicons
|
||||
name={alert.level === 1 ? "alert-circle" : "warning"}
|
||||
size={18}
|
||||
color={levelColor.text}
|
||||
/>
|
||||
<Text style={[styles.alertName, { color: levelColor.text }]}>
|
||||
{alert.name || t("diary.tripDetail.unknownAlert")}
|
||||
</Text>
|
||||
</View>
|
||||
{alert.confirmed && (
|
||||
<View style={[styles.confirmedBadge, { backgroundColor: colors.success || "#22C55E" }]}>
|
||||
<Text style={styles.confirmedText}>{t("diary.tripDetail.confirmed")}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.alertTime, { color: levelColor.text }]}>
|
||||
{formatTime(alert.time)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
emptyContainer: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 24,
|
||||
gap: 8,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
list: {
|
||||
gap: 10,
|
||||
},
|
||||
alertItem: {
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
alertHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 4,
|
||||
},
|
||||
alertInfo: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
},
|
||||
alertName: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
flex: 1,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
alertTime: {
|
||||
fontSize: 12,
|
||||
marginLeft: 26,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
confirmedBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
confirmedText: {
|
||||
color: "#FFFFFF",
|
||||
fontSize: 11,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
135
components/diary/TripDetailSections/BasicInfoSection.tsx
Normal file
135
components/diary/TripDetailSections/BasicInfoSection.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface BasicInfoSectionProps {
|
||||
trip: Model.Trip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays basic trip information like ship, dates, ports
|
||||
*/
|
||||
export default function BasicInfoSection({ trip }: BasicInfoSectionProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const formatDateTime = (dateStr?: string) => {
|
||||
if (!dateStr) return "--";
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString("vi-VN", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const infoItems = [
|
||||
{
|
||||
icon: "boat" as const,
|
||||
label: t("diary.tripDetail.shipId"),
|
||||
value: trip.vms_id || "--",
|
||||
},
|
||||
{
|
||||
icon: "play-circle" as const,
|
||||
label: t("diary.tripDetail.departureTime"),
|
||||
value: formatDateTime(trip.departure_time),
|
||||
},
|
||||
{
|
||||
icon: "stop-circle" as const,
|
||||
label: t("diary.tripDetail.arrivalTime"),
|
||||
value: formatDateTime(trip.arrival_time),
|
||||
},
|
||||
{
|
||||
icon: "location" as const,
|
||||
label: t("diary.tripDetail.departurePort"),
|
||||
value: trip.departure_port_id ? `Cảng #${trip.departure_port_id}` : "--",
|
||||
},
|
||||
{
|
||||
icon: "flag" as const,
|
||||
label: t("diary.tripDetail.arrivalPort"),
|
||||
value: trip.arrival_port_id ? `Cảng #${trip.arrival_port_id}` : "--",
|
||||
},
|
||||
{
|
||||
icon: "map" as const,
|
||||
label: t("diary.tripDetail.fishingGrounds"),
|
||||
value: trip.fishing_ground_codes?.length > 0
|
||||
? trip.fishing_ground_codes.join(", ")
|
||||
: "--",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card, borderColor: colors.separator }]}>
|
||||
<View style={styles.header}>
|
||||
<Ionicons name="information-circle-outline" size={20} color={colors.primary} />
|
||||
<Text style={[styles.title, { color: colors.text }]}>
|
||||
{t("diary.tripDetail.basicInfo")}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
{infoItems.map((item, index) => (
|
||||
<View key={index} style={styles.infoItem}>
|
||||
<View style={styles.infoLabel}>
|
||||
<Ionicons name={item.icon} size={16} color={colors.textSecondary} />
|
||||
<Text style={[styles.infoLabelText, { color: colors.textSecondary }]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.infoValue, { color: colors.text }]}>
|
||||
{item.value}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
marginBottom: 16,
|
||||
overflow: "hidden",
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
gap: 12,
|
||||
},
|
||||
infoItem: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
infoLabel: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
infoLabelText: {
|
||||
fontSize: 13,
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: "500",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
},
|
||||
});
|
||||
356
components/diary/TripDetailSections/FishingLogsSection.tsx
Normal file
356
components/diary/TripDetailSections/FishingLogsSection.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import SectionCard from "./SectionCard";
|
||||
|
||||
interface FishingLogsSectionProps {
|
||||
fishingLogs?: Model.FishingLog[] | null;
|
||||
}
|
||||
|
||||
export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSectionProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const logs = fishingLogs || [];
|
||||
|
||||
const formatDateTime = (date?: Date) => {
|
||||
if (!date) return "--";
|
||||
const d = new Date(date);
|
||||
return d.toLocaleString("vi-VN", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const formatCoord = (lat?: number, lon?: number) => {
|
||||
if (lat === undefined || lon === undefined) return "--";
|
||||
return `${lat.toFixed(4)}°N, ${lon.toFixed(4)}°E`;
|
||||
};
|
||||
|
||||
const getStatusLabel = (status?: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return { label: t("diary.tripDetail.logStatusPending"), color: "#FEF3C7", textColor: "#92400E" };
|
||||
case 1:
|
||||
return { label: t("diary.tripDetail.logStatusActive"), color: "#DBEAFE", textColor: "#1E40AF" };
|
||||
case 2:
|
||||
return { label: t("diary.tripDetail.logStatusCompleted"), color: "#D1FAE5", textColor: "#065F46" };
|
||||
default:
|
||||
return { label: t("diary.tripDetail.logStatusUnknown"), color: "#F3F4F6", textColor: "#4B5563" };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("diary.tripDetail.fishingLogs")}
|
||||
icon="boat-outline"
|
||||
count={logs.length}
|
||||
collapsible
|
||||
defaultExpanded={true}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="fish-outline" size={40} color={colors.textSecondary} />
|
||||
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.noFishingLogs")}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.list}>
|
||||
{logs.map((log, index) => {
|
||||
const status = getStatusLabel(log.status);
|
||||
const catchCount = log.info?.length || 0;
|
||||
|
||||
return (
|
||||
<View
|
||||
key={log.fishing_log_id || index}
|
||||
style={[styles.logItem, { borderColor: colors.separator }]}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.logHeader}>
|
||||
<View style={styles.logIndex}>
|
||||
<Text style={[styles.logIndexText, { color: colors.primary }]}>
|
||||
#{index + 1}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: status.color }]}>
|
||||
<Text style={[styles.statusText, { color: status.textColor }]}>
|
||||
{status.label}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Time Info */}
|
||||
<View style={styles.timeRow}>
|
||||
<View style={styles.timeItem}>
|
||||
<Ionicons name="play-circle-outline" size={16} color={colors.success || "#22C55E"} />
|
||||
<Text style={[styles.timeLabel, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.startTime")}:
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colors.text }]}>
|
||||
{formatDateTime(log.start_at)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.timeItem}>
|
||||
<Ionicons name="stop-circle-outline" size={16} color={colors.error || "#EF4444"} />
|
||||
<Text style={[styles.timeLabel, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.endTime")}:
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colors.text }]}>
|
||||
{formatDateTime(log.end_at)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Location Info */}
|
||||
<View style={[styles.locationContainer, { backgroundColor: colors.backgroundSecondary }]}>
|
||||
<View style={styles.locationItem}>
|
||||
<Ionicons name="location" size={14} color={colors.success || "#22C55E"} />
|
||||
<Text style={[styles.locationLabel, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.startLocation")}:
|
||||
</Text>
|
||||
<Text style={[styles.locationValue, { color: colors.text }]}>
|
||||
{formatCoord(log.start_lat, log.start_lon)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.locationItem}>
|
||||
<Ionicons name="location" size={14} color={colors.error || "#EF4444"} />
|
||||
<Text style={[styles.locationLabel, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.haulLocation")}:
|
||||
</Text>
|
||||
<Text style={[styles.locationValue, { color: colors.text }]}>
|
||||
{formatCoord(log.haul_lat, log.haul_lon)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Catch Info */}
|
||||
{catchCount > 0 && (
|
||||
<View style={styles.catchContainer}>
|
||||
<View style={styles.catchHeader}>
|
||||
<Ionicons name="fish" size={16} color={colors.primary} />
|
||||
<Text style={[styles.catchLabel, { color: colors.text }]}>
|
||||
{t("diary.tripDetail.catchInfo")} ({catchCount} {t("diary.tripDetail.species")})
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.catchList}>
|
||||
{log.info?.slice(0, 3).map((fish, fishIndex) => (
|
||||
<View key={fishIndex} style={styles.catchItem}>
|
||||
<Text style={[styles.fishName, { color: colors.text }]}>
|
||||
{fish.fish_name || t("diary.tripDetail.unknownFish")}
|
||||
</Text>
|
||||
<Text style={[styles.fishAmount, { color: colors.textSecondary }]}>
|
||||
{fish.catch_number} {fish.catch_unit}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{catchCount > 3 && (
|
||||
<Text style={[styles.moreText, { color: colors.primary }]}>
|
||||
+{catchCount - 3} {t("diary.tripDetail.more")}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Weather */}
|
||||
{log.weather_description && (
|
||||
<View style={styles.weatherRow}>
|
||||
<Ionicons name="cloudy-outline" size={14} color={colors.textSecondary} />
|
||||
<Text style={[styles.weatherText, { color: colors.textSecondary }]}>
|
||||
{log.weather_description}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
emptyContainer: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 24,
|
||||
gap: 8,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
list: {
|
||||
gap: 16,
|
||||
},
|
||||
logItem: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
},
|
||||
logHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 10,
|
||||
},
|
||||
logIndex: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
||||
},
|
||||
logIndexText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "700",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
timeRow: {
|
||||
gap: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
timeItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
},
|
||||
timeLabel: {
|
||||
fontSize: 12,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
timeValue: {
|
||||
fontSize: 12,
|
||||
fontWeight: "500",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
locationContainer: {
|
||||
padding: 10,
|
||||
borderRadius: 6,
|
||||
gap: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
locationItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
},
|
||||
locationLabel: {
|
||||
fontSize: 11,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
locationValue: {
|
||||
fontSize: 11,
|
||||
fontWeight: "500",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
catchContainer: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
catchHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
marginBottom: 6,
|
||||
},
|
||||
catchLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
catchList: {
|
||||
gap: 4,
|
||||
paddingLeft: 22,
|
||||
},
|
||||
catchItem: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
fishName: {
|
||||
fontSize: 12,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
fishAmount: {
|
||||
fontSize: 12,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
moreText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "500",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
weatherRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
},
|
||||
weatherText: {
|
||||
fontSize: 12,
|
||||
fontStyle: "italic",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
127
components/diary/TripDetailSections/SectionCard.tsx
Normal file
127
components/diary/TripDetailSections/SectionCard.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface SectionCardProps {
|
||||
title: string;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
children: React.ReactNode;
|
||||
count?: number;
|
||||
collapsible?: boolean;
|
||||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
export default function SectionCard({
|
||||
title,
|
||||
icon,
|
||||
children,
|
||||
count,
|
||||
collapsible = false,
|
||||
defaultExpanded = true,
|
||||
}: SectionCardProps) {
|
||||
const { colors } = useThemeContext();
|
||||
const [expanded, setExpanded] = React.useState(defaultExpanded);
|
||||
|
||||
const themedStyles = {
|
||||
container: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.separator,
|
||||
},
|
||||
title: {
|
||||
color: colors.text,
|
||||
},
|
||||
count: {
|
||||
color: colors.textSecondary,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
icon: colors.primary,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, themedStyles.container]}>
|
||||
<TouchableOpacity
|
||||
style={styles.header}
|
||||
onPress={() => collapsible && setExpanded(!expanded)}
|
||||
activeOpacity={collapsible ? 0.7 : 1}
|
||||
disabled={!collapsible}
|
||||
>
|
||||
<View style={styles.headerLeft}>
|
||||
<Ionicons name={icon} size={20} color={themedStyles.icon} />
|
||||
<Text style={[styles.title, themedStyles.title]}>{title}</Text>
|
||||
{count !== undefined && (
|
||||
<View style={[styles.countBadge, themedStyles.count]}>
|
||||
<Text style={[styles.countText, { color: colors.textSecondary }]}>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{collapsible && (
|
||||
<Ionicons
|
||||
name={expanded ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{expanded && <View style={styles.content}>{children}</View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
marginBottom: 16,
|
||||
overflow: "hidden",
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
countBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
minWidth: 24,
|
||||
alignItems: "center",
|
||||
},
|
||||
countText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
});
|
||||
4
components/diary/TripDetailSections/index.ts
Normal file
4
components/diary/TripDetailSections/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as SectionCard } from "./SectionCard";
|
||||
export { default as AlertsSection } from "./AlertsSection";
|
||||
export { default as FishingLogsSection } from "./FishingLogsSection";
|
||||
export { default as BasicInfoSection } from "./BasicInfoSection";
|
||||
Reference in New Issue
Block a user