Some checks failed
Build Flutter Web and Docker Image for Local Registry / Build React Native Web App (push) Failing after 4m49s
261 lines
11 KiB
TypeScript
261 lines
11 KiB
TypeScript
import React, { useEffect, useState, useRef } from "react";
|
|
import useWebSocket from "react-use-websocket";
|
|
import axios from "axios";
|
|
import { Animated, Easing, ImageBackground, StyleSheet, View } from "react-native";
|
|
import { Avatar, List, Button, useTheme, } from "react-native-paper";
|
|
|
|
export const API_URL = process.env.EXPO_PUBLIC_API_URL;
|
|
export const WS_URL = process.env.EXPO_PUBLIC_WS_URL;
|
|
|
|
interface Message {
|
|
Id: string;
|
|
Name: string;
|
|
Image: string;
|
|
Status: string;
|
|
Timestamp: string;
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, alignItems: "stretch" },
|
|
listContainer: { flex: 1, width: "100%", backgroundColor: 'transparent' },
|
|
listSubheader: {
|
|
fontSize: 18, // Larger text
|
|
textAlign: "center", // Center the text
|
|
fontWeight: "bold", // Make it more distinct
|
|
marginBottom: 10, // Add spacing below
|
|
zIndex: 0,
|
|
},
|
|
listWrapper: { flex: 1, padding: 5 },
|
|
listRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "flex-start", // Aligns subheaders properly
|
|
paddingHorizontal: 10, // Adds some spacing
|
|
},
|
|
listColumn: { flex: 1, paddingHorizontal: 5, zIndex: 1},
|
|
buttonContainer: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignSelf: "stretch",
|
|
paddingHorizontal: 10,
|
|
paddingBottom: 20,
|
|
},
|
|
actionButton: {
|
|
flex: 1,
|
|
marginHorizontal: 5,
|
|
},
|
|
card: {
|
|
marginVertical: 5,
|
|
elevation: 4, // Android shadow
|
|
shadowColor: "#000", // iOS shadow
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.2,
|
|
shadowRadius: 4,
|
|
borderRadius: 10,
|
|
},
|
|
imageBackground: {
|
|
position: "absolute", // Allows child elements to layer on top
|
|
width: "100%", // Ensure full coverage of the column
|
|
height: "100%", // Fully stretches to column height
|
|
resizeMode: "cover", // Ensures it fits well
|
|
opacity: 0.2, // Fades the image
|
|
zIndex: -1,
|
|
},
|
|
});
|
|
|
|
interface StatusProps {
|
|
id: string;
|
|
name: string;
|
|
image: string;
|
|
currentStatus: string;
|
|
setStatus: (currentStatus: string) => void;
|
|
isProfileActive: boolean;
|
|
}
|
|
|
|
const StatusPage: React.FC<StatusProps> = ({ id, name, image, currentStatus, setStatus, isProfileActive }) => {
|
|
const theme = useTheme();
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const { lastMessage } = useWebSocket(WS_URL + "/ws", {
|
|
shouldReconnect: () => true,
|
|
});
|
|
|
|
// Animated values for background color pulsing
|
|
const pulseAnimOnTheWay = useRef(new Animated.Value(0)).current;
|
|
const pulseAnimArrived = useRef(new Animated.Value(0)).current;
|
|
|
|
// Function to trigger the color pulsing animation
|
|
const startPulsing = (animationRef: Animated.Value) => {
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(animationRef, {
|
|
toValue: 1,
|
|
duration: 800,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: false,
|
|
}),
|
|
Animated.timing(animationRef, {
|
|
toValue: 0,
|
|
duration: 800,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: false,
|
|
}),
|
|
])
|
|
).start();
|
|
};
|
|
|
|
const stopPulsing = (animationRef: Animated.Value) => {
|
|
animationRef.setValue(0);
|
|
animationRef.stopAnimation();
|
|
};
|
|
|
|
useEffect(() => {
|
|
console.log("Updated status: ", currentStatus);
|
|
|
|
if (currentStatus === "On the Way") {
|
|
startPulsing(pulseAnimOnTheWay);
|
|
} else {
|
|
stopPulsing(pulseAnimOnTheWay);
|
|
}
|
|
|
|
if (currentStatus === "Arrived") {
|
|
startPulsing(pulseAnimArrived);
|
|
} else {
|
|
stopPulsing(pulseAnimArrived);
|
|
}
|
|
}, [currentStatus]);
|
|
|
|
// Interpolated colors for pulsing effect
|
|
const getPulseColor = (animValue: Animated.Value) => animValue.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [theme.colors.secondary, theme.colors.primary],
|
|
});
|
|
|
|
const pulseColorOnTheWay = getPulseColor(pulseAnimOnTheWay);
|
|
const pulseColorArrived = getPulseColor(pulseAnimArrived);
|
|
|
|
// Function to handle status change
|
|
const handleStatusPress = async (status: string) => {
|
|
try {
|
|
if (currentStatus === status) {
|
|
// If pressed again, send "expired" status and clear currentStatus
|
|
console.log(`Expiring status: ${status}`);
|
|
const message: Message = { Id: id, Name: name, Image: image, Status: "expired", Timestamp: new Date().toISOString() };
|
|
await axios.post(API_URL + "/set", message);
|
|
setStatus(""); // Reset status
|
|
} else {
|
|
// Otherwise, send the new status
|
|
console.log(`Setting status: ${status}`);
|
|
const message: Message = { Id: id, Name: name, Image: image, Status: status, Timestamp: new Date().toISOString() };
|
|
await axios.post(API_URL + "/set", message);
|
|
setStatus(status);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error sending status '${status}':`, error);
|
|
}
|
|
};
|
|
|
|
// Determine the button label based on whether it's animating
|
|
const getButtonLabel = (status: string) => {
|
|
if (status === "On the Way") return currentStatus === "On the Way" ? "Traveling" : "On the way";
|
|
if (status === "Arrived") return currentStatus === "Arrived" ? "At the dog park" : "Arrived";
|
|
return status;
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (lastMessage?.data) {
|
|
try {
|
|
const newMessage: Message = JSON.parse(lastMessage.data);
|
|
setMessages((prev) => {
|
|
if (newMessage.Status === "removed") {
|
|
if (newMessage.Id === id) {
|
|
setTimeout(() => {
|
|
setStatus(""); // Correctly clears the status
|
|
}, 0);
|
|
}
|
|
return prev.filter((msg) => msg.Id !== newMessage.Id);
|
|
}
|
|
return prev.filter((msg) => msg.Id !== newMessage.Id).concat(newMessage);
|
|
});
|
|
} catch (error) {
|
|
console.error("Error parsing WebSocket message:", error);
|
|
}
|
|
}
|
|
}, [lastMessage, setStatus, id]);
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: theme.colors.background }, isProfileActive && { display: 'none' }]}>
|
|
<View style={styles.listContainer}>
|
|
<View style={styles.listRow}>
|
|
<View style={styles.listColumn}>
|
|
<List.Section>
|
|
<List.Subheader style={[styles.listSubheader, { color: theme.colors.onSurface }]}>On the Way</List.Subheader>
|
|
{messages.filter(msg => msg.Status === "On the Way")
|
|
.sort((a, b) => new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime())
|
|
.map(item => (
|
|
<View key={item.Id} style={[styles.card, { backgroundColor: theme.colors.primaryContainer }]}>
|
|
<List.Item
|
|
key={item.Id}
|
|
title={item.Name}
|
|
titleStyle={{ color: theme.colors.onSurface }}
|
|
description={new Date(item.Timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true })}
|
|
descriptionStyle={{ color: theme.colors.onSurface }}
|
|
left={(props) => <Avatar.Image {...props} size={40} source={{ uri: `data:image/png;base64,${item.Image}` }} />}
|
|
/>
|
|
</View>
|
|
))}
|
|
</List.Section>
|
|
</View>
|
|
<View style={styles.listColumn}>
|
|
<List.Section>
|
|
<List.Subheader style={[styles.listSubheader, { color: theme.colors.onSurface }]}>Arrived</List.Subheader>
|
|
{messages.filter(msg => msg.Status === "Arrived")
|
|
.sort((a, b) => new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime())
|
|
.map(item => (
|
|
<View key={item.Id} style={[styles.card, { backgroundColor: theme.colors.primaryContainer }]}>
|
|
<List.Item
|
|
key={item.Id}
|
|
title={item.Name}
|
|
titleStyle={{ color: theme.colors.onSurface }}
|
|
description={new Date(item.Timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true })}
|
|
descriptionStyle={{ color: theme.colors.onSurface }}
|
|
left={(props) => <Avatar.Image {...props} size={40} source={{ uri: `data:image/png;base64,${item.Image}` }} />}
|
|
/>
|
|
</View>
|
|
))}
|
|
</List.Section>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
<ImageBackground source={require('../assets/images/bg.webp')} style={styles.imageBackground} />
|
|
<View style={styles.buttonContainer}>
|
|
<Animated.View style={{ flex: 1 }}>
|
|
<Button
|
|
mode="contained"
|
|
onPress={() => handleStatusPress("On the Way")}
|
|
style={[
|
|
styles.actionButton,
|
|
{ backgroundColor: currentStatus === "On the Way" ? pulseColorOnTheWay : theme.colors.primary }
|
|
]}
|
|
labelStyle={{ color: theme.colors.onPrimary }}>
|
|
{getButtonLabel("On the Way")}
|
|
</Button>
|
|
</Animated.View>
|
|
<Animated.View style={{ flex: 1 }}>
|
|
<Button
|
|
mode="contained"
|
|
onPress={() => handleStatusPress("Arrived")}
|
|
style={[
|
|
styles.actionButton,
|
|
{ backgroundColor: currentStatus === "Arrived" ? pulseColorArrived : theme.colors.primary }
|
|
]}
|
|
labelStyle={{ color: theme.colors.onPrimary }}>
|
|
{getButtonLabel("Arrived")}
|
|
</Button>
|
|
</Animated.View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default StatusPage;
|