pogdark-app/app/StatusPage.tsx
whysman 3ced969c4f
All checks were successful
Build Flutter Web and Docker Image for Local Registry / Build React Native Web App (push) Successful in 10m31s
New logo, new fonts, added license
2025-03-01 13:11:36 -05:00

293 lines
13 KiB
TypeScript

import React, { useEffect, useState, useRef } from "react";
import useWebSocket from "react-use-websocket";
import axios from "axios";
import {Animated, Easing, ImageBackground, StyleSheet, useColorScheme, View} from "react-native";
import { Avatar, List, Button, useTheme, } from "react-native-paper";
import { themes } from "@/app/themes";
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;
Theme: string;
Timestamp: string;
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "stretch" },
listContainer: { flex: 1, width: "100%", backgroundColor: 'transparent' },
listSubheader: {
fontFamily: "Medium",
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;
currentTheme: string;
isProfileActive: boolean;
}
const StatusPage: React.FC<StatusProps> = ({ id, name, image, currentStatus, setStatus, currentTheme, isProfileActive }) => {
//console.log("WebSocket URL: ", WS_URL);
//console.log("API URL: ", API_URL);
const theme = useTheme();
const colorScheme = useColorScheme();
console.log(themes[currentTheme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primary);
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.inversePrimary, theme.colors.primaryContainer],
});
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 "none" status and clear currentStatus
console.log(`Removing status: ${status}`);
const message: Message = {
Id: id,
Name: name,
Image: image,
Status: "none",
Theme: currentTheme,
Timestamp: new Date().toISOString()
};
await axios.post(API_URL + "/set", message);
setTimeout(() => {
setStatus("none"); // Reset status
}, 0)
} else {
// Otherwise, send the new status
console.log(`Setting status: ${status}`);
const message: Message = {
Id: id,
Name: name,
Image: image,
Status: status,
Theme: currentTheme,
Timestamp: new Date().toISOString()
};
await axios.post(API_URL + "/set", message);
setTimeout(() => {
setStatus(status);
}, 0)
}
} 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);
console.log("Current Status", currentStatus);
setMessages((prev) => {
if (newMessage.Id === id && newMessage.Status !== currentStatus) {
console.log("Status different, change to: ", newMessage.Status);
setTimeout(() => {
setStatus(newMessage.Status);
}, 0);
//return prev.filter((msg) => msg.Id !== newMessage.Id);
}else{
console.log("Status equal, no change");
}
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, 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.primary }]}>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: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primaryContainer }]}>
<List.Item
key={item.Id}
title={item.Name}
titleStyle={{ color: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primary, fontFamily: "SpaceBold" }}
description={new Date(item.Timestamp).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', hour12: true })}
descriptionStyle={{ color: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primary, fontFamily: "SpaceReg" }}
left={(props) => <Avatar.Image {...props} size={50} 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.primary }]}>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: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primaryContainer }]}>
<List.Item
key={item.Id}
title={item.Name}
titleStyle={{ color: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primary, fontFamily: "SpaceBold" }}
description={new Date(item.Timestamp).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', hour12: true })}
descriptionStyle={{ color: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primary, fontFamily: "SpaceReg" }}
left={(props) => <Avatar.Image {...props} size={50} 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.primaryContainer }
]}
labelStyle={{ color: theme.colors.primary, fontFamily: "Heavy",}}>
{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.primaryContainer }
]}
labelStyle={{ color: theme.colors.primary, fontFamily: "Heavy", }}>
{getButtonLabel("Arrived")}
</Button>
</Animated.View>
</View>
</View>
);
};
export default StatusPage;