Khởi tạo ban đầu
This commit is contained in:
92
app/(tabs)/_layout.tsx
Normal file
92
app/(tabs)/_layout.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Tabs, useSegments } from "expo-router";
|
||||
|
||||
import { HapticTab } from "@/components/haptic-tab";
|
||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useColorScheme } from "@/hooks/use-theme-context";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const segments = useSegments() as string[];
|
||||
const prev = useRef<string | null>(null);
|
||||
const currentSegment = segments[1] ?? segments[segments.length - 1] ?? null;
|
||||
const { t, locale } = useI18n();
|
||||
useEffect(() => {
|
||||
if (prev.current !== currentSegment) {
|
||||
// console.log("Tab changed ->", { from: prev.current, to: currentSegment });
|
||||
// TODO: xử lý khi chuyển tab ở đây
|
||||
if (prev.current === "(tabs)" && currentSegment !== "(tabs)") {
|
||||
// stopEvents();
|
||||
// console.log("Stop events");
|
||||
} else if (prev.current !== "(tabs)" && currentSegment === "(tabs)") {
|
||||
// we came back into the tabs group — restart polling
|
||||
// startEvents();
|
||||
// console.log("start events");
|
||||
}
|
||||
prev.current = currentSegment;
|
||||
}
|
||||
}, [currentSegment]);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: t("navigation.home"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="map.fill" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tabs.Screen
|
||||
name="tripInfo"
|
||||
options={{
|
||||
title: t("navigation.trip"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="ferry.fill" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="diary"
|
||||
options={{
|
||||
title: t("navigation.diary"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="book.closed.fill" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="sensor"
|
||||
options={{
|
||||
title: t("navigation.sensor"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol
|
||||
size={28}
|
||||
name="dot.radiowaves.left.and.right"
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="setting"
|
||||
options={{
|
||||
title: t("navigation.setting"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="gear" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
47
app/(tabs)/diary.tsx
Normal file
47
app/(tabs)/diary.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import CreateOrUpdateHaulModal from "@/components/tripInfo/modal/CreateOrUpdateHaulModal";
|
||||
import { useState } from "react";
|
||||
import { Button, Platform, StyleSheet, View } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
export default function Warning() {
|
||||
const [isShowModal, setIsShowModal] = useState(false);
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
container: {
|
||||
alignItems: "center",
|
||||
padding: 15,
|
||||
},
|
||||
titleText: {
|
||||
fontSize: 32,
|
||||
fontWeight: "700",
|
||||
lineHeight: 40,
|
||||
marginBottom: 10,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#007AFF",
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 8,
|
||||
marginTop: 20,
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
461
app/(tabs)/index.tsx
Normal file
461
app/(tabs)/index.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import type { PolygonWithLabelProps } from "@/components/map/PolygonWithLabel";
|
||||
import type { PolylineWithLabelProps } from "@/components/map/PolylineWithLabel";
|
||||
import { IOS_PLATFORM, LIGHT_THEME } from "@/constants";
|
||||
import { usePlatform } from "@/hooks/use-platform";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { useRef, useState } from "react";
|
||||
import { Animated, StyleSheet, View } from "react-native";
|
||||
import MapView from "react-native-maps";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const [gpsData, setGpsData] = useState<Model.GPSResponse | null>(null);
|
||||
const [alarmData, setAlarmData] = useState<Model.AlarmResponse | null>(null);
|
||||
const [entityData, setEntityData] = useState<
|
||||
Model.TransformedEntity[] | null
|
||||
>(null);
|
||||
const [banzoneData, setBanzoneData] = useState<Model.Zone[] | null>(null);
|
||||
const [trackPointsData, setTrackPointsData] = useState<
|
||||
Model.ShipTrackPoint[] | null
|
||||
>(null);
|
||||
const [circleRadius, setCircleRadius] = useState(100);
|
||||
const [zoomLevel, setZoomLevel] = useState(10);
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||
const [polylineCoordinates, setPolylineCoordinates] = useState<
|
||||
PolylineWithLabelProps[]
|
||||
>([]);
|
||||
const [polygonCoordinates, setPolygonCoordinates] = useState<
|
||||
PolygonWithLabelProps[]
|
||||
>([]);
|
||||
const platform = usePlatform();
|
||||
const theme = useThemeContext().colorScheme;
|
||||
const scale = useRef(new Animated.Value(0)).current;
|
||||
const opacity = useRef(new Animated.Value(1)).current;
|
||||
|
||||
// useEffect(() => {
|
||||
// getGpsEventBus();
|
||||
// getAlarmEventBus();
|
||||
// getEntitiesEventBus();
|
||||
// getBanzonesEventBus();
|
||||
// getTrackPointsEventBus();
|
||||
// const queryGpsData = (gpsData: Model.GPSResponse) => {
|
||||
// if (gpsData) {
|
||||
// // console.log("GPS Data: ", gpsData);
|
||||
// setGpsData(gpsData);
|
||||
// } else {
|
||||
// setGpsData(null);
|
||||
// setPolygonCoordinates([]);
|
||||
// setPolylineCoordinates([]);
|
||||
// }
|
||||
// };
|
||||
// const queryAlarmData = (alarmData: Model.AlarmResponse) => {
|
||||
// // console.log("Alarm Data: ", alarmData.alarms.length);
|
||||
// setAlarmData(alarmData);
|
||||
// };
|
||||
// const queryEntityData = (entityData: Model.TransformedEntity[]) => {
|
||||
// // console.log("Entities Length Data: ", entityData.length);
|
||||
// setEntityData(entityData);
|
||||
// };
|
||||
// const queryBanzonesData = (banzoneData: Model.Zone[]) => {
|
||||
// // console.log("Banzone Data: ", banzoneData.length);
|
||||
|
||||
// setBanzoneData(banzoneData);
|
||||
// };
|
||||
// const queryTrackPointsData = (TrackPointsData: Model.ShipTrackPoint[]) => {
|
||||
// // console.log("TrackPoints Data: ", TrackPointsData.length);
|
||||
// if (TrackPointsData && TrackPointsData.length > 0) {
|
||||
// setTrackPointsData(TrackPointsData);
|
||||
// } else {
|
||||
// setTrackPointsData([]);
|
||||
// }
|
||||
// };
|
||||
|
||||
// eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
||||
// // console.log("Registering event handlers in HomeScreen");
|
||||
// eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
||||
// // console.log("Subscribed to EVENT_GPS_DATA");
|
||||
// eventBus.on(EVENT_ALARM_DATA, queryAlarmData);
|
||||
// // console.log("Subscribed to EVENT_ALARM_DATA");
|
||||
// eventBus.on(EVENT_ENTITY_DATA, queryEntityData);
|
||||
// // console.log("Subscribed to EVENT_ENTITY_DATA");
|
||||
// eventBus.on(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
||||
// // console.log("Subscribed to EVENT_TRACK_POINTS_DATA");
|
||||
// eventBus.once(EVENT_BANZONE_DATA, queryBanzonesData);
|
||||
// // console.log("Subscribed once to EVENT_BANZONE_DATA");
|
||||
|
||||
// return () => {
|
||||
// // console.log("Unregistering event handlers in HomeScreen");
|
||||
// eventBus.off(EVENT_GPS_DATA, queryGpsData);
|
||||
// // console.log("Unsubscribed EVENT_GPS_DATA");
|
||||
// eventBus.off(EVENT_ALARM_DATA, queryAlarmData);
|
||||
// // console.log("Unsubscribed EVENT_ALARM_DATA");
|
||||
// eventBus.off(EVENT_ENTITY_DATA, queryEntityData);
|
||||
// // console.log("Unsubscribed EVENT_ENTITY_DATA");
|
||||
// eventBus.off(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
||||
// // console.log("Unsubscribed EVENT_TRACK_POINTS_DATA");
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// setPolylineCoordinates([]);
|
||||
// setPolygonCoordinates([]);
|
||||
// if (!entityData) return;
|
||||
// if (!banzoneData) return;
|
||||
// for (const entity of entityData) {
|
||||
// if (entity.id !== ENTITY.ZONE_ALARM_LIST) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// let zones: any[] = [];
|
||||
// try {
|
||||
// zones = entity.valueString ? JSON.parse(entity.valueString) : [];
|
||||
// } catch (parseError) {
|
||||
// console.error("Error parsing zone list:", parseError);
|
||||
// continue;
|
||||
// }
|
||||
// // Nếu danh sách zone rỗng, clear tất cả
|
||||
// if (zones.length === 0) {
|
||||
// setPolylineCoordinates([]);
|
||||
// setPolygonCoordinates([]);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let polylines: PolylineWithLabelProps[] = [];
|
||||
// let polygons: PolygonWithLabelProps[] = [];
|
||||
|
||||
// for (const zone of zones) {
|
||||
// // console.log("Zone Data: ", zone);
|
||||
// const geom = banzoneData.find((b) => b.id === zone.zone_id);
|
||||
// if (!geom) {
|
||||
// continue;
|
||||
// }
|
||||
// const { geom_type, geom_lines, geom_poly } = geom.geom || {};
|
||||
// if (typeof geom_type !== "number") {
|
||||
// continue;
|
||||
// }
|
||||
// if (geom_type === 2) {
|
||||
// // if(oldEntityData.find(e => e.id === ))
|
||||
// // foundPolyline = true;
|
||||
// const coordinates = convertWKTLineStringToLatLngArray(
|
||||
// geom_lines || ""
|
||||
// );
|
||||
// if (coordinates.length > 0) {
|
||||
// polylines.push({
|
||||
// coordinates: coordinates.map((coord) => ({
|
||||
// latitude: coord[0],
|
||||
// longitude: coord[1],
|
||||
// })),
|
||||
// label: zone?.zone_name ?? "",
|
||||
// content: zone?.message ?? "",
|
||||
// });
|
||||
// } else {
|
||||
// console.log("Không tìm thấy polyline trong alarm");
|
||||
// }
|
||||
// } else if (geom_type === 1) {
|
||||
// // foundPolygon = true;
|
||||
// const coordinates = convertWKTtoLatLngString(geom_poly || "");
|
||||
// if (coordinates.length > 0) {
|
||||
// // console.log("Polygon Coordinate: ", coordinates);
|
||||
// const zonePolygons = coordinates.map((polygon) => ({
|
||||
// coordinates: polygon.map((coord) => ({
|
||||
// latitude: coord[0],
|
||||
// longitude: coord[1],
|
||||
// })),
|
||||
// label: zone?.zone_name ?? "",
|
||||
// content: zone?.message ?? "",
|
||||
// }));
|
||||
// polygons.push(...zonePolygons);
|
||||
// } else {
|
||||
// console.log("Không tìm thấy polygon trong alarm");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// setPolylineCoordinates(polylines);
|
||||
// setPolygonCoordinates(polygons);
|
||||
// }
|
||||
// }, [banzoneData, entityData]);
|
||||
|
||||
// Hàm tính radius cố định khi zoom change
|
||||
const calculateRadiusFromZoom = (zoom: number) => {
|
||||
const baseZoom = 10;
|
||||
const baseRadius = 100;
|
||||
const zoomDifference = baseZoom - zoom;
|
||||
const calculatedRadius = baseRadius * Math.pow(2, zoomDifference);
|
||||
// console.log("Caculate Radius: ", calculatedRadius);
|
||||
|
||||
return Math.max(calculatedRadius, 50);
|
||||
};
|
||||
|
||||
// Xử lý khi region (zoom) thay đổi
|
||||
const handleRegionChangeComplete = (newRegion: any) => {
|
||||
// Tính zoom level từ latitudeDelta
|
||||
// zoom = log2(360 / (latitudeDelta * 2)) + 8
|
||||
const zoom = Math.round(Math.log2(360 / (newRegion.latitudeDelta * 2)) + 8);
|
||||
const newRadius = calculateRadiusFromZoom(zoom);
|
||||
setCircleRadius(newRadius);
|
||||
setZoomLevel(zoom);
|
||||
// console.log("Zoom level:", zoom, "Circle radius:", newRadius);
|
||||
};
|
||||
|
||||
// Tính toán region để focus vào marker của tàu (chỉ lần đầu tiên)
|
||||
const getMapRegion = () => {
|
||||
if (!isFirstLoad) {
|
||||
// Sau lần đầu, return undefined để không force region
|
||||
return undefined;
|
||||
}
|
||||
if (!gpsData) {
|
||||
return {
|
||||
latitude: 15.70581,
|
||||
longitude: 116.152685,
|
||||
latitudeDelta: 0.05,
|
||||
longitudeDelta: 0.05,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
latitude: gpsData.lat,
|
||||
longitude: gpsData.lon,
|
||||
latitudeDelta: 0.05,
|
||||
longitudeDelta: 0.05,
|
||||
};
|
||||
};
|
||||
|
||||
const handleMapReady = () => {
|
||||
setTimeout(() => {
|
||||
setIsFirstLoad(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// if (alarmData?.level === 3) {
|
||||
// const loop = Animated.loop(
|
||||
// Animated.sequence([
|
||||
// Animated.parallel([
|
||||
// Animated.timing(scale, {
|
||||
// toValue: 3, // nở to 3 lần
|
||||
// duration: 1500,
|
||||
// useNativeDriver: true,
|
||||
// }),
|
||||
// Animated.timing(opacity, {
|
||||
// toValue: 0, // mờ dần
|
||||
// duration: 1500,
|
||||
// useNativeDriver: true,
|
||||
// }),
|
||||
// ]),
|
||||
// Animated.parallel([
|
||||
// Animated.timing(scale, {
|
||||
// toValue: 0,
|
||||
// duration: 0,
|
||||
// useNativeDriver: true,
|
||||
// }),
|
||||
// Animated.timing(opacity, {
|
||||
// toValue: 1,
|
||||
// duration: 0,
|
||||
// useNativeDriver: true,
|
||||
// }),
|
||||
// ]),
|
||||
// ])
|
||||
// );
|
||||
// loop.start();
|
||||
// return () => loop.stop();
|
||||
// }
|
||||
// }, [alarmData?.level, scale, opacity]);
|
||||
|
||||
return (
|
||||
<View
|
||||
// edges={["top"]}
|
||||
style={styles.container}
|
||||
>
|
||||
<MapView
|
||||
onMapReady={handleMapReady}
|
||||
onRegionChangeComplete={handleRegionChangeComplete}
|
||||
style={styles.map}
|
||||
// initialRegion={getMapRegion()}
|
||||
region={getMapRegion()}
|
||||
userInterfaceStyle={theme === LIGHT_THEME ? "light" : "dark"}
|
||||
showsBuildings={false}
|
||||
showsIndoors={false}
|
||||
loadingEnabled={true}
|
||||
mapType={platform === IOS_PLATFORM ? "mutedStandard" : "standard"}
|
||||
rotateEnabled={false}
|
||||
>
|
||||
{/* {trackPointsData &&
|
||||
trackPointsData.length > 0 &&
|
||||
trackPointsData.map((point, index) => {
|
||||
// console.log(`Rendering circle ${index}:`, point);
|
||||
return (
|
||||
<Circle
|
||||
key={`circle-${index}`}
|
||||
center={{
|
||||
latitude: point.lat,
|
||||
longitude: point.lon,
|
||||
}}
|
||||
// zIndex={50}
|
||||
// radius={platform === IOS_PLATFORM ? 200 : 50}
|
||||
radius={circleRadius}
|
||||
strokeColor="rgba(16, 85, 201, 0.7)"
|
||||
fillColor="rgba(16, 85, 201, 0.7)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{polylineCoordinates.length > 0 && (
|
||||
<>
|
||||
{polylineCoordinates.map((polyline, index) => (
|
||||
<PolylineWithLabel
|
||||
key={`polyline-${index}-${gpsData?.lat || 0}-${
|
||||
gpsData?.lon || 0
|
||||
}`}
|
||||
coordinates={polyline.coordinates}
|
||||
label={polyline.label}
|
||||
content={polyline.content}
|
||||
strokeColor="#FF5733"
|
||||
strokeWidth={4}
|
||||
showDistance={false}
|
||||
// zIndex={50}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{polygonCoordinates.length > 0 && (
|
||||
<>
|
||||
{polygonCoordinates.map((polygon, index) => {
|
||||
return (
|
||||
<PolygonWithLabel
|
||||
key={`polygon-${index}-${gpsData?.lat || 0}-${
|
||||
gpsData?.lon || 0
|
||||
}`}
|
||||
coordinates={polygon.coordinates}
|
||||
label={polygon.label}
|
||||
content={polygon.content}
|
||||
fillColor="rgba(16, 85, 201, 0.6)"
|
||||
strokeColor="rgba(16, 85, 201, 0.8)"
|
||||
strokeWidth={2}
|
||||
// zIndex={50}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)} */}
|
||||
{/* {gpsData !== null && (
|
||||
<Marker
|
||||
key={
|
||||
platform === IOS_PLATFORM
|
||||
? `${gpsData.lat}-${gpsData.lon}`
|
||||
: "gps-data"
|
||||
}
|
||||
coordinate={{
|
||||
latitude: gpsData.lat,
|
||||
longitude: gpsData.lon,
|
||||
}}
|
||||
zIndex={20}
|
||||
anchor={
|
||||
platform === IOS_PLATFORM
|
||||
? { x: 0.5, y: 0.5 }
|
||||
: { x: 0.6, y: 0.4 }
|
||||
}
|
||||
tracksViewChanges={platform === IOS_PLATFORM ? true : undefined}
|
||||
>
|
||||
<View className="w-8 h-8 items-center justify-center">
|
||||
<View style={styles.pingContainer}>
|
||||
{alarmData?.level === 3 && (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.pingCircle,
|
||||
{
|
||||
transform: [{ scale }],
|
||||
opacity,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<RNImage
|
||||
source={(() => {
|
||||
const icon = getShipIcon(
|
||||
alarmData?.level || 0,
|
||||
gpsData.fishing
|
||||
);
|
||||
// console.log("Ship icon:", icon, "for level:", alarmData?.level, "fishing:", gpsData.fishing);
|
||||
return typeof icon === "string" ? { uri: icon } : icon;
|
||||
})()}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
transform: [
|
||||
{
|
||||
rotate: `${
|
||||
typeof gpsData.h === "number" && !isNaN(gpsData.h)
|
||||
? gpsData.h
|
||||
: 0
|
||||
}deg`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Marker>
|
||||
)} */}
|
||||
</MapView>
|
||||
|
||||
{/* <View className="absolute top-14 right-2 shadow-md">
|
||||
<SosButton />
|
||||
</View>
|
||||
<GPSInfoPanel gpsData={gpsData!} /> */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
map: {
|
||||
flex: 1,
|
||||
},
|
||||
button: {
|
||||
// display: "none",
|
||||
position: "absolute",
|
||||
top: 50,
|
||||
right: 20,
|
||||
backgroundColor: "#007AFF",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
elevation: 5,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
|
||||
pingContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "visible",
|
||||
},
|
||||
pingCircle: {
|
||||
position: "absolute",
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#ED3F27",
|
||||
},
|
||||
centerDot: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "#0096FF",
|
||||
},
|
||||
});
|
||||
129
app/(tabs)/sensor.tsx
Normal file
129
app/(tabs)/sensor.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import ScanQRCode from "@/components/ScanQRCode";
|
||||
import Select from "@/components/Select";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
export default function Sensor() {
|
||||
const [scanModalVisible, setScanModalVisible] = useState(false);
|
||||
const [scannedData, setScannedData] = useState<string | null>(null);
|
||||
const handleQRCodeScanned = (data: string) => {
|
||||
setScannedData(data);
|
||||
// Alert.alert("QR Code Scanned", `Result: ${data}`);
|
||||
};
|
||||
|
||||
const handleScanPress = () => {
|
||||
setScanModalVisible(true);
|
||||
};
|
||||
const [selectedValue, setSelectedValue] = useState<
|
||||
string | number | undefined
|
||||
>(undefined);
|
||||
|
||||
const options = [
|
||||
{ label: "Apple", value: "apple" },
|
||||
{ label: "Banana", value: "banana" },
|
||||
{ label: "Cherry", value: "cherry", disabled: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.titleText}>Cảm biến trên tàu</Text>
|
||||
<Select
|
||||
style={{ width: "80%", marginBottom: 20 }}
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={(val) => {
|
||||
setSelectedValue(val);
|
||||
console.log("Value: ", val);
|
||||
}}
|
||||
placeholder="Select a fruit"
|
||||
allowClear
|
||||
/>
|
||||
<Pressable style={styles.scanButton} onPress={handleScanPress}>
|
||||
<Text style={styles.scanButtonText}>📱 Scan QR Code</Text>
|
||||
</Pressable>
|
||||
|
||||
{scannedData && (
|
||||
<View style={styles.resultContainer}>
|
||||
<Text style={styles.resultLabel}>Last Scanned:</Text>
|
||||
<Text style={styles.resultText}>{scannedData}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<ScanQRCode
|
||||
visible={scanModalVisible}
|
||||
onClose={() => setScanModalVisible(false)}
|
||||
onScanned={handleQRCodeScanned}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
container: {
|
||||
alignItems: "center",
|
||||
padding: 15,
|
||||
},
|
||||
titleText: {
|
||||
fontSize: 32,
|
||||
fontWeight: "700",
|
||||
lineHeight: 40,
|
||||
marginBottom: 30,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
scanButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 30,
|
||||
borderRadius: 10,
|
||||
marginVertical: 15,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
scanButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
},
|
||||
resultContainer: {
|
||||
marginTop: 30,
|
||||
padding: 15,
|
||||
backgroundColor: "#f0f0f0",
|
||||
borderRadius: 10,
|
||||
minWidth: "80%",
|
||||
},
|
||||
resultLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#666",
|
||||
marginBottom: 8,
|
||||
},
|
||||
resultText: {
|
||||
fontSize: 14,
|
||||
color: "#333",
|
||||
fontFamily: "Menlo",
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
153
app/(tabs)/setting.tsx
Normal file
153
app/(tabs)/setting.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ScrollView, StyleSheet, View } from "react-native";
|
||||
|
||||
import EnIcon from "@/assets/icons/en_icon.png";
|
||||
import VnIcon from "@/assets/icons/vi_icon.png";
|
||||
import RotateSwitch from "@/components/rotate-switch";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { DOMAIN, TOKEN } from "@/constants";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { removeStorageItem } from "@/utils/storage";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
type Todo = {
|
||||
userId: number;
|
||||
id: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
};
|
||||
|
||||
export default function SettingScreen() {
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Todo | null>(null);
|
||||
const { t, locale, setLocale } = useI18n();
|
||||
const { colors } = useAppTheme();
|
||||
const [isEnabled, setIsEnabled] = useState(locale === "vi");
|
||||
|
||||
// Sync isEnabled state khi locale thay đổi
|
||||
useEffect(() => {
|
||||
setIsEnabled(locale === "vi");
|
||||
}, [locale]);
|
||||
|
||||
const toggleSwitch = async () => {
|
||||
const newLocale = isEnabled ? "en" : "vi";
|
||||
await setLocale(newLocale);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
||||
<ThemedView style={styles.container}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
{t("navigation.setting")}
|
||||
</ThemedText>
|
||||
|
||||
{/* Theme Toggle Section */}
|
||||
<ThemeToggle style={styles.themeSection} />
|
||||
|
||||
{/* Language Section */}
|
||||
<View
|
||||
style={[
|
||||
styles.settingItem,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ThemedText type="default">{t("common.language")}</ThemedText>
|
||||
<RotateSwitch
|
||||
initialValue={isEnabled}
|
||||
onChange={toggleSwitch}
|
||||
size="sm"
|
||||
offImage={EnIcon}
|
||||
onImage={VnIcon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Logout Button */}
|
||||
<ThemedView
|
||||
style={[styles.button, { backgroundColor: colors.primary }]}
|
||||
onTouchEnd={async () => {
|
||||
await removeStorageItem(TOKEN);
|
||||
await removeStorageItem(DOMAIN);
|
||||
router.navigate("/auth/login");
|
||||
}}
|
||||
>
|
||||
<ThemedText type="defaultSemiBold" style={styles.buttonText}>
|
||||
{t("auth.logout")}
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
{data && (
|
||||
<ThemedView
|
||||
style={[styles.debugSection, { backgroundColor: colors.surface }]}
|
||||
>
|
||||
<ThemedText type="default">{data.title}</ThemedText>
|
||||
<ThemedText type="default">{data.completed}</ThemedText>
|
||||
<ThemedText type="default">{data.id}</ThemedText>
|
||||
</ThemedView>
|
||||
)}
|
||||
</ScrollView>
|
||||
</ThemedView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 20,
|
||||
gap: 16,
|
||||
},
|
||||
title: {
|
||||
textAlign: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
themeSection: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
},
|
||||
button: {
|
||||
marginTop: 20,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
debugSection: {
|
||||
marginTop: 20,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
gap: 8,
|
||||
},
|
||||
});
|
||||
83
app/(tabs)/tripInfo.tsx
Normal file
83
app/(tabs)/tripInfo.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import ButtonCancelTrip from "@/components/ButtonCancelTrip";
|
||||
import ButtonCreateNewHaulOrTrip from "@/components/ButtonCreateNewHaulOrTrip";
|
||||
import ButtonEndTrip from "@/components/ButtonEndTrip";
|
||||
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 }]}>
|
||||
{t("trip.infoTrip")}
|
||||
</Text>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<ButtonCreateNewHaulOrTrip />
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.container}>
|
||||
<TripCostTable />
|
||||
<FishingToolsTable />
|
||||
<CrewListTable />
|
||||
<NetListTable />
|
||||
<View style={styles.buttonRow}>
|
||||
<ButtonCancelTrip />
|
||||
<ButtonEndTrip />
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
header: {
|
||||
width: "100%",
|
||||
paddingHorizontal: 15,
|
||||
paddingTop: 15,
|
||||
paddingBottom: 10,
|
||||
alignItems: "center",
|
||||
},
|
||||
buttonWrapper: {
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
container: {
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
},
|
||||
titleText: {
|
||||
fontSize: 32,
|
||||
fontWeight: "700",
|
||||
lineHeight: 40,
|
||||
paddingBottom: 10,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
70
app/_layout.tsx
Normal file
70
app/_layout.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
DarkTheme,
|
||||
DefaultTheme,
|
||||
ThemeProvider,
|
||||
} from "@react-navigation/native";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { useEffect } from "react";
|
||||
import "react-native-reanimated";
|
||||
// import Toast from "react-native-toast-message";
|
||||
|
||||
// import { toastConfig } from "@/config";
|
||||
import { toastConfig } from "@/config";
|
||||
import { setRouterInstance } from "@/config/auth";
|
||||
import "@/global.css";
|
||||
import { I18nProvider } from "@/hooks/use-i18n";
|
||||
import { ThemeProvider as AppThemeProvider, useThemeContext } from "@/hooks/use-theme-context";
|
||||
import Toast from "react-native-toast-message";
|
||||
import "../global.css";
|
||||
function AppContent() {
|
||||
const router = useRouter();
|
||||
const { colorScheme } = useThemeContext();
|
||||
console.log("Color Scheme: ", colorScheme);
|
||||
|
||||
useEffect(() => {
|
||||
setRouterInstance(router);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||
<Stack
|
||||
screenOptions={{ headerShown: false }}
|
||||
initialRouteName="auth/login"
|
||||
>
|
||||
<Stack.Screen
|
||||
name="auth/login"
|
||||
options={{
|
||||
title: "Login",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="(tabs)"
|
||||
options={{
|
||||
title: "Home",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="modal"
|
||||
options={{ presentation: "formSheet", title: "Modal" }}
|
||||
/>
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<AppThemeProvider>
|
||||
<AppContent />
|
||||
</AppThemeProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
410
app/auth/login.tsx
Normal file
410
app/auth/login.tsx
Normal file
@@ -0,0 +1,410 @@
|
||||
import EnIcon from "@/assets/icons/en_icon.png";
|
||||
import VnIcon from "@/assets/icons/vi_icon.png";
|
||||
import RotateSwitch from "@/components/rotate-switch";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import SliceSwitch from "@/components/ui/slice-switch";
|
||||
import { TOKEN } from "@/constants";
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { queryLogin } from "@/controller/AuthController";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import {
|
||||
ColorScheme as ThemeColorScheme,
|
||||
useTheme,
|
||||
useThemeContext,
|
||||
} from "@/hooks/use-theme-context";
|
||||
import { showErrorToast } from "@/services/toast_service";
|
||||
import {
|
||||
getStorageItem,
|
||||
removeStorageItem,
|
||||
setStorageItem,
|
||||
} from "@/utils/storage";
|
||||
import { parseJwtToken } from "@/utils/token";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
export default function LoginScreen() {
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { t, setLocale, locale } = useI18n();
|
||||
const { colors, colorScheme } = useTheme();
|
||||
const { setThemeMode } = useThemeContext();
|
||||
const styles = useMemo(
|
||||
() => createStyles(colors, colorScheme),
|
||||
[colors, colorScheme]
|
||||
);
|
||||
const placeholderColor = colors.textSecondary;
|
||||
const buttonTextColor = colorScheme === "dark" ? colors.text : colors.surface;
|
||||
const [isVNLang, setIsVNLang] = useState(false);
|
||||
|
||||
const checkLogin = useCallback(async () => {
|
||||
const token = await getStorageItem(TOKEN);
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseJwtToken(token);
|
||||
|
||||
const { exp } = parsed;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const oneHour = 60 * 60;
|
||||
if (exp - now < oneHour) {
|
||||
await removeStorageItem(TOKEN);
|
||||
} else {
|
||||
console.log("Token còn hạn");
|
||||
|
||||
router.replace("/(tabs)");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVNLang(locale === "vi");
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
checkLogin();
|
||||
}, [checkLogin]);
|
||||
|
||||
const handleLogin = async (creds?: {
|
||||
username: string;
|
||||
password: string;
|
||||
}) => {
|
||||
const user = creds?.username ?? username;
|
||||
const pass = creds?.password ?? password;
|
||||
|
||||
// Validate input
|
||||
if (!user?.trim() || !pass?.trim()) {
|
||||
showErrorToast("Vui lòng nhập tài khoản và mật khẩu");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const body: Model.LoginRequestBody = {
|
||||
guid: "9812739812738213",
|
||||
email: user,
|
||||
password: pass,
|
||||
};
|
||||
|
||||
const response = await queryLogin(body);
|
||||
|
||||
// Nếu thành công, lưu token và chuyển sang (tabs)
|
||||
console.log("Login thành công với data:", response.data);
|
||||
if (response?.data.token) {
|
||||
// Lưu token vào storage nếu cần (thêm logic này sau)
|
||||
console.log("Login Token ");
|
||||
|
||||
await setStorageItem(TOKEN, response.data.token);
|
||||
// console.log("Token:", response.data.token);
|
||||
router.replace("/(tabs)");
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error instanceof Error ? error.message : "Đăng nhập thất bại"
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchLanguage = (isVN: boolean) => {
|
||||
if (isVN) {
|
||||
setLocale("vi");
|
||||
} else {
|
||||
setLocale("en");
|
||||
}
|
||||
setIsVNLang(isVN);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<ScrollView
|
||||
style={{ flex: 1, backgroundColor: colors.background }}
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
>
|
||||
<ThemedView style={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.headerContainer}>
|
||||
{/* Logo */}
|
||||
<Image
|
||||
source={require("@/assets/images/logo.png")}
|
||||
style={styles.logo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
{t("common.app_name")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
{/* Form */}
|
||||
<View style={styles.formContainer}>
|
||||
{/* Username Input */}
|
||||
<View style={styles.inputGroup}>
|
||||
<ThemedText style={styles.label}>{t("auth.username")}</ThemedText>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder={t("auth.username_placeholder")}
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
editable={!loading}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Password Input */}
|
||||
<View style={styles.inputGroup}>
|
||||
<ThemedText style={styles.label}>{t("auth.password")}</ThemedText>
|
||||
<View className="relative">
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder={t("auth.password_placeholder")}
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
editable={!loading}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
{/* Position absolute with top:0 and bottom:0 and justifyContent:center
|
||||
ensures the icon remains vertically centered inside the input */}
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 4,
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? "eye-off" : "eye"}
|
||||
size={22}
|
||||
color={colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Login Button (3/4) + QR Scan (1/4) */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.loginButton,
|
||||
loading && styles.loginButtonDisabled,
|
||||
{ flex: 5, marginTop: 0 },
|
||||
]}
|
||||
onPress={() => handleLogin()}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={buttonTextColor} size="small" />
|
||||
) : (
|
||||
<Text
|
||||
style={[styles.loginButtonText, { color: buttonTextColor }]}
|
||||
>
|
||||
{t("auth.login")}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Language Switcher */}
|
||||
<View style={styles.languageSwitcherContainer}>
|
||||
<RotateSwitch
|
||||
initialValue={isVNLang}
|
||||
onChange={handleSwitchLanguage}
|
||||
size="sm"
|
||||
offImage={EnIcon}
|
||||
onImage={VnIcon}
|
||||
/>
|
||||
<SliceSwitch
|
||||
size="sm"
|
||||
leftIcon="moon"
|
||||
leftIconColor={
|
||||
colorScheme === "dark" ? colors.background : colors.surface
|
||||
}
|
||||
rightIcon="sunny"
|
||||
rightIconColor={
|
||||
colorScheme === "dark" ? colors.warning : "orange"
|
||||
}
|
||||
activeBackgroundColor={colors.text}
|
||||
inactiveBackgroundColor={colors.surface}
|
||||
inactiveOverlayColor={colors.textSecondary}
|
||||
activeOverlayColor={colors.background}
|
||||
value={colorScheme === "light"}
|
||||
onChange={(val) => {
|
||||
setThemeMode(val ? "light" : "dark");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Copyright */}
|
||||
<View style={styles.copyrightContainer}>
|
||||
<ThemedText style={styles.copyrightText}>
|
||||
© {new Date().getFullYear()} - {t("common.footer_text")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</ThemedView>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const createStyles = (colors: typeof Colors.light, scheme: ThemeColorScheme) =>
|
||||
StyleSheet.create({
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
justifyContent: "center",
|
||||
},
|
||||
headerContainer: {
|
||||
marginBottom: 40,
|
||||
alignItems: "center",
|
||||
},
|
||||
logo: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
marginBottom: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
opacity: 0.7,
|
||||
},
|
||||
formContainer: {
|
||||
gap: 16,
|
||||
},
|
||||
inputGroup: {
|
||||
gap: 8,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.text,
|
||||
},
|
||||
loginButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
marginTop: 16,
|
||||
},
|
||||
loginButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
loginButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
footerContainer: {
|
||||
marginTop: 16,
|
||||
alignItems: "center",
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
linkText: {
|
||||
color: colors.primary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
copyrightContainer: {
|
||||
marginTop: 20,
|
||||
alignItems: "center",
|
||||
},
|
||||
copyrightText: {
|
||||
fontSize: 12,
|
||||
opacity: 0.6,
|
||||
textAlign: "center",
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
languageSwitcherContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginTop: 24,
|
||||
gap: 20,
|
||||
},
|
||||
languageSwitcherLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
opacity: 0.8,
|
||||
},
|
||||
languageButtonsContainer: {
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
},
|
||||
languageButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
languageButtonActive: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
languageButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
languageButtonTextActive: {
|
||||
color: scheme === "dark" ? colors.text : colors.surface,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
5
app/index.tsx
Normal file
5
app/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Redirect } from "expo-router";
|
||||
|
||||
export default function Index() {
|
||||
return <Redirect href="/auth/login" />;
|
||||
}
|
||||
29
app/modal.tsx
Normal file
29
app/modal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Link } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">This is a modal</ThemedText>
|
||||
<Link href="/" dismissTo style={styles.link}>
|
||||
<ThemedText type="link">Go to home screen</ThemedText>
|
||||
</Link>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user