thêm chức năng Sos và thêm glustack-ui
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
|
import AlarmList from "@/components/AlarmList";
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
import {
|
import {
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -9,20 +9,49 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
const alarmExample = {
|
||||||
|
alarms: [
|
||||||
|
{
|
||||||
|
name: "Ngập nước có cảnh báo",
|
||||||
|
t: 1762226488,
|
||||||
|
level: 1,
|
||||||
|
id: "0:8:1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tầu cảnh báo sos",
|
||||||
|
t: 1762226596,
|
||||||
|
level: 3,
|
||||||
|
id: "50:15",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Khói có cảnh báo",
|
||||||
|
t: 1762226589,
|
||||||
|
level: 1,
|
||||||
|
id: "0:1:1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cửa có cảnh báo",
|
||||||
|
t: 1762226547,
|
||||||
|
level: 1,
|
||||||
|
id: "0:7:1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
level: 3,
|
||||||
|
};
|
||||||
|
|
||||||
export default function Warning() {
|
export default function Warning() {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<View style={styles.container}>
|
||||||
<View style={styles.container}>
|
<Text style={styles.titleText}>Nhật Ký Chuyến Đi</Text>
|
||||||
<Text style={styles.titleText}>Nhật Ký Chuyến Đi</Text>
|
|
||||||
|
|
||||||
<Link href="/modal" asChild>
|
<Link href="/modal" asChild>
|
||||||
<TouchableOpacity style={styles.button}>
|
<TouchableOpacity style={styles.button}>
|
||||||
<Text style={styles.buttonText}>Mở Modal</Text>
|
<Text style={styles.buttonText}>Mở Modal</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Link>
|
</Link>
|
||||||
</View>
|
<AlarmList alarmsData={alarmExample.alarms} />
|
||||||
</ScrollView>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import GPSInfoPanel from "@/components/map/GPSInfoPanel";
|
||||||
|
import type { PolygonWithLabelProps } from "@/components/map/PolygonWithLabel";
|
||||||
import { PolygonWithLabel } from "@/components/map/PolygonWithLabel";
|
import { PolygonWithLabel } from "@/components/map/PolygonWithLabel";
|
||||||
|
import type { PolylineWithLabelProps } from "@/components/map/PolylineWithLabel";
|
||||||
import { PolylineWithLabel } from "@/components/map/PolylineWithLabel";
|
import { PolylineWithLabel } from "@/components/map/PolylineWithLabel";
|
||||||
|
import SosButton from "@/components/map/SosButton";
|
||||||
import {
|
import {
|
||||||
ENTITY,
|
ENTITY,
|
||||||
EVENT_ALARM_DATA,
|
EVENT_ALARM_DATA,
|
||||||
@@ -30,12 +34,10 @@ import {
|
|||||||
Animated,
|
Animated,
|
||||||
Image as RNImage,
|
Image as RNImage,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
View
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import MapView, { Circle, Marker } from "react-native-maps";
|
import MapView, { Circle, Marker } from "react-native-maps";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const [gpsData, setGpsData] = useState<Model.GPSResonse | undefined>(
|
const [gpsData, setGpsData] = useState<Model.GPSResonse | undefined>(
|
||||||
@@ -53,15 +55,16 @@ export default function HomeScreen() {
|
|||||||
const [zoomLevel, setZoomLevel] = useState(10);
|
const [zoomLevel, setZoomLevel] = useState(10);
|
||||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||||
const [polylineCoordinates, setPolylineCoordinates] = useState<
|
const [polylineCoordinates, setPolylineCoordinates] = useState<
|
||||||
number[][] | undefined
|
PolylineWithLabelProps | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [polygonCoordinates, setPolygonCoordinates] = useState<
|
const [polygonCoordinates, setPolygonCoordinates] = useState<
|
||||||
number[][][] | undefined
|
PolygonWithLabelProps[] | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
const theme = useColorScheme();
|
const theme = useColorScheme();
|
||||||
const scale = useRef(new Animated.Value(0)).current;
|
const scale = useRef(new Animated.Value(0)).current;
|
||||||
const opacity = useRef(new Animated.Value(1)).current;
|
const opacity = useRef(new Animated.Value(1)).current;
|
||||||
|
|
||||||
// console.log("Platform: ", platform);
|
// console.log("Platform: ", platform);
|
||||||
// console.log("Theme: ", theme);
|
// console.log("Theme: ", theme);
|
||||||
|
|
||||||
@@ -74,7 +77,12 @@ export default function HomeScreen() {
|
|||||||
getBanzonesEventBus();
|
getBanzonesEventBus();
|
||||||
getTrackPointsEventBus();
|
getTrackPointsEventBus();
|
||||||
const queryGpsData = (gpsData: Model.GPSResonse) => {
|
const queryGpsData = (gpsData: Model.GPSResonse) => {
|
||||||
setGpsData(gpsData);
|
if (gpsData) {
|
||||||
|
// console.log("GPS Data: ", gpsData);
|
||||||
|
setGpsData(gpsData);
|
||||||
|
} else {
|
||||||
|
setGpsData(undefined);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const queryAlarmData = (alarmData: Model.AlarmResponse) => {
|
const queryAlarmData = (alarmData: Model.AlarmResponse) => {
|
||||||
// console.log("Alarm Data: ", alarmData.alarms.length);
|
// console.log("Alarm Data: ", alarmData.alarms.length);
|
||||||
@@ -82,7 +90,6 @@ export default function HomeScreen() {
|
|||||||
};
|
};
|
||||||
const queryEntityData = (entityData: Model.TransformedEntity[]) => {
|
const queryEntityData = (entityData: Model.TransformedEntity[]) => {
|
||||||
// console.log("Entities Length Data: ", entityData.length);
|
// console.log("Entities Length Data: ", entityData.length);
|
||||||
|
|
||||||
setEntityData(entityData);
|
setEntityData(entityData);
|
||||||
};
|
};
|
||||||
const queryBanzonesData = (banzoneData: Model.Zone[]) => {
|
const queryBanzonesData = (banzoneData: Model.Zone[]) => {
|
||||||
@@ -92,32 +99,36 @@ export default function HomeScreen() {
|
|||||||
};
|
};
|
||||||
const queryTrackPointsData = (TrackPointsData: Model.ShipTrackPoint[]) => {
|
const queryTrackPointsData = (TrackPointsData: Model.ShipTrackPoint[]) => {
|
||||||
// console.log("TrackPoints Data: ", TrackPointsData.length);
|
// console.log("TrackPoints Data: ", TrackPointsData.length);
|
||||||
setTrackPointsData(TrackPointsData);
|
if (TrackPointsData && TrackPointsData.length > 0) {
|
||||||
|
setTrackPointsData(TrackPointsData);
|
||||||
|
} else {
|
||||||
|
setTrackPointsData(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
||||||
console.log("Registering event handlers in HomeScreen");
|
// console.log("Registering event handlers in HomeScreen");
|
||||||
eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
||||||
console.log("Subscribed to EVENT_GPS_DATA");
|
// console.log("Subscribed to EVENT_GPS_DATA");
|
||||||
eventBus.on(EVENT_ALARM_DATA, queryAlarmData);
|
eventBus.on(EVENT_ALARM_DATA, queryAlarmData);
|
||||||
console.log("Subscribed to EVENT_ALARM_DATA");
|
// console.log("Subscribed to EVENT_ALARM_DATA");
|
||||||
eventBus.on(EVENT_ENTITY_DATA, queryEntityData);
|
eventBus.on(EVENT_ENTITY_DATA, queryEntityData);
|
||||||
console.log("Subscribed to EVENT_ENTITY_DATA");
|
// console.log("Subscribed to EVENT_ENTITY_DATA");
|
||||||
eventBus.on(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
eventBus.on(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
||||||
console.log("Subscribed to EVENT_TRACK_POINTS_DATA");
|
// console.log("Subscribed to EVENT_TRACK_POINTS_DATA");
|
||||||
eventBus.once(EVENT_BANZONE_DATA, queryBanzonesData);
|
eventBus.once(EVENT_BANZONE_DATA, queryBanzonesData);
|
||||||
console.log("Subscribed once to EVENT_BANZONE_DATA");
|
// console.log("Subscribed once to EVENT_BANZONE_DATA");
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("Unregistering event handlers in HomeScreen");
|
// console.log("Unregistering event handlers in HomeScreen");
|
||||||
eventBus.off(EVENT_GPS_DATA, queryGpsData);
|
eventBus.off(EVENT_GPS_DATA, queryGpsData);
|
||||||
console.log("Unsubscribed EVENT_GPS_DATA");
|
// console.log("Unsubscribed EVENT_GPS_DATA");
|
||||||
eventBus.off(EVENT_ALARM_DATA, queryAlarmData);
|
eventBus.off(EVENT_ALARM_DATA, queryAlarmData);
|
||||||
console.log("Unsubscribed EVENT_ALARM_DATA");
|
// console.log("Unsubscribed EVENT_ALARM_DATA");
|
||||||
eventBus.off(EVENT_ENTITY_DATA, queryEntityData);
|
eventBus.off(EVENT_ENTITY_DATA, queryEntityData);
|
||||||
console.log("Unsubscribed EVENT_ENTITY_DATA");
|
// console.log("Unsubscribed EVENT_ENTITY_DATA");
|
||||||
eventBus.off(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
eventBus.off(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
||||||
console.log("Unsubscribed EVENT_TRACK_POINTS_DATA");
|
// console.log("Unsubscribed EVENT_TRACK_POINTS_DATA");
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -170,14 +181,31 @@ export default function HomeScreen() {
|
|||||||
geom_lines || ""
|
geom_lines || ""
|
||||||
);
|
);
|
||||||
if (coordinates.length > 0) {
|
if (coordinates.length > 0) {
|
||||||
setPolylineCoordinates(coordinates);
|
setPolylineCoordinates({
|
||||||
|
coordinates: coordinates.map((coord) => ({
|
||||||
|
latitude: coord[0],
|
||||||
|
longitude: coord[1],
|
||||||
|
})),
|
||||||
|
label: zone?.zone_name ?? "",
|
||||||
|
content: zone?.message ?? "",
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if (geom_type === 1) {
|
} else if (geom_type === 1) {
|
||||||
// foundPolygon = true;
|
// foundPolygon = true;
|
||||||
const coordinates = convertWKTtoLatLngString(geom_poly || "");
|
const coordinates = convertWKTtoLatLngString(geom_poly || "");
|
||||||
if (coordinates.length > 0) {
|
if (coordinates.length > 0) {
|
||||||
console.log("Polygon Coordinate: ", coordinates);
|
console.log("Polygon Coordinate: ", coordinates);
|
||||||
setPolygonCoordinates(coordinates);
|
setPolygonCoordinates(
|
||||||
|
coordinates.map((polygon) => ({
|
||||||
|
coordinates: polygon.map((coord) => ({
|
||||||
|
latitude: coord[0],
|
||||||
|
longitude: coord[1],
|
||||||
|
})),
|
||||||
|
label: zone?.zone_name ?? "",
|
||||||
|
content: zone?.message ?? "",
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +299,9 @@ export default function HomeScreen() {
|
|||||||
}, [alarmData?.level, scale, opacity]);
|
}, [alarmData?.level, scale, opacity]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView edges={["top"]} style={styles.container}>
|
<View
|
||||||
|
// edges={["top"]}
|
||||||
|
style={styles.container}>
|
||||||
<MapView
|
<MapView
|
||||||
onMapReady={handleMapReady}
|
onMapReady={handleMapReady}
|
||||||
onRegionChangeComplete={handleRegionChangeComplete}
|
onRegionChangeComplete={handleRegionChangeComplete}
|
||||||
@@ -297,7 +327,8 @@ export default function HomeScreen() {
|
|||||||
longitude: point.lon,
|
longitude: point.lon,
|
||||||
}}
|
}}
|
||||||
zIndex={50}
|
zIndex={50}
|
||||||
radius={platform === IOS_PLATFORM ? 200 : 50}
|
// radius={platform === IOS_PLATFORM ? 200 : 50}
|
||||||
|
radius={circleRadius}
|
||||||
strokeColor="rgba(16, 85, 201, 0.7)"
|
strokeColor="rgba(16, 85, 201, 0.7)"
|
||||||
fillColor="rgba(16, 85, 201, 0.7)"
|
fillColor="rgba(16, 85, 201, 0.7)"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -306,11 +337,9 @@ export default function HomeScreen() {
|
|||||||
})}
|
})}
|
||||||
{polylineCoordinates !== undefined && (
|
{polylineCoordinates !== undefined && (
|
||||||
<PolylineWithLabel
|
<PolylineWithLabel
|
||||||
coordinates={polylineCoordinates.map((coord) => ({
|
coordinates={polylineCoordinates.coordinates}
|
||||||
latitude: coord[0],
|
label={polylineCoordinates.label}
|
||||||
longitude: coord[1],
|
content={polylineCoordinates.content}
|
||||||
}))}
|
|
||||||
label="Tuyến bờ"
|
|
||||||
strokeColor="#FF5733"
|
strokeColor="#FF5733"
|
||||||
strokeWidth={4}
|
strokeWidth={4}
|
||||||
showDistance={false}
|
showDistance={false}
|
||||||
@@ -322,30 +351,16 @@ export default function HomeScreen() {
|
|||||||
{polygonCoordinates.map((polygon, index) => {
|
{polygonCoordinates.map((polygon, index) => {
|
||||||
// Tạo key ổn định từ tọa độ đầu tiên của polygon
|
// Tạo key ổn định từ tọa độ đầu tiên của polygon
|
||||||
const polygonKey =
|
const polygonKey =
|
||||||
polygon.length > 0
|
polygon.coordinates.length > 0
|
||||||
? `polygon-${polygon[0][0]}-${polygon[0][1]}-${index}`
|
? `polygon-${polygon.coordinates[0].latitude}-${polygon.coordinates[0].longitude}-${index}`
|
||||||
: `polygon-${index}`;
|
: `polygon-${index}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <Polygon
|
|
||||||
// key={polygonKey}
|
|
||||||
// coordinates={polygon.map((coords) => ({
|
|
||||||
// latitude: coords[0],
|
|
||||||
// longitude: coords[1],
|
|
||||||
// }))}
|
|
||||||
// fillColor="rgba(16, 85, 201, 0.6)"
|
|
||||||
// strokeColor="rgba(16, 85, 201, 0.8)"
|
|
||||||
// strokeWidth={2}
|
|
||||||
// zIndex={50}
|
|
||||||
// />
|
|
||||||
<PolygonWithLabel
|
<PolygonWithLabel
|
||||||
key={polygonKey}
|
key={polygonKey}
|
||||||
coordinates={polygon.map((coords) => ({
|
coordinates={polygon.coordinates}
|
||||||
latitude: coords[0],
|
label={polygon.label}
|
||||||
longitude: coords[1],
|
content={polygon.content}
|
||||||
}))}
|
|
||||||
label="Test khu đánh bắt"
|
|
||||||
content="Thời gian cấm (từ tháng 1 đến tháng 12)"
|
|
||||||
fillColor="rgba(16, 85, 201, 0.6)"
|
fillColor="rgba(16, 85, 201, 0.6)"
|
||||||
strokeColor="rgba(16, 85, 201, 0.8)"
|
strokeColor="rgba(16, 85, 201, 0.8)"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -371,7 +386,7 @@ export default function HomeScreen() {
|
|||||||
anchor={
|
anchor={
|
||||||
platform === IOS_PLATFORM
|
platform === IOS_PLATFORM
|
||||||
? { x: 0.5, y: 0.5 }
|
? { x: 0.5, y: 0.5 }
|
||||||
: { x: 0.5, y: 0.4 }
|
: { x: 0.6, y: 0.4 }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View className="w-8 h-8 items-center justify-center">
|
<View className="w-8 h-8 items-center justify-center">
|
||||||
@@ -400,11 +415,10 @@ export default function HomeScreen() {
|
|||||||
height: 32,
|
height: 32,
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
rotate: `${
|
rotate: `${typeof gpsData.h === "number" && !isNaN(gpsData.h)
|
||||||
typeof gpsData.h === "number" && !isNaN(gpsData.h)
|
|
||||||
? gpsData.h
|
? gpsData.h
|
||||||
: 0
|
: 0
|
||||||
}deg`,
|
}deg`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
@@ -414,16 +428,12 @@ export default function HomeScreen() {
|
|||||||
</Marker>
|
</Marker>
|
||||||
)}
|
)}
|
||||||
</MapView>
|
</MapView>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.button}
|
<View className="absolute top-14 right-2 shadow-md">
|
||||||
onPress={() => {
|
<SosButton />
|
||||||
setPolygonCoordinates(undefined);
|
</View>
|
||||||
setPolylineCoordinates(undefined);
|
<GPSInfoPanel gpsData={gpsData} />
|
||||||
}}
|
</View>
|
||||||
>
|
|
||||||
<Text style={styles.buttonText}>Get GPS Data</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ import { useEffect } from "react";
|
|||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
|
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider/gluestack-ui-provider";
|
||||||
import { setRouterInstance } from "@/config/auth";
|
import { setRouterInstance } from "@/config/auth";
|
||||||
|
import "@/global.css";
|
||||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -21,34 +24,36 @@ export default function RootLayout() {
|
|||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
<GluestackUIProvider>
|
||||||
<Stack
|
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||||
screenOptions={{ headerShown: false }}
|
<Stack
|
||||||
initialRouteName="auth/login"
|
screenOptions={{ headerShown: false }}
|
||||||
>
|
initialRouteName="auth/login"
|
||||||
<Stack.Screen
|
>
|
||||||
name="auth/login"
|
<Stack.Screen
|
||||||
options={{
|
name="auth/login"
|
||||||
title: "Login",
|
options={{
|
||||||
headerShown: false,
|
title: "Login",
|
||||||
}}
|
headerShown: false,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="(tabs)"
|
name="(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Home",
|
title: "Home",
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="modal"
|
name="modal"
|
||||||
options={{ presentation: "formSheet", title: "Modal" }}
|
options={{ presentation: "formSheet", title: "Modal" }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<Toast />
|
<Toast />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</GluestackUIProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
module.exports = function (api) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
presets: [
|
presets: [['babel-preset-expo'], 'nativewind/babel'],
|
||||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
|
||||||
"nativewind/babel",
|
plugins: [
|
||||||
|
[
|
||||||
|
'module-resolver',
|
||||||
|
{
|
||||||
|
root: ['./'],
|
||||||
|
|
||||||
|
alias: {
|
||||||
|
'@': './',
|
||||||
|
'tailwind.config': './tailwind.config.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react-native-worklets/plugin',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
76
components/AlarmList.tsx
Normal file
76
components/AlarmList.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { FlatList, Text, TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
|
type AlarmItem = {
|
||||||
|
name: string;
|
||||||
|
t: number;
|
||||||
|
level: number;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AlarmProp = {
|
||||||
|
alarmsData: AlarmItem[];
|
||||||
|
onPress?: (alarm: AlarmItem) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlarmList = ({ alarmsData, onPress }: AlarmProp) => {
|
||||||
|
const sortedAlarmsData = [...alarmsData].sort((a, b) => b.level - a.level);
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={sortedAlarmsData}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => onPress?.(item)}
|
||||||
|
className="flex flex-row gap-5 p-3 justify-start items-baseline w-full"
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className={`flex-none h-3 w-3 rounded-full ${getBackgroundColorByLevel(
|
||||||
|
item.level
|
||||||
|
)}`}
|
||||||
|
></View>
|
||||||
|
<View className="flex">
|
||||||
|
<Text className={`grow text-lg ${getTextColorByLevel(item.level)}`}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<Text className="grow text-md text-gray-400">
|
||||||
|
{formatTimestamp(item.t)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBackgroundColorByLevel = (level: number) => {
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return "bg-yellow-500";
|
||||||
|
case 2:
|
||||||
|
return "bg-orange-500";
|
||||||
|
case 3:
|
||||||
|
return "bg-red-500";
|
||||||
|
default:
|
||||||
|
return "bg-gray-500";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTextColorByLevel = (level: number) => {
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return "text-yellow-600";
|
||||||
|
case 2:
|
||||||
|
return "text-orange-600";
|
||||||
|
case 3:
|
||||||
|
return "text-red-600";
|
||||||
|
default:
|
||||||
|
return "text-gray-600";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimestamp = (timestamp: number) => {
|
||||||
|
return dayjs.unix(timestamp).format("DD/MM/YYYY HH:mm:ss");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmList;
|
||||||
17
components/map/Description.tsx
Normal file
17
components/map/Description.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
|
interface DescriptionProps {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
export const Description = ({
|
||||||
|
title = "",
|
||||||
|
description = "",
|
||||||
|
}: DescriptionProps) => {
|
||||||
|
return (
|
||||||
|
<View className="flex-row gap-2 ">
|
||||||
|
<Text className="opacity-50 text-lg">{title}:</Text>
|
||||||
|
<Text className="text-lg">{description}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
113
components/map/GPSInfoPanel.tsx
Normal file
113
components/map/GPSInfoPanel.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
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 { Description } from "./Description";
|
||||||
|
|
||||||
|
type GPSInfoPanelProps = {
|
||||||
|
gpsData: Model.GPSResonse | 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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
backgroundColor: "blue",
|
||||||
|
borderRadius: 4,
|
||||||
|
zIndex: 30,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Animated.View
|
||||||
|
style={{
|
||||||
|
transform: [{ translateY }],
|
||||||
|
}}
|
||||||
|
className="absolute bottom-0 gap-3 right-0 p-3 left-0 h-auto w-full rounded-t-xl bg-white shadow-md"
|
||||||
|
onLayout={(event) => setPanelHeight(event.nativeEvent.layout.height)}
|
||||||
|
>
|
||||||
|
{/* Nút toggle ở top-right */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={togglePanel}
|
||||||
|
className="absolute top-2 right-2 z-10 bg-white rounded-full p-1 shadow-sm"
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name={isExpanded ? "expand-more" : "expand-less"}
|
||||||
|
size={20}
|
||||||
|
color="#666"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View className="flex-row justify-between">
|
||||||
|
<View className="flex-1">
|
||||||
|
<Description
|
||||||
|
title="Kinh độ"
|
||||||
|
description={convertToDMS(gpsData?.lat ?? 0, true)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="flex-1">
|
||||||
|
<Description
|
||||||
|
title="Vĩ độ"
|
||||||
|
description={convertToDMS(gpsData?.lon ?? 0, false)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="flex-row justify-between">
|
||||||
|
<View className="flex-1">
|
||||||
|
<Description
|
||||||
|
title="Tốc độ"
|
||||||
|
description={`${kmhToKnot(gpsData?.s ?? 0).toString()} knot`}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="flex-1">
|
||||||
|
<Description title="Hướng" 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 bg-white rounded-full p-2 shadow-lg"
|
||||||
|
>
|
||||||
|
<MaterialIcons name="info-outline" size={24} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GPSInfoPanel;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { ANDROID_PLATFORM } from "@/constants";
|
||||||
|
import { usePlatform } from "@/hooks/use-platform";
|
||||||
import { getPolygonCenter } from "@/utils/polyline";
|
import { getPolygonCenter } from "@/utils/polyline";
|
||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { StyleSheet, Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
import { Marker, Polygon } from "react-native-maps";
|
import { MapMarker, Marker, Polygon } from "react-native-maps";
|
||||||
|
|
||||||
export interface PolygonWithLabelProps {
|
export interface PolygonWithLabelProps {
|
||||||
coordinates: {
|
coordinates: {
|
||||||
@@ -33,6 +35,8 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
|
|||||||
if (!coordinates || coordinates.length < 3) {
|
if (!coordinates || coordinates.length < 3) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const platform = usePlatform();
|
||||||
|
const markerRef = useRef<MapMarker>(null);
|
||||||
|
|
||||||
const centerPoint = getPolygonCenter(coordinates);
|
const centerPoint = getPolygonCenter(coordinates);
|
||||||
|
|
||||||
@@ -51,8 +55,7 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
|
|||||||
|
|
||||||
const paddingScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.2), 0.5);
|
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);
|
const minWidthScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.25), 0.9);
|
||||||
// console.log("Min Width Scale: ", minWidthScale);
|
markerRef.current?.showCallout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Polygon
|
<Polygon
|
||||||
@@ -64,10 +67,13 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
|
|||||||
/>
|
/>
|
||||||
{label && (
|
{label && (
|
||||||
<Marker
|
<Marker
|
||||||
|
ref={markerRef}
|
||||||
coordinate={centerPoint}
|
coordinate={centerPoint}
|
||||||
zIndex={50}
|
zIndex={50}
|
||||||
tracksViewChanges={false}
|
tracksViewChanges={false}
|
||||||
anchor={{ x: 0.5, y: 0.5 }}
|
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.markerContainer}>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { ANDROID_PLATFORM } from "@/constants";
|
||||||
|
import { usePlatform } from "@/hooks/use-platform";
|
||||||
import {
|
import {
|
||||||
calculateTotalDistance,
|
calculateTotalDistance,
|
||||||
getMiddlePointOfPolyline,
|
getMiddlePointOfPolyline,
|
||||||
} from "@/utils/polyline";
|
} from "@/utils/polyline";
|
||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { StyleSheet, Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
import { Marker, Polyline } from "react-native-maps";
|
import { MapMarker, Marker, Polyline } from "react-native-maps";
|
||||||
|
|
||||||
export interface PolylineWithLabelProps {
|
export interface PolylineWithLabelProps {
|
||||||
coordinates: {
|
coordinates: {
|
||||||
@@ -12,6 +14,7 @@ export interface PolylineWithLabelProps {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
}[];
|
}[];
|
||||||
label?: string;
|
label?: string;
|
||||||
|
content?: string;
|
||||||
strokeColor?: string;
|
strokeColor?: string;
|
||||||
strokeWidth?: number;
|
strokeWidth?: number;
|
||||||
showDistance?: boolean;
|
showDistance?: boolean;
|
||||||
@@ -24,6 +27,7 @@ export interface PolylineWithLabelProps {
|
|||||||
export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
|
export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
|
||||||
coordinates,
|
coordinates,
|
||||||
label,
|
label,
|
||||||
|
content,
|
||||||
strokeColor = "#FF5733",
|
strokeColor = "#FF5733",
|
||||||
strokeWidth = 4,
|
strokeWidth = 4,
|
||||||
showDistance = false,
|
showDistance = false,
|
||||||
@@ -35,14 +39,15 @@ export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
|
|||||||
|
|
||||||
const middlePoint = getMiddlePointOfPolyline(coordinates);
|
const middlePoint = getMiddlePointOfPolyline(coordinates);
|
||||||
const distance = calculateTotalDistance(coordinates);
|
const distance = calculateTotalDistance(coordinates);
|
||||||
|
const platform = usePlatform();
|
||||||
|
const markerRef = useRef<MapMarker>(null);
|
||||||
let displayText = label || "";
|
let displayText = label || "";
|
||||||
if (showDistance) {
|
if (showDistance) {
|
||||||
displayText += displayText
|
displayText += displayText
|
||||||
? ` (${distance.toFixed(2)}km)`
|
? ` (${distance.toFixed(2)}km)`
|
||||||
: `${distance.toFixed(2)}km`;
|
: `${distance.toFixed(2)}km`;
|
||||||
}
|
}
|
||||||
|
markerRef.current?.showCallout();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Polyline
|
<Polyline
|
||||||
@@ -53,10 +58,13 @@ export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
|
|||||||
/>
|
/>
|
||||||
{displayText && (
|
{displayText && (
|
||||||
<Marker
|
<Marker
|
||||||
|
ref={markerRef}
|
||||||
coordinate={middlePoint}
|
coordinate={middlePoint}
|
||||||
zIndex={zIndex + 10}
|
zIndex={zIndex + 10}
|
||||||
tracksViewChanges={false}
|
tracksViewChanges={false}
|
||||||
anchor={{ x: 0.5, y: 0.5 }}
|
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.markerContainer}>
|
||||||
<View style={styles.labelContainer}>
|
<View style={styles.labelContainer}>
|
||||||
|
|||||||
385
components/map/SosButton.tsx
Normal file
385
components/map/SosButton.tsx
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
import { showToastError } from "@/config";
|
||||||
|
import {
|
||||||
|
queryDeleteSos,
|
||||||
|
queryGetSos,
|
||||||
|
querySendSosMessage,
|
||||||
|
} from "@/controller/DeviceController";
|
||||||
|
import { sosMessage } from "@/utils/sosUtils";
|
||||||
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
FlatList,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { Button, ButtonText } from "../ui/gluestack-ui-provider/button";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBackdrop,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "../ui/gluestack-ui-provider/modal";
|
||||||
|
|
||||||
|
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 [showDropdown, setShowDropdown] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
|
||||||
|
const sosOptions = [
|
||||||
|
...sosMessage.map((msg) => ({ ma: msg.ma, moTa: msg.moTa })),
|
||||||
|
{ ma: 999, moTa: "Khác" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getSosData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await queryGetSos();
|
||||||
|
setSosData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch SOS data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSosData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
// Không cần validate sosMessage vì luôn có default value (11)
|
||||||
|
|
||||||
|
if (selectedSosMessage === 999 && customMessage.trim() === "") {
|
||||||
|
newErrors.customMessage = "Vui lòng nhập trạng thái";
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmSos = async () => {
|
||||||
|
if (validateForm()) {
|
||||||
|
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
|
||||||
|
setShowConfirmSosDialog(false);
|
||||||
|
// Reset form
|
||||||
|
setSelectedSosMessage(null);
|
||||||
|
setCustomMessage("");
|
||||||
|
setErrors({});
|
||||||
|
await sendSosMessage(messageToSend);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickButton = async (isActive: boolean) => {
|
||||||
|
if (isActive) {
|
||||||
|
console.log("Active");
|
||||||
|
const resp = await queryDeleteSos();
|
||||||
|
if (resp.status === 200) {
|
||||||
|
await getSosData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No Active");
|
||||||
|
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);
|
||||||
|
showToastError("Không thể gửi tín hiệu SOS", "Lỗi");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="shadow-md rounded-full"
|
||||||
|
size="lg"
|
||||||
|
action="negative"
|
||||||
|
onPress={() => handleClickButton(sosData?.active || false)}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="warning" size={15} color="white" />
|
||||||
|
<ButtonText className="text-center">
|
||||||
|
{sosData?.active ? "Đang trong trạng thái khẩn cấp" : "Khẩn cấp"}
|
||||||
|
</ButtonText>
|
||||||
|
{/* <ButtonSpinner /> */}
|
||||||
|
{/* <ButtonIcon /> */}
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={showConfirmSosDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setShowConfirmSosDialog(false);
|
||||||
|
setSelectedSosMessage(null);
|
||||||
|
setCustomMessage("");
|
||||||
|
setErrors({});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalBackdrop />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader className="flex-col gap-0.5 items-center">
|
||||||
|
<Text
|
||||||
|
style={{ fontSize: 18, fontWeight: "bold", textAlign: "center" }}
|
||||||
|
>
|
||||||
|
Thông báo khẩn cấp
|
||||||
|
</Text>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="mb-4">
|
||||||
|
<ScrollView style={{ maxHeight: 400 }}>
|
||||||
|
{/* Dropdown Nội dung SOS */}
|
||||||
|
<View style={styles.formGroup}>
|
||||||
|
<Text style={styles.label}>Nội dung:</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.dropdownButton,
|
||||||
|
errors.sosMessage ? styles.errorBorder : {},
|
||||||
|
]}
|
||||||
|
onPress={() => setShowDropdown(!showDropdown)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.dropdownButtonText,
|
||||||
|
!selectedSosMessage && styles.placeholderText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{selectedSosMessage !== null
|
||||||
|
? sosOptions.find((opt) => opt.ma === selectedSosMessage)
|
||||||
|
?.moTa || "Chọn lý do"
|
||||||
|
: "Chọn lý do"}
|
||||||
|
</Text>
|
||||||
|
<MaterialIcons
|
||||||
|
name={showDropdown ? "expand-less" : "expand-more"}
|
||||||
|
size={20}
|
||||||
|
color="#666"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{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}>Nhập trạng thái</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[
|
||||||
|
styles.input,
|
||||||
|
errors.customMessage ? styles.errorInput : {},
|
||||||
|
]}
|
||||||
|
placeholder="Mô tả trạng thái khẩn cấp"
|
||||||
|
placeholderTextColor="#999"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter className="flex-row items-start gap-2">
|
||||||
|
<Button
|
||||||
|
onPress={handleConfirmSos}
|
||||||
|
// className="w-1/3"
|
||||||
|
action="negative"
|
||||||
|
>
|
||||||
|
<ButtonText>Xác nhận</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
setShowConfirmSosDialog(false);
|
||||||
|
setSelectedSosMessage(null);
|
||||||
|
setCustomMessage("");
|
||||||
|
setErrors({});
|
||||||
|
}}
|
||||||
|
// className="w-1/3"
|
||||||
|
action="secondary"
|
||||||
|
>
|
||||||
|
<ButtonText>Hủy</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Dropdown Modal - Nổi lên */}
|
||||||
|
{showDropdown && showConfirmSosDialog && (
|
||||||
|
<Modal isOpen={showDropdown} onClose={() => setShowDropdown(false)}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.dropdownOverlay}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setShowDropdown(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.dropdownModalContainer}>
|
||||||
|
<FlatList
|
||||||
|
data={sosOptions}
|
||||||
|
keyExtractor={(item) => item.ma.toString()}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.dropdownModalItem}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedSosMessage(item.ma);
|
||||||
|
setShowDropdown(false);
|
||||||
|
|
||||||
|
// Clear custom message nếu chọn khác lý do
|
||||||
|
if (item.ma !== 999) {
|
||||||
|
setCustomMessage("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.dropdownModalItemText,
|
||||||
|
selectedSosMessage === item.ma &&
|
||||||
|
styles.selectedItemText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.moTa}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
formGroup: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: 8,
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
dropdownButton: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ddd",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 12,
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
errorBorder: {
|
||||||
|
borderColor: "#ff4444",
|
||||||
|
},
|
||||||
|
dropdownButtonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#333",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
placeholderText: {
|
||||||
|
color: "#999",
|
||||||
|
},
|
||||||
|
dropdownList: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ddd",
|
||||||
|
borderRadius: 8,
|
||||||
|
marginTop: 4,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#eee",
|
||||||
|
},
|
||||||
|
dropdownItemText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
dropdownOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
dropdownModalContainer: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderRadius: 12,
|
||||||
|
maxHeight: 400,
|
||||||
|
minWidth: 280,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 10,
|
||||||
|
},
|
||||||
|
dropdownModalItem: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#f0f0f0",
|
||||||
|
},
|
||||||
|
dropdownModalItemText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
selectedItemText: {
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#1054C9",
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ddd",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 12,
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#333",
|
||||||
|
textAlignVertical: "top",
|
||||||
|
},
|
||||||
|
errorInput: {
|
||||||
|
borderColor: "#ff4444",
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: "#ff4444",
|
||||||
|
fontSize: 12,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SosButton;
|
||||||
296
components/ui/gluestack-ui-provider/alert-dialog/index.tsx
Normal file
296
components/ui/gluestack-ui-provider/alert-dialog/index.tsx
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAlertDialog } from '@gluestack-ui/core/alert-dialog/creator';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
Motion,
|
||||||
|
AnimatePresence,
|
||||||
|
createMotionAnimatedComponent,
|
||||||
|
MotionComponentProps,
|
||||||
|
} from '@legendapp/motion';
|
||||||
|
import { View, Pressable, ScrollView, ViewStyle } from 'react-native';
|
||||||
|
|
||||||
|
const SCOPE = 'ALERT_DIALOG';
|
||||||
|
|
||||||
|
const RootComponent = withStyleContext(View, SCOPE);
|
||||||
|
|
||||||
|
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||||
|
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||||
|
|
||||||
|
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
||||||
|
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const AnimatedPressable = createMotionAnimatedComponent(
|
||||||
|
Pressable
|
||||||
|
) as React.ComponentType<IAnimatedPressableProps>;
|
||||||
|
|
||||||
|
const UIAccessibleAlertDialog = createAlertDialog({
|
||||||
|
Root: RootComponent,
|
||||||
|
Body: ScrollView,
|
||||||
|
Content: MotionView,
|
||||||
|
CloseButton: Pressable,
|
||||||
|
Header: View,
|
||||||
|
Footer: View,
|
||||||
|
Backdrop: AnimatedPressable,
|
||||||
|
AnimatePresence: AnimatePresence,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(MotionView, { className: 'style' });
|
||||||
|
cssInterop(AnimatedPressable, { className: 'style' });
|
||||||
|
|
||||||
|
const alertDialogStyle = tva({
|
||||||
|
base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
xs: '',
|
||||||
|
sm: '',
|
||||||
|
md: '',
|
||||||
|
lg: '',
|
||||||
|
full: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogContentStyle = tva({
|
||||||
|
base: 'bg-background-0 rounded-lg overflow-hidden border border-outline-100 p-6',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
xs: 'w-[60%] max-w-[360px]',
|
||||||
|
sm: 'w-[70%] max-w-[420px]',
|
||||||
|
md: 'w-[80%] max-w-[510px]',
|
||||||
|
lg: 'w-[90%] max-w-[640px]',
|
||||||
|
full: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogCloseButtonStyle = tva({
|
||||||
|
base: 'group/alert-dialog-close-button z-10 rounded-sm p-2 data-[focus-visible=true]:bg-background-100 web:cursor-pointer outline-0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogHeaderStyle = tva({
|
||||||
|
base: 'justify-between items-center flex-row',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogFooterStyle = tva({
|
||||||
|
base: 'flex-row justify-end items-center gap-3',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertDialogBodyStyle = tva({ base: '' });
|
||||||
|
|
||||||
|
const alertDialogBackdropStyle = tva({
|
||||||
|
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
|
||||||
|
});
|
||||||
|
|
||||||
|
type IAlertDialogProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogContentProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Content
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogContentStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IAlertDialogCloseButtonProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.CloseButton
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogCloseButtonStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogHeaderProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Header
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogHeaderStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogFooterProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Footer
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogFooterStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogBodyProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Body
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogBodyStyle>;
|
||||||
|
|
||||||
|
type IAlertDialogBackdropProps = React.ComponentPropsWithoutRef<
|
||||||
|
typeof UIAccessibleAlertDialog.Backdrop
|
||||||
|
> &
|
||||||
|
VariantProps<typeof alertDialogBackdropStyle> & { className?: string };
|
||||||
|
|
||||||
|
const AlertDialog = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog>,
|
||||||
|
IAlertDialogProps
|
||||||
|
>(function AlertDialog({ className, size = 'md', ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogStyle({ class: className })}
|
||||||
|
context={{ size }}
|
||||||
|
pointerEvents="box-none"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Content>,
|
||||||
|
IAlertDialogContentProps
|
||||||
|
>(function AlertDialogContent({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Content
|
||||||
|
pointerEvents="auto"
|
||||||
|
ref={ref}
|
||||||
|
initial={{
|
||||||
|
scale: 0.9,
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
scale: 0.9,
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 250,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogContentStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogCloseButton = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.CloseButton>,
|
||||||
|
IAlertDialogCloseButtonProps
|
||||||
|
>(function AlertDialogCloseButton({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.CloseButton
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogCloseButtonStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogHeader = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Header>,
|
||||||
|
IAlertDialogHeaderProps
|
||||||
|
>(function AlertDialogHeader({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Header
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogHeaderStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogFooter = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Footer>,
|
||||||
|
IAlertDialogFooterProps
|
||||||
|
>(function AlertDialogFooter({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Footer
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogFooterStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogBody = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Body>,
|
||||||
|
IAlertDialogBodyProps
|
||||||
|
>(function AlertDialogBody({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Body
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogBodyStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertDialogBackdrop = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIAccessibleAlertDialog.Backdrop>,
|
||||||
|
IAlertDialogBackdropProps
|
||||||
|
>(function AlertDialogBackdrop({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIAccessibleAlertDialog.Backdrop
|
||||||
|
ref={ref}
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 250,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={alertDialogBackdropStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AlertDialog.displayName = 'AlertDialog';
|
||||||
|
AlertDialogContent.displayName = 'AlertDialogContent';
|
||||||
|
AlertDialogCloseButton.displayName = 'AlertDialogCloseButton';
|
||||||
|
AlertDialogHeader.displayName = 'AlertDialogHeader';
|
||||||
|
AlertDialogFooter.displayName = 'AlertDialogFooter';
|
||||||
|
AlertDialogBody.displayName = 'AlertDialogBody';
|
||||||
|
AlertDialogBackdrop.displayName = 'AlertDialogBackdrop';
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogCloseButton,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogBackdrop,
|
||||||
|
};
|
||||||
434
components/ui/gluestack-ui-provider/button/index.tsx
Normal file
434
components/ui/gluestack-ui-provider/button/index.tsx
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createButton } from '@gluestack-ui/core/button/creator';
|
||||||
|
import {
|
||||||
|
tva,
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
type VariantProps,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
|
||||||
|
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||||
|
|
||||||
|
const SCOPE = 'BUTTON';
|
||||||
|
|
||||||
|
const Root = withStyleContext(Pressable, SCOPE);
|
||||||
|
|
||||||
|
const UIButton = createButton({
|
||||||
|
Root: Root,
|
||||||
|
Text,
|
||||||
|
Group: View,
|
||||||
|
Spinner: ActivityIndicator,
|
||||||
|
Icon: UIIcon,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(PrimitiveIcon, {
|
||||||
|
className: {
|
||||||
|
target: 'style',
|
||||||
|
nativeStyleToProp: {
|
||||||
|
height: true,
|
||||||
|
width: true,
|
||||||
|
fill: true,
|
||||||
|
color: 'classNameColor',
|
||||||
|
stroke: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonStyle = tva({
|
||||||
|
base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
|
||||||
|
variants: {
|
||||||
|
action: {
|
||||||
|
primary:
|
||||||
|
'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
secondary:
|
||||||
|
'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
positive:
|
||||||
|
'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
negative:
|
||||||
|
'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info',
|
||||||
|
default:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
link: 'px-0',
|
||||||
|
outline:
|
||||||
|
'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
solid: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
size: {
|
||||||
|
xs: 'px-3.5 h-8',
|
||||||
|
sm: 'px-4 h-9',
|
||||||
|
md: 'px-5 h-10',
|
||||||
|
lg: 'px-6 h-11',
|
||||||
|
xl: 'px-7 h-12',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
action: 'primary',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'secondary',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'positive',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'negative',
|
||||||
|
variant: 'link',
|
||||||
|
class:
|
||||||
|
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'primary',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'secondary',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'positive',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'negative',
|
||||||
|
variant: 'outline',
|
||||||
|
class:
|
||||||
|
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonTextStyle = tva({
|
||||||
|
base: 'text-typography-0 font-semibold web:select-none',
|
||||||
|
parentVariants: {
|
||||||
|
action: {
|
||||||
|
primary:
|
||||||
|
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
|
||||||
|
secondary:
|
||||||
|
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
|
||||||
|
positive:
|
||||||
|
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
|
||||||
|
negative:
|
||||||
|
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
link: 'data-[hover=true]:underline data-[active=true]:underline',
|
||||||
|
outline: '',
|
||||||
|
solid:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
xs: 'text-xs',
|
||||||
|
sm: 'text-sm',
|
||||||
|
md: 'text-base',
|
||||||
|
lg: 'text-lg',
|
||||||
|
xl: 'text-xl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentCompoundVariants: [
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'primary',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'secondary',
|
||||||
|
class:
|
||||||
|
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'positive',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'negative',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'primary',
|
||||||
|
class:
|
||||||
|
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'secondary',
|
||||||
|
class:
|
||||||
|
'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'positive',
|
||||||
|
class:
|
||||||
|
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'outline',
|
||||||
|
action: 'negative',
|
||||||
|
class:
|
||||||
|
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonIconStyle = tva({
|
||||||
|
base: 'fill-none',
|
||||||
|
parentVariants: {
|
||||||
|
variant: {
|
||||||
|
link: 'data-[hover=true]:underline data-[active=true]:underline',
|
||||||
|
outline: '',
|
||||||
|
solid:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
xs: 'h-3.5 w-3.5',
|
||||||
|
sm: 'h-4 w-4',
|
||||||
|
md: 'h-[18px] w-[18px]',
|
||||||
|
lg: 'h-[18px] w-[18px]',
|
||||||
|
xl: 'h-5 w-5',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
primary:
|
||||||
|
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
|
||||||
|
secondary:
|
||||||
|
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
|
||||||
|
positive:
|
||||||
|
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
|
||||||
|
|
||||||
|
negative:
|
||||||
|
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parentCompoundVariants: [
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'primary',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'secondary',
|
||||||
|
class:
|
||||||
|
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'positive',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'solid',
|
||||||
|
action: 'negative',
|
||||||
|
class:
|
||||||
|
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonGroupStyle = tva({
|
||||||
|
base: '',
|
||||||
|
variants: {
|
||||||
|
space: {
|
||||||
|
'xs': 'gap-1',
|
||||||
|
'sm': 'gap-2',
|
||||||
|
'md': 'gap-3',
|
||||||
|
'lg': 'gap-4',
|
||||||
|
'xl': 'gap-5',
|
||||||
|
'2xl': 'gap-6',
|
||||||
|
'3xl': 'gap-7',
|
||||||
|
'4xl': 'gap-8',
|
||||||
|
},
|
||||||
|
isAttached: {
|
||||||
|
true: 'gap-0',
|
||||||
|
},
|
||||||
|
flexDirection: {
|
||||||
|
'row': 'flex-row',
|
||||||
|
'column': 'flex-col',
|
||||||
|
'row-reverse': 'flex-row-reverse',
|
||||||
|
'column-reverse': 'flex-col-reverse',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type IButtonProps = Omit<
|
||||||
|
React.ComponentPropsWithoutRef<typeof UIButton>,
|
||||||
|
'context'
|
||||||
|
> &
|
||||||
|
VariantProps<typeof buttonStyle> & { className?: string };
|
||||||
|
|
||||||
|
const Button = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton>,
|
||||||
|
IButtonProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<UIButton
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonStyle({ variant, size, action, class: className })}
|
||||||
|
context={{ variant, size, action }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text> &
|
||||||
|
VariantProps<typeof buttonTextStyle> & { className?: string };
|
||||||
|
|
||||||
|
const ButtonText = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton.Text>,
|
||||||
|
IButtonTextProps
|
||||||
|
>(({ className, variant, size, action, ...props }, ref) => {
|
||||||
|
const {
|
||||||
|
variant: parentVariant,
|
||||||
|
size: parentSize,
|
||||||
|
action: parentAction,
|
||||||
|
} = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIButton.Text
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonTextStyle({
|
||||||
|
parentVariants: {
|
||||||
|
variant: parentVariant,
|
||||||
|
size: parentSize,
|
||||||
|
action: parentAction,
|
||||||
|
},
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
action,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ButtonSpinner = UIButton.Spinner;
|
||||||
|
|
||||||
|
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon> &
|
||||||
|
VariantProps<typeof buttonIconStyle> & {
|
||||||
|
className?: string | undefined;
|
||||||
|
as?: React.ElementType;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonIcon = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton.Icon>,
|
||||||
|
IButtonIcon
|
||||||
|
>(({ className, size, ...props }, ref) => {
|
||||||
|
const {
|
||||||
|
variant: parentVariant,
|
||||||
|
size: parentSize,
|
||||||
|
action: parentAction,
|
||||||
|
} = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
if (typeof size === 'number') {
|
||||||
|
return (
|
||||||
|
<UIButton.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonIconStyle({ class: className })}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
(props.height !== undefined || props.width !== undefined) &&
|
||||||
|
size === undefined
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIButton.Icon
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={buttonIconStyle({ class: className })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UIButton.Icon
|
||||||
|
{...props}
|
||||||
|
className={buttonIconStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
variant: parentVariant,
|
||||||
|
action: parentAction,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group> &
|
||||||
|
VariantProps<typeof buttonGroupStyle>;
|
||||||
|
|
||||||
|
const ButtonGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof UIButton.Group>,
|
||||||
|
IButtonGroupProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
space = 'md',
|
||||||
|
isAttached = false,
|
||||||
|
flexDirection = 'column',
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<UIButton.Group
|
||||||
|
className={buttonGroupStyle({
|
||||||
|
class: className,
|
||||||
|
space,
|
||||||
|
isAttached,
|
||||||
|
flexDirection,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
ButtonText.displayName = 'ButtonText';
|
||||||
|
ButtonSpinner.displayName = 'ButtonSpinner';
|
||||||
|
ButtonIcon.displayName = 'ButtonIcon';
|
||||||
|
ButtonGroup.displayName = 'ButtonGroup';
|
||||||
|
|
||||||
|
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
'use client';
|
||||||
|
import { vars } from 'nativewind';
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
light: vars({
|
||||||
|
'--color-primary-0': '179 179 179',
|
||||||
|
'--color-primary-50': '153 153 153',
|
||||||
|
'--color-primary-100': '128 128 128',
|
||||||
|
'--color-primary-200': '115 115 115',
|
||||||
|
'--color-primary-300': '102 102 102',
|
||||||
|
'--color-primary-400': '82 82 82',
|
||||||
|
'--color-primary-500': '51 51 51',
|
||||||
|
'--color-primary-600': '41 41 41',
|
||||||
|
'--color-primary-700': '31 31 31',
|
||||||
|
'--color-primary-800': '13 13 13',
|
||||||
|
'--color-primary-900': '10 10 10',
|
||||||
|
'--color-primary-950': '8 8 8',
|
||||||
|
|
||||||
|
/* Secondary */
|
||||||
|
'--color-secondary-0': '253 253 253',
|
||||||
|
'--color-secondary-50': '251 251 251',
|
||||||
|
'--color-secondary-100': '246 246 246',
|
||||||
|
'--color-secondary-200': '242 242 242',
|
||||||
|
'--color-secondary-300': '237 237 237',
|
||||||
|
'--color-secondary-400': '230 230 231',
|
||||||
|
'--color-secondary-500': '217 217 219',
|
||||||
|
'--color-secondary-600': '198 199 199',
|
||||||
|
'--color-secondary-700': '189 189 189',
|
||||||
|
'--color-secondary-800': '177 177 177',
|
||||||
|
'--color-secondary-900': '165 164 164',
|
||||||
|
'--color-secondary-950': '157 157 157',
|
||||||
|
|
||||||
|
/* Tertiary */
|
||||||
|
'--color-tertiary-0': '255 250 245',
|
||||||
|
'--color-tertiary-50': '255 242 229',
|
||||||
|
'--color-tertiary-100': '255 233 213',
|
||||||
|
'--color-tertiary-200': '254 209 170',
|
||||||
|
'--color-tertiary-300': '253 180 116',
|
||||||
|
'--color-tertiary-400': '251 157 75',
|
||||||
|
'--color-tertiary-500': '231 129 40',
|
||||||
|
'--color-tertiary-600': '215 117 31',
|
||||||
|
'--color-tertiary-700': '180 98 26',
|
||||||
|
'--color-tertiary-800': '130 73 23',
|
||||||
|
'--color-tertiary-900': '108 61 19',
|
||||||
|
'--color-tertiary-950': '84 49 18',
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
'--color-error-0': '254 233 233',
|
||||||
|
'--color-error-50': '254 226 226',
|
||||||
|
'--color-error-100': '254 202 202',
|
||||||
|
'--color-error-200': '252 165 165',
|
||||||
|
'--color-error-300': '248 113 113',
|
||||||
|
'--color-error-400': '239 68 68',
|
||||||
|
'--color-error-500': '230 53 53',
|
||||||
|
'--color-error-600': '220 38 38',
|
||||||
|
'--color-error-700': '185 28 28',
|
||||||
|
'--color-error-800': '153 27 27',
|
||||||
|
'--color-error-900': '127 29 29',
|
||||||
|
'--color-error-950': '83 19 19',
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
'--color-success-0': '228 255 244',
|
||||||
|
'--color-success-50': '202 255 232',
|
||||||
|
'--color-success-100': '162 241 192',
|
||||||
|
'--color-success-200': '132 211 162',
|
||||||
|
'--color-success-300': '102 181 132',
|
||||||
|
'--color-success-400': '72 151 102',
|
||||||
|
'--color-success-500': '52 131 82',
|
||||||
|
'--color-success-600': '42 121 72',
|
||||||
|
'--color-success-700': '32 111 62',
|
||||||
|
'--color-success-800': '22 101 52',
|
||||||
|
'--color-success-900': '20 83 45',
|
||||||
|
'--color-success-950': '27 50 36',
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
'--color-warning-0': '255 249 245',
|
||||||
|
'--color-warning-50': '255 244 236',
|
||||||
|
'--color-warning-100': '255 231 213',
|
||||||
|
'--color-warning-200': '254 205 170',
|
||||||
|
'--color-warning-300': '253 173 116',
|
||||||
|
'--color-warning-400': '251 149 75',
|
||||||
|
'--color-warning-500': '231 120 40',
|
||||||
|
'--color-warning-600': '215 108 31',
|
||||||
|
'--color-warning-700': '180 90 26',
|
||||||
|
'--color-warning-800': '130 68 23',
|
||||||
|
'--color-warning-900': '108 56 19',
|
||||||
|
'--color-warning-950': '84 45 18',
|
||||||
|
|
||||||
|
/* Info */
|
||||||
|
'--color-info-0': '236 248 254',
|
||||||
|
'--color-info-50': '199 235 252',
|
||||||
|
'--color-info-100': '162 221 250',
|
||||||
|
'--color-info-200': '124 207 248',
|
||||||
|
'--color-info-300': '87 194 246',
|
||||||
|
'--color-info-400': '50 180 244',
|
||||||
|
'--color-info-500': '13 166 242',
|
||||||
|
'--color-info-600': '11 141 205',
|
||||||
|
'--color-info-700': '9 115 168',
|
||||||
|
'--color-info-800': '7 90 131',
|
||||||
|
'--color-info-900': '5 64 93',
|
||||||
|
'--color-info-950': '3 38 56',
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
'--color-typography-0': '254 254 255',
|
||||||
|
'--color-typography-50': '245 245 245',
|
||||||
|
'--color-typography-100': '229 229 229',
|
||||||
|
'--color-typography-200': '219 219 220',
|
||||||
|
'--color-typography-300': '212 212 212',
|
||||||
|
'--color-typography-400': '163 163 163',
|
||||||
|
'--color-typography-500': '140 140 140',
|
||||||
|
'--color-typography-600': '115 115 115',
|
||||||
|
'--color-typography-700': '82 82 82',
|
||||||
|
'--color-typography-800': '64 64 64',
|
||||||
|
'--color-typography-900': '38 38 39',
|
||||||
|
'--color-typography-950': '23 23 23',
|
||||||
|
|
||||||
|
/* Outline */
|
||||||
|
'--color-outline-0': '253 254 254',
|
||||||
|
'--color-outline-50': '243 243 243',
|
||||||
|
'--color-outline-100': '230 230 230',
|
||||||
|
'--color-outline-200': '221 220 219',
|
||||||
|
'--color-outline-300': '211 211 211',
|
||||||
|
'--color-outline-400': '165 163 163',
|
||||||
|
'--color-outline-500': '140 141 141',
|
||||||
|
'--color-outline-600': '115 116 116',
|
||||||
|
'--color-outline-700': '83 82 82',
|
||||||
|
'--color-outline-800': '65 65 65',
|
||||||
|
'--color-outline-900': '39 38 36',
|
||||||
|
'--color-outline-950': '26 23 23',
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
'--color-background-0': '255 255 255',
|
||||||
|
'--color-background-50': '246 246 246',
|
||||||
|
'--color-background-100': '242 241 241',
|
||||||
|
'--color-background-200': '220 219 219',
|
||||||
|
'--color-background-300': '213 212 212',
|
||||||
|
'--color-background-400': '162 163 163',
|
||||||
|
'--color-background-500': '142 142 142',
|
||||||
|
'--color-background-600': '116 116 116',
|
||||||
|
'--color-background-700': '83 82 82',
|
||||||
|
'--color-background-800': '65 64 64',
|
||||||
|
'--color-background-900': '39 38 37',
|
||||||
|
'--color-background-950': '18 18 18',
|
||||||
|
|
||||||
|
/* Background Special */
|
||||||
|
'--color-background-error': '254 241 241',
|
||||||
|
'--color-background-warning': '255 243 234',
|
||||||
|
'--color-background-success': '237 252 242',
|
||||||
|
'--color-background-muted': '247 248 247',
|
||||||
|
'--color-background-info': '235 248 254',
|
||||||
|
|
||||||
|
/* Focus Ring Indicator */
|
||||||
|
'--color-indicator-primary': '55 55 55',
|
||||||
|
'--color-indicator-info': '83 153 236',
|
||||||
|
'--color-indicator-error': '185 28 28',
|
||||||
|
}),
|
||||||
|
dark: vars({
|
||||||
|
'--color-primary-0': '166 166 166',
|
||||||
|
'--color-primary-50': '175 175 175',
|
||||||
|
'--color-primary-100': '186 186 186',
|
||||||
|
'--color-primary-200': '197 197 197',
|
||||||
|
'--color-primary-300': '212 212 212',
|
||||||
|
'--color-primary-400': '221 221 221',
|
||||||
|
'--color-primary-500': '230 230 230',
|
||||||
|
'--color-primary-600': '240 240 240',
|
||||||
|
'--color-primary-700': '250 250 250',
|
||||||
|
'--color-primary-800': '253 253 253',
|
||||||
|
'--color-primary-900': '254 249 249',
|
||||||
|
'--color-primary-950': '253 252 252',
|
||||||
|
|
||||||
|
/* Secondary */
|
||||||
|
'--color-secondary-0': '20 20 20',
|
||||||
|
'--color-secondary-50': '23 23 23',
|
||||||
|
'--color-secondary-100': '31 31 31',
|
||||||
|
'--color-secondary-200': '39 39 39',
|
||||||
|
'--color-secondary-300': '44 44 44',
|
||||||
|
'--color-secondary-400': '56 57 57',
|
||||||
|
'--color-secondary-500': '63 64 64',
|
||||||
|
'--color-secondary-600': '86 86 86',
|
||||||
|
'--color-secondary-700': '110 110 110',
|
||||||
|
'--color-secondary-800': '135 135 135',
|
||||||
|
'--color-secondary-900': '150 150 150',
|
||||||
|
'--color-secondary-950': '164 164 164',
|
||||||
|
|
||||||
|
/* Tertiary */
|
||||||
|
'--color-tertiary-0': '84 49 18',
|
||||||
|
'--color-tertiary-50': '108 61 19',
|
||||||
|
'--color-tertiary-100': '130 73 23',
|
||||||
|
'--color-tertiary-200': '180 98 26',
|
||||||
|
'--color-tertiary-300': '215 117 31',
|
||||||
|
'--color-tertiary-400': '231 129 40',
|
||||||
|
'--color-tertiary-500': '251 157 75',
|
||||||
|
'--color-tertiary-600': '253 180 116',
|
||||||
|
'--color-tertiary-700': '254 209 170',
|
||||||
|
'--color-tertiary-800': '255 233 213',
|
||||||
|
'--color-tertiary-900': '255 242 229',
|
||||||
|
'--color-tertiary-950': '255 250 245',
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
'--color-error-0': '83 19 19',
|
||||||
|
'--color-error-50': '127 29 29',
|
||||||
|
'--color-error-100': '153 27 27',
|
||||||
|
'--color-error-200': '185 28 28',
|
||||||
|
'--color-error-300': '220 38 38',
|
||||||
|
'--color-error-400': '230 53 53',
|
||||||
|
'--color-error-500': '239 68 68',
|
||||||
|
'--color-error-600': '249 97 96',
|
||||||
|
'--color-error-700': '229 91 90',
|
||||||
|
'--color-error-800': '254 202 202',
|
||||||
|
'--color-error-900': '254 226 226',
|
||||||
|
'--color-error-950': '254 233 233',
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
'--color-success-0': '27 50 36',
|
||||||
|
'--color-success-50': '20 83 45',
|
||||||
|
'--color-success-100': '22 101 52',
|
||||||
|
'--color-success-200': '32 111 62',
|
||||||
|
'--color-success-300': '42 121 72',
|
||||||
|
'--color-success-400': '52 131 82',
|
||||||
|
'--color-success-500': '72 151 102',
|
||||||
|
'--color-success-600': '102 181 132',
|
||||||
|
'--color-success-700': '132 211 162',
|
||||||
|
'--color-success-800': '162 241 192',
|
||||||
|
'--color-success-900': '202 255 232',
|
||||||
|
'--color-success-950': '228 255 244',
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
'--color-warning-0': '84 45 18',
|
||||||
|
'--color-warning-50': '108 56 19',
|
||||||
|
'--color-warning-100': '130 68 23',
|
||||||
|
'--color-warning-200': '180 90 26',
|
||||||
|
'--color-warning-300': '215 108 31',
|
||||||
|
'--color-warning-400': '231 120 40',
|
||||||
|
'--color-warning-500': '251 149 75',
|
||||||
|
'--color-warning-600': '253 173 116',
|
||||||
|
'--color-warning-700': '254 205 170',
|
||||||
|
'--color-warning-800': '255 231 213',
|
||||||
|
'--color-warning-900': '255 244 237',
|
||||||
|
'--color-warning-950': '255 249 245',
|
||||||
|
|
||||||
|
/* Info */
|
||||||
|
'--color-info-0': '3 38 56',
|
||||||
|
'--color-info-50': '5 64 93',
|
||||||
|
'--color-info-100': '7 90 131',
|
||||||
|
'--color-info-200': '9 115 168',
|
||||||
|
'--color-info-300': '11 141 205',
|
||||||
|
'--color-info-400': '13 166 242',
|
||||||
|
'--color-info-500': '50 180 244',
|
||||||
|
'--color-info-600': '87 194 246',
|
||||||
|
'--color-info-700': '124 207 248',
|
||||||
|
'--color-info-800': '162 221 250',
|
||||||
|
'--color-info-900': '199 235 252',
|
||||||
|
'--color-info-950': '236 248 254',
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
'--color-typography-0': '23 23 23',
|
||||||
|
'--color-typography-50': '38 38 39',
|
||||||
|
'--color-typography-100': '64 64 64',
|
||||||
|
'--color-typography-200': '82 82 82',
|
||||||
|
'--color-typography-300': '115 115 115',
|
||||||
|
'--color-typography-400': '140 140 140',
|
||||||
|
'--color-typography-500': '163 163 163',
|
||||||
|
'--color-typography-600': '212 212 212',
|
||||||
|
'--color-typography-700': '219 219 220',
|
||||||
|
'--color-typography-800': '229 229 229',
|
||||||
|
'--color-typography-900': '245 245 245',
|
||||||
|
'--color-typography-950': '254 254 255',
|
||||||
|
|
||||||
|
/* Outline */
|
||||||
|
'--color-outline-0': '26 23 23',
|
||||||
|
'--color-outline-50': '39 38 36',
|
||||||
|
'--color-outline-100': '65 65 65',
|
||||||
|
'--color-outline-200': '83 82 82',
|
||||||
|
'--color-outline-300': '115 116 116',
|
||||||
|
'--color-outline-400': '140 141 141',
|
||||||
|
'--color-outline-500': '165 163 163',
|
||||||
|
'--color-outline-600': '211 211 211',
|
||||||
|
'--color-outline-700': '221 220 219',
|
||||||
|
'--color-outline-800': '230 230 230',
|
||||||
|
'--color-outline-900': '243 243 243',
|
||||||
|
'--color-outline-950': '253 254 254',
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
'--color-background-0': '18 18 18',
|
||||||
|
'--color-background-50': '39 38 37',
|
||||||
|
'--color-background-100': '65 64 64',
|
||||||
|
'--color-background-200': '83 82 82',
|
||||||
|
'--color-background-300': '116 116 116',
|
||||||
|
'--color-background-400': '142 142 142',
|
||||||
|
'--color-background-500': '162 163 163',
|
||||||
|
'--color-background-600': '213 212 212',
|
||||||
|
'--color-background-700': '229 228 228',
|
||||||
|
'--color-background-800': '242 241 241',
|
||||||
|
'--color-background-900': '246 246 246',
|
||||||
|
'--color-background-950': '255 255 255',
|
||||||
|
|
||||||
|
/* Background Special */
|
||||||
|
'--color-background-error': '66 43 43',
|
||||||
|
'--color-background-warning': '65 47 35',
|
||||||
|
'--color-background-success': '28 43 33',
|
||||||
|
'--color-background-muted': '51 51 51',
|
||||||
|
'--color-background-info': '26 40 46',
|
||||||
|
|
||||||
|
/* Focus Ring Indicator */
|
||||||
|
'--color-indicator-primary': '247 247 247',
|
||||||
|
'--color-indicator-info': '161 199 245',
|
||||||
|
'--color-indicator-error': '232 70 69',
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// This is a Next.js 15 compatible version of the GluestackUIProvider
|
||||||
|
'use client';
|
||||||
|
import React, { useEffect, useLayoutEffect } from 'react';
|
||||||
|
import { config } from './config';
|
||||||
|
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||||
|
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||||
|
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { script } from './script';
|
||||||
|
|
||||||
|
const variableStyleTagId = 'nativewind-style';
|
||||||
|
const createStyle = (styleTagId: string) => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleTagId;
|
||||||
|
style.appendChild(document.createTextNode(''));
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSafeLayoutEffect =
|
||||||
|
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
|
export function GluestackUIProvider({
|
||||||
|
mode = 'light',
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
mode?: 'light' | 'dark' | 'system';
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
let cssVariablesWithMode = ``;
|
||||||
|
Object.keys(config).forEach((configKey) => {
|
||||||
|
cssVariablesWithMode +=
|
||||||
|
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
||||||
|
const cssVariables = Object.keys(
|
||||||
|
config[configKey as keyof typeof config]
|
||||||
|
).reduce((acc: string, curr: string) => {
|
||||||
|
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
||||||
|
return acc;
|
||||||
|
}, '');
|
||||||
|
cssVariablesWithMode += `${cssVariables} \n}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFlushStyles(cssVariablesWithMode);
|
||||||
|
|
||||||
|
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
||||||
|
script(e.matches ? 'dark' : 'light');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
documentElement.classList.add(mode);
|
||||||
|
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
||||||
|
documentElement.style.colorScheme = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') return;
|
||||||
|
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
media.addListener(handleMediaQuery);
|
||||||
|
|
||||||
|
return () => media.removeListener(handleMediaQuery);
|
||||||
|
}, [handleMediaQuery]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
const head = documentElement.querySelector('head');
|
||||||
|
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
||||||
|
if (!style) {
|
||||||
|
style = createStyle(variableStyleTagId);
|
||||||
|
style.innerHTML = cssVariablesWithMode;
|
||||||
|
if (head) head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayProvider>
|
||||||
|
<ToastProvider>{props.children}</ToastProvider>
|
||||||
|
</OverlayProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { config } from './config';
|
||||||
|
import { View, ViewProps } from 'react-native';
|
||||||
|
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||||
|
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||||
|
import { useColorScheme } from 'nativewind';
|
||||||
|
|
||||||
|
export type ModeType = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
export function GluestackUIProvider({
|
||||||
|
mode = 'light',
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
mode?: ModeType;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
style?: ViewProps['style'];
|
||||||
|
}) {
|
||||||
|
const { colorScheme, setColorScheme } = useColorScheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setColorScheme(mode);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
config[colorScheme!],
|
||||||
|
{ flex: 1, height: '100%', width: '100%' },
|
||||||
|
props.style,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<OverlayProvider>
|
||||||
|
<ToastProvider>{props.children}</ToastProvider>
|
||||||
|
</OverlayProvider>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useEffect, useLayoutEffect } from 'react';
|
||||||
|
import { config } from './config';
|
||||||
|
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||||
|
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||||
|
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { script } from './script';
|
||||||
|
|
||||||
|
export type ModeType = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
const variableStyleTagId = 'nativewind-style';
|
||||||
|
const createStyle = (styleTagId: string) => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleTagId;
|
||||||
|
style.appendChild(document.createTextNode(''));
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSafeLayoutEffect =
|
||||||
|
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
|
export function GluestackUIProvider({
|
||||||
|
mode = 'light',
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
mode?: ModeType;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
let cssVariablesWithMode = ``;
|
||||||
|
Object.keys(config).forEach((configKey) => {
|
||||||
|
cssVariablesWithMode +=
|
||||||
|
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
||||||
|
const cssVariables = Object.keys(
|
||||||
|
config[configKey as keyof typeof config]
|
||||||
|
).reduce((acc: string, curr: string) => {
|
||||||
|
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
||||||
|
return acc;
|
||||||
|
}, '');
|
||||||
|
cssVariablesWithMode += `${cssVariables} \n}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFlushStyles(cssVariablesWithMode);
|
||||||
|
|
||||||
|
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
||||||
|
script(e.matches ? 'dark' : 'light');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
documentElement.classList.add(mode);
|
||||||
|
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
||||||
|
documentElement.style.colorScheme = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (mode !== 'system') return;
|
||||||
|
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
media.addListener(handleMediaQuery);
|
||||||
|
|
||||||
|
return () => media.removeListener(handleMediaQuery);
|
||||||
|
}, [handleMediaQuery]);
|
||||||
|
|
||||||
|
useSafeLayoutEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
if (documentElement) {
|
||||||
|
const head = documentElement.querySelector('head');
|
||||||
|
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
||||||
|
if (!style) {
|
||||||
|
style = createStyle(variableStyleTagId);
|
||||||
|
style.innerHTML = cssVariablesWithMode;
|
||||||
|
if (head) head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
suppressHydrationWarning
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `(${script.toString()})('${mode}')`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<OverlayProvider>
|
||||||
|
<ToastProvider>{props.children}</ToastProvider>
|
||||||
|
</OverlayProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export const script = (mode: string) => {
|
||||||
|
const documentElement = document.documentElement;
|
||||||
|
|
||||||
|
function getSystemColorMode() {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isSystem = mode === 'system';
|
||||||
|
const theme = isSystem ? getSystemColorMode() : mode;
|
||||||
|
documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
|
||||||
|
documentElement.classList.add(theme);
|
||||||
|
documentElement.style.colorScheme = theme;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
276
components/ui/gluestack-ui-provider/modal/index.tsx
Normal file
276
components/ui/gluestack-ui-provider/modal/index.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createModal } from '@gluestack-ui/core/modal/creator';
|
||||||
|
import { Pressable, View, ScrollView, ViewStyle } from 'react-native';
|
||||||
|
import {
|
||||||
|
Motion,
|
||||||
|
AnimatePresence,
|
||||||
|
createMotionAnimatedComponent,
|
||||||
|
MotionComponentProps,
|
||||||
|
} from '@legendapp/motion';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
||||||
|
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const AnimatedPressable = createMotionAnimatedComponent(
|
||||||
|
Pressable
|
||||||
|
) as React.ComponentType<IAnimatedPressableProps>;
|
||||||
|
const SCOPE = 'MODAL';
|
||||||
|
|
||||||
|
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||||
|
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||||
|
|
||||||
|
const UIModal = createModal({
|
||||||
|
Root: withStyleContext(View, SCOPE),
|
||||||
|
Backdrop: AnimatedPressable,
|
||||||
|
Content: MotionView,
|
||||||
|
Body: ScrollView,
|
||||||
|
CloseButton: Pressable,
|
||||||
|
Footer: View,
|
||||||
|
Header: View,
|
||||||
|
AnimatePresence: AnimatePresence,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(AnimatedPressable, { className: 'style' });
|
||||||
|
cssInterop(MotionView, { className: 'style' });
|
||||||
|
|
||||||
|
const modalStyle = tva({
|
||||||
|
base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
xs: '',
|
||||||
|
sm: '',
|
||||||
|
md: '',
|
||||||
|
lg: '',
|
||||||
|
full: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalBackdropStyle = tva({
|
||||||
|
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalContentStyle = tva({
|
||||||
|
base: 'bg-background-0 rounded-md overflow-hidden border border-outline-100 shadow-hard-2 p-6',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
xs: 'w-[60%] max-w-[360px]',
|
||||||
|
sm: 'w-[70%] max-w-[420px]',
|
||||||
|
md: 'w-[80%] max-w-[510px]',
|
||||||
|
lg: 'w-[90%] max-w-[640px]',
|
||||||
|
full: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalBodyStyle = tva({
|
||||||
|
base: 'mt-2 mb-6',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalCloseButtonStyle = tva({
|
||||||
|
base: 'group/modal-close-button z-10 rounded data-[focus-visible=true]:web:bg-background-100 web:outline-0 cursor-pointer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalHeaderStyle = tva({
|
||||||
|
base: 'justify-between items-center flex-row',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalFooterStyle = tva({
|
||||||
|
base: 'flex-row justify-end items-center gap-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
type IModalProps = React.ComponentProps<typeof UIModal> &
|
||||||
|
VariantProps<typeof modalStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IModalBackdropProps = React.ComponentProps<typeof UIModal.Backdrop> &
|
||||||
|
VariantProps<typeof modalBackdropStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IModalContentProps = React.ComponentProps<typeof UIModal.Content> &
|
||||||
|
VariantProps<typeof modalContentStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IModalHeaderProps = React.ComponentProps<typeof UIModal.Header> &
|
||||||
|
VariantProps<typeof modalHeaderStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IModalBodyProps = React.ComponentProps<typeof UIModal.Body> &
|
||||||
|
VariantProps<typeof modalBodyStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IModalFooterProps = React.ComponentProps<typeof UIModal.Footer> &
|
||||||
|
VariantProps<typeof modalFooterStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IModalCloseButtonProps = React.ComponentProps<typeof UIModal.CloseButton> &
|
||||||
|
VariantProps<typeof modalCloseButtonStyle> & { className?: string };
|
||||||
|
|
||||||
|
const Modal = React.forwardRef<React.ComponentRef<typeof UIModal>, IModalProps>(
|
||||||
|
({ className, size = 'md', ...props }, ref) => (
|
||||||
|
<UIModal
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
pointerEvents="box-none"
|
||||||
|
className={modalStyle({ size, class: className })}
|
||||||
|
context={{ size }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalBackdrop = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIModal.Backdrop>,
|
||||||
|
IModalBackdropProps
|
||||||
|
>(function ModalBackdrop({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIModal.Backdrop
|
||||||
|
ref={ref}
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 250,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={modalBackdropStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIModal.Content>,
|
||||||
|
IModalContentProps
|
||||||
|
>(function ModalContent({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIModal.Content
|
||||||
|
ref={ref}
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.9,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 250,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={modalContentStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalHeader = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIModal.Header>,
|
||||||
|
IModalHeaderProps
|
||||||
|
>(function ModalHeader({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIModal.Header
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={modalHeaderStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalBody = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIModal.Body>,
|
||||||
|
IModalBodyProps
|
||||||
|
>(function ModalBody({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIModal.Body
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={modalBodyStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalFooter = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIModal.Footer>,
|
||||||
|
IModalFooterProps
|
||||||
|
>(function ModalFooter({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIModal.Footer
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={modalFooterStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalCloseButton = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIModal.CloseButton>,
|
||||||
|
IModalCloseButtonProps
|
||||||
|
>(function ModalCloseButton({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIModal.CloseButton
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={modalCloseButtonStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Modal.displayName = 'Modal';
|
||||||
|
ModalBackdrop.displayName = 'ModalBackdrop';
|
||||||
|
ModalContent.displayName = 'ModalContent';
|
||||||
|
ModalHeader.displayName = 'ModalHeader';
|
||||||
|
ModalBody.displayName = 'ModalBody';
|
||||||
|
ModalFooter.displayName = 'ModalFooter';
|
||||||
|
ModalCloseButton.displayName = 'ModalCloseButton';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Modal,
|
||||||
|
ModalBackdrop,
|
||||||
|
ModalContent,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
};
|
||||||
345
components/ui/gluestack-ui-provider/popover/index.tsx
Normal file
345
components/ui/gluestack-ui-provider/popover/index.tsx
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Pressable, ScrollView, ViewStyle } from 'react-native';
|
||||||
|
import {
|
||||||
|
Motion,
|
||||||
|
createMotionAnimatedComponent,
|
||||||
|
AnimatePresence,
|
||||||
|
MotionComponentProps,
|
||||||
|
} from '@legendapp/motion';
|
||||||
|
import { createPopover } from '@gluestack-ui/core/popover/creator';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
withStyleContext,
|
||||||
|
useStyleContext,
|
||||||
|
} from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
|
||||||
|
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
||||||
|
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const AnimatedPressable = createMotionAnimatedComponent(
|
||||||
|
Pressable
|
||||||
|
) as React.ComponentType<IAnimatedPressableProps>;
|
||||||
|
|
||||||
|
const SCOPE = 'POPOVER';
|
||||||
|
|
||||||
|
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||||
|
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||||
|
|
||||||
|
const UIPopover = createPopover({
|
||||||
|
Root: withStyleContext(View, SCOPE),
|
||||||
|
Arrow: MotionView,
|
||||||
|
Backdrop: AnimatedPressable,
|
||||||
|
Body: ScrollView,
|
||||||
|
CloseButton: Pressable,
|
||||||
|
Content: MotionView,
|
||||||
|
Footer: View,
|
||||||
|
Header: View,
|
||||||
|
AnimatePresence: AnimatePresence,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(MotionView, { className: 'style' });
|
||||||
|
cssInterop(AnimatedPressable, { className: 'style' });
|
||||||
|
|
||||||
|
const popoverStyle = tva({
|
||||||
|
base: 'group/popover w-full h-full justify-center items-center web:pointer-events-none',
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
xs: '',
|
||||||
|
sm: '',
|
||||||
|
md: '',
|
||||||
|
lg: '',
|
||||||
|
full: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const popoverArrowStyle = tva({
|
||||||
|
base: 'bg-background-0 z-[1] border absolute overflow-hidden h-3.5 w-3.5 border-outline-100',
|
||||||
|
variants: {
|
||||||
|
placement: {
|
||||||
|
'top left':
|
||||||
|
'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
|
||||||
|
'top':
|
||||||
|
'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
|
||||||
|
'top right':
|
||||||
|
'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
|
||||||
|
'bottom':
|
||||||
|
'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
|
||||||
|
'bottom left':
|
||||||
|
'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
|
||||||
|
'bottom right':
|
||||||
|
'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
|
||||||
|
'left':
|
||||||
|
'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
|
||||||
|
'left top':
|
||||||
|
'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
|
||||||
|
'left bottom':
|
||||||
|
'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
|
||||||
|
'right':
|
||||||
|
'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
|
||||||
|
'right top':
|
||||||
|
'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
|
||||||
|
'right bottom':
|
||||||
|
'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const popoverBackdropStyle = tva({
|
||||||
|
base: 'absolute left-0 top-0 right-0 bottom-0 web:cursor-default',
|
||||||
|
});
|
||||||
|
|
||||||
|
const popoverCloseButtonStyle = tva({
|
||||||
|
base: 'group/popover-close-button z-[1] rounded-sm data-[focus-visible=true]:web:bg-background-100 web:outline-0 web:cursor-pointer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const popoverContentStyle = tva({
|
||||||
|
base: 'bg-background-0 rounded-lg overflow-hidden border border-outline-100 w-full',
|
||||||
|
parentVariants: {
|
||||||
|
size: {
|
||||||
|
xs: 'max-w-[360px] p-3.5',
|
||||||
|
sm: 'max-w-[420px] p-4',
|
||||||
|
md: 'max-w-[510px] p-[18px]',
|
||||||
|
lg: 'max-w-[640px] p-5',
|
||||||
|
full: 'p-6',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const popoverHeaderStyle = tva({
|
||||||
|
base: 'flex-row justify-between items-center',
|
||||||
|
});
|
||||||
|
|
||||||
|
const popoverBodyStyle = tva({
|
||||||
|
base: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const popoverFooterStyle = tva({
|
||||||
|
base: 'flex-row justify-between items-center',
|
||||||
|
});
|
||||||
|
|
||||||
|
type IPopoverProps = React.ComponentProps<typeof UIPopover> &
|
||||||
|
VariantProps<typeof popoverStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IPopoverArrowProps = React.ComponentProps<typeof UIPopover.Arrow> &
|
||||||
|
VariantProps<typeof popoverArrowStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IPopoverContentProps = React.ComponentProps<typeof UIPopover.Content> &
|
||||||
|
VariantProps<typeof popoverContentStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IPopoverHeaderProps = React.ComponentProps<typeof UIPopover.Header> &
|
||||||
|
VariantProps<typeof popoverHeaderStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IPopoverFooterProps = React.ComponentProps<typeof UIPopover.Footer> &
|
||||||
|
VariantProps<typeof popoverFooterStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IPopoverBodyProps = React.ComponentProps<typeof UIPopover.Body> &
|
||||||
|
VariantProps<typeof popoverBodyStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IPopoverBackdropProps = React.ComponentProps<typeof UIPopover.Backdrop> &
|
||||||
|
VariantProps<typeof popoverBackdropStyle> & { className?: string };
|
||||||
|
|
||||||
|
type IPopoverCloseButtonProps = React.ComponentProps<
|
||||||
|
typeof UIPopover.CloseButton
|
||||||
|
> &
|
||||||
|
VariantProps<typeof popoverCloseButtonStyle> & { className?: string };
|
||||||
|
|
||||||
|
const Popover = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover>,
|
||||||
|
IPopoverProps
|
||||||
|
>(function Popover(
|
||||||
|
{ className, size = 'md', placement = 'bottom', ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<UIPopover
|
||||||
|
ref={ref}
|
||||||
|
placement={placement}
|
||||||
|
{...props}
|
||||||
|
className={popoverStyle({ size, class: className })}
|
||||||
|
context={{ size, placement }}
|
||||||
|
pointerEvents="box-none"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover.Content>,
|
||||||
|
IPopoverContentProps
|
||||||
|
>(function PopoverContent({ className, size, ...props }, ref) {
|
||||||
|
const { size: parentSize } = useStyleContext(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIPopover.Content
|
||||||
|
ref={ref}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
mass: 0.9,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 50,
|
||||||
|
delay: 50,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={popoverContentStyle({
|
||||||
|
parentVariants: {
|
||||||
|
size: parentSize,
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PopoverArrow = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover.Arrow>,
|
||||||
|
IPopoverArrowProps
|
||||||
|
>(function PopoverArrow({ className, ...props }, ref) {
|
||||||
|
const { placement } = useStyleContext(SCOPE);
|
||||||
|
return (
|
||||||
|
<UIPopover.Arrow
|
||||||
|
ref={ref}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 250,
|
||||||
|
mass: 0.9,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 50,
|
||||||
|
delay: 50,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
className={popoverArrowStyle({
|
||||||
|
class: className,
|
||||||
|
placement,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PopoverBackdrop = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover.Backdrop>,
|
||||||
|
IPopoverBackdropProps
|
||||||
|
>(function PopoverBackdrop({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIPopover.Backdrop
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 0.1,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 18,
|
||||||
|
stiffness: 450,
|
||||||
|
mass: 0.9,
|
||||||
|
opacity: {
|
||||||
|
type: 'timing',
|
||||||
|
duration: 50,
|
||||||
|
delay: 50,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className={popoverBackdropStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PopoverBody = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover.Body>,
|
||||||
|
IPopoverBodyProps
|
||||||
|
>(function PopoverBody({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIPopover.Body
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={popoverBodyStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PopoverCloseButton = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover.CloseButton>,
|
||||||
|
IPopoverCloseButtonProps
|
||||||
|
>(function PopoverCloseButton({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIPopover.CloseButton
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={popoverCloseButtonStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PopoverFooter = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover.Footer>,
|
||||||
|
IPopoverFooterProps
|
||||||
|
>(function PopoverFooter({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIPopover.Footer
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={popoverFooterStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PopoverHeader = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UIPopover.Header>,
|
||||||
|
IPopoverHeaderProps
|
||||||
|
>(function PopoverHeader({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UIPopover.Header
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={popoverHeaderStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Popover.displayName = 'Popover';
|
||||||
|
PopoverArrow.displayName = 'PopoverArrow';
|
||||||
|
PopoverBackdrop.displayName = 'PopoverBackdrop';
|
||||||
|
PopoverContent.displayName = 'PopoverContent';
|
||||||
|
PopoverHeader.displayName = 'PopoverHeader';
|
||||||
|
PopoverFooter.displayName = 'PopoverFooter';
|
||||||
|
PopoverBody.displayName = 'PopoverBody';
|
||||||
|
PopoverCloseButton.displayName = 'PopoverCloseButton';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Popover,
|
||||||
|
PopoverBackdrop,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverCloseButton,
|
||||||
|
PopoverFooter,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
};
|
||||||
131
components/ui/gluestack-ui-provider/tooltip/index.tsx
Normal file
131
components/ui/gluestack-ui-provider/tooltip/index.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createTooltip } from '@gluestack-ui/core/tooltip/creator';
|
||||||
|
import { View, Text, ViewStyle } from 'react-native';
|
||||||
|
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils';
|
||||||
|
import {
|
||||||
|
Motion,
|
||||||
|
AnimatePresence,
|
||||||
|
MotionComponentProps,
|
||||||
|
} from '@legendapp/motion';
|
||||||
|
import { cssInterop } from 'nativewind';
|
||||||
|
|
||||||
|
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||||
|
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||||
|
|
||||||
|
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||||
|
|
||||||
|
export const UITooltip = createTooltip({
|
||||||
|
Root: withStyleContext(View),
|
||||||
|
Content: MotionView,
|
||||||
|
Text: Text,
|
||||||
|
AnimatePresence: AnimatePresence,
|
||||||
|
});
|
||||||
|
|
||||||
|
cssInterop(MotionView, { className: 'style' });
|
||||||
|
|
||||||
|
const tooltipStyle = tva({
|
||||||
|
base: 'w-full h-full web:pointer-events-none',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipContentStyle = tva({
|
||||||
|
base: 'py-1 px-3 rounded-sm bg-background-900 web:pointer-events-auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipTextStyle = tva({
|
||||||
|
base: 'font-normal tracking-normal web:select-none text-xs text-typography-50',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isTruncated: {
|
||||||
|
true: 'line-clamp-1 truncate',
|
||||||
|
},
|
||||||
|
bold: {
|
||||||
|
true: 'font-bold',
|
||||||
|
},
|
||||||
|
underline: {
|
||||||
|
true: 'underline',
|
||||||
|
},
|
||||||
|
strikeThrough: {
|
||||||
|
true: 'line-through',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
'2xs': 'text-2xs',
|
||||||
|
'xs': 'text-xs',
|
||||||
|
'sm': 'text-sm',
|
||||||
|
'md': 'text-base',
|
||||||
|
'lg': 'text-lg',
|
||||||
|
'xl': 'text-xl',
|
||||||
|
'2xl': 'text-2xl',
|
||||||
|
'3xl': 'text-3xl',
|
||||||
|
'4xl': 'text-4xl',
|
||||||
|
'5xl': 'text-5xl',
|
||||||
|
'6xl': 'text-6xl',
|
||||||
|
},
|
||||||
|
sub: {
|
||||||
|
true: 'text-xs',
|
||||||
|
},
|
||||||
|
italic: {
|
||||||
|
true: 'italic',
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
true: 'bg-yellow-500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ITooltipProps = React.ComponentProps<typeof UITooltip> &
|
||||||
|
VariantProps<typeof tooltipStyle> & { className?: string };
|
||||||
|
type ITooltipContentProps = React.ComponentProps<typeof UITooltip.Content> &
|
||||||
|
VariantProps<typeof tooltipContentStyle> & { className?: string };
|
||||||
|
type ITooltipTextProps = React.ComponentProps<typeof UITooltip.Text> &
|
||||||
|
VariantProps<typeof tooltipTextStyle> & { className?: string };
|
||||||
|
|
||||||
|
const Tooltip = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UITooltip>,
|
||||||
|
ITooltipProps
|
||||||
|
>(function Tooltip({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UITooltip
|
||||||
|
ref={ref}
|
||||||
|
className={tooltipStyle({ class: className })}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UITooltip.Content>,
|
||||||
|
ITooltipContentProps & { className?: string }
|
||||||
|
>(function TooltipContent({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UITooltip.Content
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={tooltipContentStyle({
|
||||||
|
class: className,
|
||||||
|
})}
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const TooltipText = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof UITooltip.Text>,
|
||||||
|
ITooltipTextProps & { className?: string }
|
||||||
|
>(function TooltipText({ size, className, ...props }, ref) {
|
||||||
|
return (
|
||||||
|
<UITooltip.Text
|
||||||
|
ref={ref}
|
||||||
|
className={tooltipTextStyle({ size, class: className })}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Tooltip.displayName = 'Tooltip';
|
||||||
|
TooltipContent.displayName = 'TooltipContent';
|
||||||
|
TooltipText.displayName = 'TooltipText';
|
||||||
|
|
||||||
|
export { Tooltip, TooltipContent, TooltipText };
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TOKEN } from "@/constants";
|
||||||
|
import { removeStorageItem } from "@/utils/storage";
|
||||||
import { Router } from "expo-router";
|
import { Router } from "expo-router";
|
||||||
|
|
||||||
let routerInstance: Router | null = null;
|
let routerInstance: Router | null = null;
|
||||||
@@ -14,6 +16,7 @@ export const setRouterInstance = (router: Router) => {
|
|||||||
*/
|
*/
|
||||||
export const handle401 = () => {
|
export const handle401 = () => {
|
||||||
if (routerInstance) {
|
if (routerInstance) {
|
||||||
|
removeStorageItem(TOKEN);
|
||||||
(routerInstance as any).replace("/login");
|
(routerInstance as any).replace("/login");
|
||||||
} else {
|
} else {
|
||||||
console.warn("Router instance not set, cannot redirect to login");
|
console.warn("Router instance not set, cannot redirect to login");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TOKEN } from "@/constants";
|
import { TOKEN } from "@/constants";
|
||||||
import { getStorageItem } from "@/utils/storage";
|
import { getStorageItem } from "@/utils/storage";
|
||||||
import axios, { AxiosInstance } from "axios";
|
import axios, { AxiosInstance } from "axios";
|
||||||
|
import { handle401 } from "./auth";
|
||||||
import { showToastError } from "./toast";
|
import { showToastError } from "./toast";
|
||||||
|
|
||||||
const codeMessage = {
|
const codeMessage = {
|
||||||
@@ -72,7 +73,7 @@ api.interceptors.response.use(
|
|||||||
showToastError(`Lỗi ${status}`, errMsg);
|
showToastError(`Lỗi ${status}`, errMsg);
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
// handle401();
|
handle401();
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
API_GET_GPS,
|
API_GET_GPS,
|
||||||
API_PATH_ENTITIES,
|
API_PATH_ENTITIES,
|
||||||
API_PATH_SHIP_TRACK_POINTS,
|
API_PATH_SHIP_TRACK_POINTS,
|
||||||
|
API_SOS,
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
import { transformEntityResponse } from "@/utils/tranform";
|
import { transformEntityResponse } from "@/utils/tranform";
|
||||||
|
|
||||||
@@ -23,3 +24,14 @@ export async function queryEntities(): Promise<Model.TransformedEntity[]> {
|
|||||||
const response = await api.get<Model.EntityResponse[]>(API_PATH_ENTITIES);
|
const response = await api.get<Model.EntityResponse[]>(API_PATH_ENTITIES);
|
||||||
return response.data.map(transformEntityResponse);
|
return response.data.map(transformEntityResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queryGetSos() {
|
||||||
|
return await api.get<Model.SosResponse>(API_SOS);
|
||||||
|
}
|
||||||
|
export async function queryDeleteSos() {
|
||||||
|
return await api.delete<Model.SosResponse>(API_SOS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function querySendSosMessage(message: string) {
|
||||||
|
return await api.put<Model.SosRequest>(API_SOS, { message });
|
||||||
|
}
|
||||||
|
|||||||
10
controller/typings.d.ts
vendored
10
controller/typings.d.ts
vendored
@@ -80,4 +80,14 @@ declare namespace Model {
|
|||||||
geom_point?: string;
|
geom_point?: string;
|
||||||
geom_radius?: number;
|
geom_radius?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SosRequest {
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SosResponse {
|
||||||
|
active: boolean;
|
||||||
|
message?: string;
|
||||||
|
started_at?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { getDefaultConfig } = require("expo/metro-config");
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
const { withNativeWind } = require("nativewind/metro");
|
const { withNativeWind } = require('nativewind/metro');
|
||||||
|
|
||||||
const config = getDefaultConfig(__dirname);
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
module.exports = withNativeWind(config, { input: "./global.css" });
|
module.exports = withNativeWind(config, { input: './global.css' });
|
||||||
|
|||||||
2381
package-lock.json
generated
2381
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -11,12 +11,18 @@
|
|||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/html-elements": "^0.10.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
"@gluestack-ui/core": "^3.0.12",
|
||||||
|
"@gluestack-ui/utils": "^3.0.11",
|
||||||
|
"@legendapp/motion": "^2.5.3",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "~54.0.20",
|
"expo": "~54.0.20",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
@@ -32,17 +38,21 @@
|
|||||||
"expo-web-browser": "~15.0.8",
|
"expo-web-browser": "~15.0.8",
|
||||||
"nativewind": "^4.2.1",
|
"nativewind": "^4.2.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-aria": "^3.44.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-keyboard-aware-scroll-view": "^0.9.5",
|
"react-native-keyboard-aware-scroll-view": "^0.9.5",
|
||||||
"react-native-maps": "^1.20.1",
|
"react-native-maps": "^1.20.1",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.0",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "^5.6.2",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
"react-native-svg": "^15.14.0",
|
||||||
"react-native-toast-message": "^2.3.3",
|
"react-native-toast-message": "^2.3.3",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.5.1",
|
"react-native-worklets": "^0.5.2",
|
||||||
|
"react-stately": "^3.42.0",
|
||||||
|
"tailwind-variants": "^0.1.20",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -50,7 +60,7 @@
|
|||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
"eslint-config-expo": "~10.0.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.18",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ const intervals: {
|
|||||||
|
|
||||||
export function getGpsEventBus() {
|
export function getGpsEventBus() {
|
||||||
if (intervals.gps) return;
|
if (intervals.gps) return;
|
||||||
console.log("Starting GPS poller");
|
// console.log("Starting GPS poller");
|
||||||
|
|
||||||
const getGpsData = async () => {
|
const getGpsData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("GPS: fetching data...");
|
// console.log("GPS: fetching data...");
|
||||||
const resp = await queryGpsData();
|
const resp = await queryGpsData();
|
||||||
if (resp && resp.data) {
|
if (resp && resp.data) {
|
||||||
console.log("GPS: emitting data", resp.data);
|
// console.log("GPS: emitting data", resp.data);
|
||||||
eventBus.emit(EVENT_GPS_DATA, resp.data);
|
eventBus.emit(EVENT_GPS_DATA, resp.data);
|
||||||
} else {
|
} else {
|
||||||
console.log("GPS: no data returned");
|
console.log("GPS: no data returned");
|
||||||
@@ -57,16 +57,16 @@ export function getGpsEventBus() {
|
|||||||
|
|
||||||
export function getAlarmEventBus() {
|
export function getAlarmEventBus() {
|
||||||
if (intervals.alarm) return;
|
if (intervals.alarm) return;
|
||||||
console.log("Goi ham get Alarm");
|
// console.log("Goi ham get Alarm");
|
||||||
const getAlarmData = async () => {
|
const getAlarmData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Alarm: fetching data...");
|
// console.log("Alarm: fetching data...");
|
||||||
const resp = await queryAlarm();
|
const resp = await queryAlarm();
|
||||||
if (resp && resp.data) {
|
if (resp && resp.data) {
|
||||||
console.log(
|
// console.log(
|
||||||
"Alarm: emitting data",
|
// "Alarm: emitting data",
|
||||||
resp.data?.alarms?.length ?? resp.data
|
// resp.data?.alarms?.length ?? resp.data
|
||||||
);
|
// );
|
||||||
eventBus.emit(EVENT_ALARM_DATA, resp.data);
|
eventBus.emit(EVENT_ALARM_DATA, resp.data);
|
||||||
} else {
|
} else {
|
||||||
console.log("Alarm: no data returned");
|
console.log("Alarm: no data returned");
|
||||||
@@ -84,13 +84,13 @@ export function getAlarmEventBus() {
|
|||||||
|
|
||||||
export function getEntitiesEventBus() {
|
export function getEntitiesEventBus() {
|
||||||
if (intervals.entities) return;
|
if (intervals.entities) return;
|
||||||
console.log("Goi ham get Entities");
|
// console.log("Goi ham get Entities");
|
||||||
const getEntitiesData = async () => {
|
const getEntitiesData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Entities: fetching data...");
|
// console.log("Entities: fetching data...");
|
||||||
const resp = await queryEntities();
|
const resp = await queryEntities();
|
||||||
if (resp && resp.length > 0) {
|
if (resp && resp.length > 0) {
|
||||||
console.log("Entities: emitting", resp.length);
|
// console.log("Entities: emitting", resp.length);
|
||||||
eventBus.emit(EVENT_ENTITY_DATA, resp);
|
eventBus.emit(EVENT_ENTITY_DATA, resp);
|
||||||
} else {
|
} else {
|
||||||
console.log("Entities: no data returned");
|
console.log("Entities: no data returned");
|
||||||
@@ -108,13 +108,13 @@ export function getEntitiesEventBus() {
|
|||||||
|
|
||||||
export function getTrackPointsEventBus() {
|
export function getTrackPointsEventBus() {
|
||||||
if (intervals.trackPoints) return;
|
if (intervals.trackPoints) return;
|
||||||
console.log("Goi ham get Track Points");
|
// console.log("Goi ham get Track Points");
|
||||||
const getTrackPointsData = async () => {
|
const getTrackPointsData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("TrackPoints: fetching data...");
|
// console.log("TrackPoints: fetching data...");
|
||||||
const resp = await queryTrackPoints();
|
const resp = await queryTrackPoints();
|
||||||
if (resp && resp.data && resp.data.length > 0) {
|
if (resp && resp.data && resp.data.length > 0) {
|
||||||
console.log("TrackPoints: emitting", resp.data.length);
|
// console.log("TrackPoints: emitting", resp.data.length);
|
||||||
eventBus.emit(EVENT_TRACK_POINTS_DATA, resp.data);
|
eventBus.emit(EVENT_TRACK_POINTS_DATA, resp.data);
|
||||||
} else {
|
} else {
|
||||||
console.log("TrackPoints: no data returned");
|
console.log("TrackPoints: no data returned");
|
||||||
@@ -134,10 +134,10 @@ export function getBanzonesEventBus() {
|
|||||||
if (intervals.banzones) return;
|
if (intervals.banzones) return;
|
||||||
const getBanzonesData = async () => {
|
const getBanzonesData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Banzones: fetching data...");
|
// console.log("Banzones: fetching data...");
|
||||||
const resp = await queryBanzones();
|
const resp = await queryBanzones();
|
||||||
if (resp && resp.data && resp.data.length > 0) {
|
if (resp && resp.data && resp.data.length > 0) {
|
||||||
console.log("Banzones: emitting", resp.data.length);
|
// console.log("Banzones: emitting", resp.data.length);
|
||||||
eventBus.emit(EVENT_BANZONE_DATA, resp.data);
|
eventBus.emit(EVENT_BANZONE_DATA, resp.data);
|
||||||
} else {
|
} else {
|
||||||
console.log("Banzones: no data returned");
|
console.log("Banzones: no data returned");
|
||||||
|
|||||||
@@ -1,13 +1,206 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
darkMode: process.env.DARK_MODE ? process.env.DARK_MODE : 'class',
|
||||||
content: [
|
content: [
|
||||||
"./app/**/*.{js,jsx,ts,tsx}",
|
'./app/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
"./components/**/*.{js,jsx,ts,tsx}",
|
'./components/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
"./screens/**/*.{js,jsx,ts,tsx}",
|
'./utils/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
'./*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
'./src/**/*.{html,js,jsx,ts,tsx,mdx}',
|
||||||
|
],
|
||||||
|
presets: [require('nativewind/preset')],
|
||||||
|
important: 'html',
|
||||||
|
safelist: [
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/(bg|border|text|stroke|fill)-(primary|secondary|tertiary|error|success|warning|info|typography|outline|background|indicator)-(0|50|100|200|300|400|500|600|700|800|900|950|white|gray|black|error|warning|muted|success|info|light|dark|primary)/,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
presets: [require("nativewind/preset")],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
0: 'rgb(var(--color-primary-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-primary-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-primary-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-primary-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-primary-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-primary-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-primary-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-primary-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-primary-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-primary-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-primary-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-primary-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
0: 'rgb(var(--color-secondary-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-secondary-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-secondary-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-secondary-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-secondary-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-secondary-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-secondary-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-secondary-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-secondary-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-secondary-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-secondary-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-secondary-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
tertiary: {
|
||||||
|
50: 'rgb(var(--color-tertiary-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-tertiary-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-tertiary-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-tertiary-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-tertiary-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-tertiary-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-tertiary-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-tertiary-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-tertiary-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-tertiary-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-tertiary-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
0: 'rgb(var(--color-error-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-error-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-error-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-error-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-error-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-error-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-error-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-error-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-error-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-error-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-error-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-error-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
0: 'rgb(var(--color-success-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-success-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-success-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-success-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-success-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-success-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-success-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-success-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-success-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-success-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-success-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-success-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
0: 'rgb(var(--color-warning-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-warning-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-warning-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-warning-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-warning-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-warning-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-warning-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-warning-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-warning-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-warning-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-warning-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-warning-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
0: 'rgb(var(--color-info-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-info-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-info-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-info-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-info-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-info-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-info-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-info-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-info-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-info-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-info-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-info-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
0: 'rgb(var(--color-typography-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-typography-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-typography-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-typography-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-typography-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-typography-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-typography-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-typography-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-typography-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-typography-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-typography-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-typography-950)/<alpha-value>)',
|
||||||
|
white: '#FFFFFF',
|
||||||
|
gray: '#D4D4D4',
|
||||||
|
black: '#181718',
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
0: 'rgb(var(--color-outline-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-outline-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-outline-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-outline-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-outline-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-outline-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-outline-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-outline-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-outline-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-outline-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-outline-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-outline-950)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
0: 'rgb(var(--color-background-0)/<alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-background-50)/<alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-background-100)/<alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-background-200)/<alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-background-300)/<alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-background-400)/<alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-background-500)/<alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-background-600)/<alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-background-700)/<alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-background-800)/<alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-background-900)/<alpha-value>)',
|
||||||
|
950: 'rgb(var(--color-background-950)/<alpha-value>)',
|
||||||
|
error: 'rgb(var(--color-background-error)/<alpha-value>)',
|
||||||
|
warning: 'rgb(var(--color-background-warning)/<alpha-value>)',
|
||||||
|
muted: 'rgb(var(--color-background-muted)/<alpha-value>)',
|
||||||
|
success: 'rgb(var(--color-background-success)/<alpha-value>)',
|
||||||
|
info: 'rgb(var(--color-background-info)/<alpha-value>)',
|
||||||
|
light: '#FBFBFB',
|
||||||
|
dark: '#181719',
|
||||||
|
},
|
||||||
|
indicator: {
|
||||||
|
primary: 'rgb(var(--color-indicator-primary)/<alpha-value>)',
|
||||||
|
info: 'rgb(var(--color-indicator-info)/<alpha-value>)',
|
||||||
|
error: 'rgb(var(--color-indicator-error)/<alpha-value>)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
heading: undefined,
|
||||||
|
body: undefined,
|
||||||
|
mono: undefined,
|
||||||
|
jakarta: ['var(--font-plus-jakarta-sans)'],
|
||||||
|
roboto: ['var(--font-roboto)'],
|
||||||
|
code: ['var(--font-source-code-pro)'],
|
||||||
|
inter: ['var(--font-inter)'],
|
||||||
|
'space-mono': ['var(--font-space-mono)'],
|
||||||
|
},
|
||||||
|
fontWeight: {
|
||||||
|
extrablack: '950',
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
'2xs': '10px',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'hard-1': '-2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-2': '0px 3px 10px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-3': '2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-4': '0px -3px 10px 0px rgba(38, 38, 38, 0.20)',
|
||||||
|
'hard-5': '0px 2px 10px 0px rgba(38, 38, 38, 0.10)',
|
||||||
|
'soft-1': '0px 0px 10px rgba(38, 38, 38, 0.1)',
|
||||||
|
'soft-2': '0px 0px 20px rgba(38, 38, 38, 0.2)',
|
||||||
|
'soft-3': '0px 0px 30px rgba(38, 38, 38, 0.1)',
|
||||||
|
'soft-4': '0px 0px 40px rgba(38, 38, 38, 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./*"
|
"./*"
|
||||||
|
],
|
||||||
|
"tailwind.config": [
|
||||||
|
"./tailwind.config.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -86,3 +86,25 @@ export const getBanzoneNameByType = (type: number) => {
|
|||||||
return "Chưa có";
|
return "Chưa có";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertToDMS = (value: number, isLat: boolean): string => {
|
||||||
|
const deg = Math.floor(Math.abs(value));
|
||||||
|
const minFloat = (Math.abs(value) - deg) * 60;
|
||||||
|
const min = Math.floor(minFloat);
|
||||||
|
const sec = (minFloat - min) * 60;
|
||||||
|
|
||||||
|
const direction = value >= 0 ? (isLat ? "N" : "E") : isLat ? "S" : "W";
|
||||||
|
|
||||||
|
return `${deg}°${min}'${sec.toFixed(2)}"${direction}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chuyển đổi tốc độ từ km/h sang knot (hải lý/giờ)
|
||||||
|
* @param kmh - tốc độ tính bằng km/h
|
||||||
|
* @returns tốc độ tính bằng knot
|
||||||
|
*/
|
||||||
|
export function kmhToKnot(kmh: number): number {
|
||||||
|
const KNOT_PER_KMH = 1 / 1.852; // 1 knot = 1.852 km/h
|
||||||
|
return parseFloat((kmh * KNOT_PER_KMH).toFixed(2)); // làm tròn 2 chữ số thập phân
|
||||||
|
}
|
||||||
|
|||||||
91
utils/sosUtils.ts
Normal file
91
utils/sosUtils.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Định nghĩa cấu trúc cho mỗi lý do cần hỗ trợ/SOS
|
||||||
|
*/
|
||||||
|
interface SosMessage {
|
||||||
|
ma: number; // Mã số thứ tự của lý do
|
||||||
|
moTa: string; // Mô tả ngắn gọn về sự cố
|
||||||
|
mucDoNghiemTrong: string;
|
||||||
|
chiTiet: string; // Chi tiết sự cố
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mảng 10 lý do phát tín hiệu SOS/Yêu cầu trợ giúp trên biển
|
||||||
|
* Sắp xếp từ nhẹ (yêu cầu hỗ trợ) đến nặng (SOS khẩn cấp)
|
||||||
|
*/
|
||||||
|
export const sosMessage: SosMessage[] = [
|
||||||
|
{
|
||||||
|
ma: 11,
|
||||||
|
moTa: "Tình huống khẩn cấp, không kịp chọn !!!",
|
||||||
|
mucDoNghiemTrong: "Nguy Hiem Can Ke (SOS)",
|
||||||
|
chiTiet:
|
||||||
|
"Tình huống nghiêm trọng nhất, đe dọa trực tiếp đến tính mạng tất cả người trên tàu.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 1,
|
||||||
|
moTa: "Hỏng hóc động cơ không tự khắc phục được",
|
||||||
|
mucDoNghiemTrong: "Nhe",
|
||||||
|
chiTiet: "Tàu bị trôi hoặc mắc cạn nhẹ; cần tàu lai hoặc thợ máy.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 2,
|
||||||
|
moTa: "Thiếu nhiên liệu/thực phẩm/nước uống nghiêm trọng",
|
||||||
|
mucDoNghiemTrong: "Nhe",
|
||||||
|
chiTiet:
|
||||||
|
"Dự trữ thiết yếu cạn kiệt do hành trình kéo dài không lường trước được.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 3,
|
||||||
|
moTa: "Sự cố y tế không nguy hiểm đến tính mạng",
|
||||||
|
mucDoNghiemTrong: "Trung Binh",
|
||||||
|
chiTiet:
|
||||||
|
"Cần chăm sóc y tế chuyên nghiệp khẩn cấp (ví dụ: gãy xương, viêm ruột thừa).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 4,
|
||||||
|
moTa: "Hỏng hóc thiết bị định vị/thông tin liên lạc chính",
|
||||||
|
mucDoNghiemTrong: "Trung Binh",
|
||||||
|
chiTiet: "Mất khả năng xác định vị trí hoặc liên lạc, tăng rủi ro bị lạc.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 5,
|
||||||
|
moTa: "Thời tiết cực đoan sắp tới không kịp trú ẩn",
|
||||||
|
mucDoNghiemTrong: "Trung Binh",
|
||||||
|
chiTiet:
|
||||||
|
"Tàu không kịp chạy vào nơi trú ẩn an toàn trước cơn bão lớn hoặc gió giật mạnh.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 6,
|
||||||
|
moTa: "Va chạm gây hư hỏng cấu trúc",
|
||||||
|
mucDoNghiemTrong: "Nang",
|
||||||
|
chiTiet:
|
||||||
|
"Tàu bị hư hại một phần do va chạm, cần kiểm tra và hỗ trợ lai dắt khẩn cấp.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 7,
|
||||||
|
moTa: "Có cháy/hỏa hoạn trên tàu không kiểm soát được",
|
||||||
|
mucDoNghiemTrong: "Nang",
|
||||||
|
chiTiet:
|
||||||
|
"Lửa bùng phát vượt quá khả năng chữa cháy của tàu, nguy cơ cháy lan.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 8,
|
||||||
|
moTa: "Tàu bị thủng/nước vào không kiểm soát được",
|
||||||
|
mucDoNghiemTrong: "Rat Nang",
|
||||||
|
chiTiet:
|
||||||
|
"Nước tràn vào khoang quá nhanh, vượt quá khả năng bơm tát, đe dọa tàu chìm.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 9,
|
||||||
|
moTa: "Sự cố y tế nguy hiểm đến tính mạng (MEDEVAC)",
|
||||||
|
mucDoNghiemTrong: "Rat Nang",
|
||||||
|
chiTiet:
|
||||||
|
"Thương tích/bệnh tật nghiêm trọng, cần sơ tán y tế (MEDEVAC) ngay lập tức bằng trực thăng/tàu cứu hộ.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ma: 10,
|
||||||
|
moTa: "Tàu bị chìm/lật úp hoàn toàn hoặc sắp xảy ra",
|
||||||
|
mucDoNghiemTrong: "Nguy Hiem Can Ke (SOS)",
|
||||||
|
chiTiet:
|
||||||
|
"Tình huống nghiêm trọng nhất, đe dọa trực tiếp đến tính mạng tất cả người trên tàu.",
|
||||||
|
},
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user