Compare commits
20 Commits
master-3ce
...
master
Author | SHA1 | Date | |
---|---|---|---|
e102d1ccb7 | |||
2b4f13cdb7 | |||
bd8d1fb31e | |||
294ee04b34 | |||
ebaef46456 | |||
eb1309f163 | |||
299bab4e6a | |||
356f604323 | |||
2ba1bc0658 | |||
77fa3417ef | |||
7671fc9787 | |||
bf22da655f | |||
6eff2b311f | |||
c2d173a4ba | |||
84a975e89a | |||
e4ee0ba827 | |||
01ca1efa32 | |||
8178179ac2 | |||
2b99575862 | |||
9e077667e9 |
@ -4,9 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
58
app/Nav.tsx
58
app/Nav.tsx
@ -1,58 +0,0 @@
|
||||
import {Appbar, Portal, Button, Dialog, Menu, Text, useTheme} from "react-native-paper";
|
||||
import {Image, StyleSheet, useColorScheme, View} from "react-native";
|
||||
import React, {useState} from "react";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
logoContainer: { flex: 1, alignItems: "center" },
|
||||
logo: { width: 150, height: 75, resizeMode: "contain" },
|
||||
});
|
||||
|
||||
const Nav = ({ toggleProfile }: { toggleProfile: () => void; }) => {
|
||||
const theme = useTheme();
|
||||
const colorScheme = useColorScheme();
|
||||
const [aboutVisible, setAboutVisible] = useState(false);
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: theme.colors.background }}>
|
||||
<Appbar.Header style={{ backgroundColor: theme.colors.primaryContainer }}>
|
||||
<View>
|
||||
<Menu visible={menuVisible} onDismiss={() => setMenuVisible(false)} anchor={menuAnchor} style={{ backgroundColor: theme.colors.primaryContainer }}>
|
||||
<Menu.Item onPress={() => { setMenuVisible(false); setAboutVisible(true);}} title="About Us" style={{ backgroundColor: theme.colors.primaryContainer }}/>
|
||||
</Menu>
|
||||
<Appbar.Action icon="menu"
|
||||
onPressIn={(event) => {
|
||||
setMenuAnchor({ x: event.nativeEvent.pageX, y: event.nativeEvent.pageY + 40 });
|
||||
setMenuVisible(true);
|
||||
}}
|
||||
iconColor={theme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image source={
|
||||
colorScheme === 'dark' ?
|
||||
require("../assets/images/pogdark_logo_inverse.png") : require("../assets/images/pogdark_logo.png")
|
||||
} style={styles.logo} />
|
||||
</View>
|
||||
<Appbar.Action icon="pencil" onPress={toggleProfile} iconColor={ theme.colors.primary } />
|
||||
</Appbar.Header>
|
||||
<Portal>
|
||||
<Dialog visible={aboutVisible} onDismiss={() => setAboutVisible(false)} style={{ backgroundColor: theme.colors.primaryContainer }}>
|
||||
<Dialog.Title style={{ color: theme.colors.primary, textAlign: 'center' }}>About Us</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Text style={{ color: theme.colors.primary, textAlign: 'justify' }}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl nec laoreet luctus, felis sapien facilisis augue, at tristique enim turpis nec turpis. Donec convallis justo vel mi consectetur, at pulvinar justo dictum. Mauris vulputate dapibus neque, sed sagittis libero dapibus eu. Integer nec mi at quam cursus suscipit sed et tortor.
|
||||
</Text>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions style={{ justifyContent: "center" }}>
|
||||
<Button onPress={() => setAboutVisible(false)} mode="contained" style={{ backgroundColor: theme.colors.inversePrimary }} labelStyle={{ color: theme.colors.primary }}>
|
||||
Close
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
@ -1,292 +0,0 @@
|
||||
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;
|
@ -5,11 +5,12 @@ import { useEffect } from 'react';
|
||||
import 'react-native-reanimated';
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { PaperProvider, Provider } from "react-native-paper";
|
||||
import { themes } from '@/app/themes'
|
||||
import { UserProvider, useUser } from "@/context/UserContext";
|
||||
import themes from '@/assets/themes'
|
||||
import log from "@/util/log"
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
void SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
@ -21,9 +22,9 @@ export default function RootLayout() {
|
||||
|
||||
function InnerRootLayout() {
|
||||
const { currentTheme } = useUser(); // Access the currentTheme from UserContext
|
||||
console.log(currentTheme);
|
||||
log.debug(currentTheme);
|
||||
const colorScheme = useColorScheme();
|
||||
console.log(colorScheme);
|
||||
log.debug(colorScheme);
|
||||
const [loaded] = useFonts({
|
||||
SpaceReg: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||
SpaceBold: require('../assets/fonts/SpaceMono-Bold.ttf'),
|
||||
@ -33,7 +34,7 @@ function InnerRootLayout() {
|
||||
});
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
void SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded]);
|
||||
|
||||
|
62
app/_layout.web.tsx
Normal file
62
app/_layout.web.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { useEffect } from 'react';
|
||||
import 'react-native-reanimated';
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { PaperProvider, Provider } from "react-native-paper";
|
||||
import { UserProvider, useUser } from "@/context/UserContext";
|
||||
import themes from '@/assets/themes'
|
||||
import log from "@/util/log"
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
void SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<UserProvider>
|
||||
<InnerRootLayout />
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerRootLayout() {
|
||||
const { currentTheme } = useUser(); // Access the currentTheme from UserContext
|
||||
log.debug(currentTheme);
|
||||
const colorScheme = useColorScheme();
|
||||
log.debug(colorScheme);
|
||||
const [loaded] = useFonts({
|
||||
SpaceReg: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||
SpaceBold: require('../assets/fonts/SpaceMono-Bold.ttf'),
|
||||
Light: require('../assets/fonts/font-light.otf'),
|
||||
Medium: require('../assets/fonts/font-medium.otf'),
|
||||
Heavy: require('../assets/fonts/font-heavy.otf'),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
void SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded]);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure currentTheme is treated as a valid key, or fallback to 'blue'
|
||||
//const themeKey: 'blue' | 'green' | 'red' | 'yellow' | 'orange' = (currentTheme as 'blue' | 'green' | 'red' | 'yellow' | 'orange') || 'blue';
|
||||
|
||||
// Use the themeKey to index into the themes object
|
||||
|
||||
//const appTheme = themes[themeKey][colorScheme === 'dark' ? 'dark' : 'light'];
|
||||
const appTheme = themes[currentTheme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light']
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<PaperProvider theme={appTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</PaperProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import {View, StyleSheet, Text } from "react-native";
|
||||
import {View, Text } from "react-native";
|
||||
import { useTheme } from "react-native-paper";
|
||||
import ProfileScreen from "@/app/ProfileScreen";
|
||||
import StatusPage from "@/app/StatusPage";
|
||||
import Nav from "@/app/Nav";
|
||||
import Profile from "@/components/Profile";
|
||||
import Status from "@/components/Status";
|
||||
import TopNav from "@/components/TopNav";
|
||||
import BottomNav from "@/components/BottomNav"
|
||||
import { useUser } from "@/context/UserContext";
|
||||
import styles from "@/assets/styles";
|
||||
import log from "@/util/log"
|
||||
|
||||
const Index = () => {
|
||||
const theme = useTheme();
|
||||
@ -26,20 +29,20 @@ const Index = () => {
|
||||
} = useUser();
|
||||
|
||||
if (isLoading) {
|
||||
console.log("Still loading");
|
||||
log.debug("Still loading");
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: theme.colors.background, justifyContent: 'center', alignItems: 'center' }]}>
|
||||
<View style={[styles.indexContainer, { backgroundColor: theme.colors.background, justifyContent: 'center', alignItems: 'center' }]}>
|
||||
<Text>Loading...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
|
||||
<Nav
|
||||
<View style={[styles.indexContainer, { backgroundColor: theme.colors.background }]}>
|
||||
<TopNav
|
||||
toggleProfile={() => setProfileActive(true)}
|
||||
/>
|
||||
<StatusPage
|
||||
<Status
|
||||
id={userId}
|
||||
name={userName}
|
||||
image={userImage}
|
||||
@ -48,7 +51,7 @@ const Index = () => {
|
||||
currentTheme={currentTheme}
|
||||
isProfileActive={isProfileActive}
|
||||
/>
|
||||
<ProfileScreen
|
||||
<Profile
|
||||
visible={isProfileActive}
|
||||
id={userId}
|
||||
name={userName}
|
||||
@ -60,19 +63,11 @@ const Index = () => {
|
||||
setChanged={setUserDataChanged}
|
||||
onClose={() => setProfileActive(false)}
|
||||
/>
|
||||
<BottomNav
|
||||
isProfileActive={isProfileActive}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, alignItems: "stretch" },
|
||||
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.3, // Fades the image
|
||||
},
|
||||
});
|
||||
|
||||
export default Index;
|
||||
|
108
app/themes.ts
108
app/themes.ts
@ -1,108 +0,0 @@
|
||||
import { MD3LightTheme, MD3DarkTheme } from 'react-native-paper';
|
||||
|
||||
export const themes = {
|
||||
purple: {
|
||||
light: MD3LightTheme,
|
||||
dark: MD3DarkTheme,
|
||||
},
|
||||
red: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#7F0000',
|
||||
primaryContainer: '#FF5252',
|
||||
inversePrimary: '#D32F2F',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FF5252',
|
||||
primaryContainer: '#7F0000',
|
||||
inversePrimary: '#D32F2F',
|
||||
},
|
||||
},
|
||||
},
|
||||
blue: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#0D47A1',
|
||||
primaryContainer: '#64B5F6',
|
||||
inversePrimary: '#1976D2',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#64B5F6',
|
||||
primaryContainer: '#0D47A1',
|
||||
inversePrimary: '#1976D2',
|
||||
},
|
||||
},
|
||||
},
|
||||
yellow: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#efb100',
|
||||
primaryContainer: '#FFEB8F',
|
||||
inversePrimary: '#fbc338',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FFEB8F',
|
||||
primaryContainer: '#EFB100',
|
||||
inversePrimary: '#FBC02D',
|
||||
},
|
||||
},
|
||||
},
|
||||
green: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#004D00',
|
||||
primaryContainer: '#66BB6A',
|
||||
inversePrimary: '#388E3C',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#66BB6A',
|
||||
primaryContainer: '#004D00',
|
||||
inversePrimary: '#388E3C',
|
||||
},
|
||||
},
|
||||
},
|
||||
orange: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#F57C00',
|
||||
primaryContainer: '#FFCC80',
|
||||
inversePrimary: '#FFAB40',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FFCC80',
|
||||
primaryContainer: '#F57C00',
|
||||
inversePrimary: '#FFAB40',
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
BIN
assets/images/broken.png
Normal file
BIN
assets/images/broken.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1022 KiB |
95
assets/styles.ts
Normal file
95
assets/styles.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import {StyleSheet} from "react-native";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
//StatusPage
|
||||
container: { flex: 1, alignItems: "stretch" },
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
backgroundColor: 'transparent',
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
//alignItems: "flex-start", // Aligns subheaders properly
|
||||
paddingHorizontal: 10, // Adds some spacing
|
||||
},
|
||||
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 },
|
||||
listColumn: { flex: 1, paddingHorizontal: 5, zIndex: 1},
|
||||
buttonContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignSelf: "stretch",
|
||||
verticalAlign: "bottom",
|
||||
paddingHorizontal: 10,
|
||||
paddingBottom: 20,
|
||||
marginHorizontal: 5,
|
||||
},
|
||||
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,
|
||||
},
|
||||
//profile screen
|
||||
themeContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignSelf: 'center',
|
||||
//alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
themeButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
},
|
||||
halfCircle: {
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
},
|
||||
//Nav
|
||||
logoContainer: { flex: 1, alignItems: "center" },
|
||||
logo: { width: 140, height: 70 },
|
||||
topBar: { },
|
||||
bottomBar: { height: 38 },
|
||||
//index
|
||||
indexImageBackground: {
|
||||
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.3, // Fades the image
|
||||
},
|
||||
indexContainer: { flex: 1, alignItems: "stretch" },
|
||||
});
|
||||
|
||||
export default styles;
|
450
assets/themes.ts
Normal file
450
assets/themes.ts
Normal file
@ -0,0 +1,450 @@
|
||||
import { MD3LightTheme, MD3DarkTheme } from 'react-native-paper';
|
||||
|
||||
const themes = {
|
||||
purple: {
|
||||
light: MD3LightTheme,
|
||||
dark: MD3DarkTheme,
|
||||
},
|
||||
red: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#7F0000',
|
||||
primaryContainer: '#FF5252',
|
||||
inversePrimary: '#D32F2F',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FF5252',
|
||||
primaryContainer: '#7F0000',
|
||||
inversePrimary: '#D32F2F',
|
||||
},
|
||||
},
|
||||
},
|
||||
blue: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#0D47A1',
|
||||
primaryContainer: '#64B5F6',
|
||||
inversePrimary: '#1976D2',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#64B5F6',
|
||||
primaryContainer: '#0D47A1',
|
||||
inversePrimary: '#1976D2',
|
||||
},
|
||||
},
|
||||
},
|
||||
yellow: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#efb100',
|
||||
primaryContainer: '#FFEB8F',
|
||||
inversePrimary: '#fbc338',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FFEB8F',
|
||||
primaryContainer: '#EFB100',
|
||||
inversePrimary: '#FBC02D',
|
||||
},
|
||||
},
|
||||
},
|
||||
green: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#004D00',
|
||||
primaryContainer: '#66BB6A',
|
||||
inversePrimary: '#388E3C',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#66BB6A',
|
||||
primaryContainer: '#004D00',
|
||||
inversePrimary: '#388E3C',
|
||||
},
|
||||
},
|
||||
},
|
||||
orange: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#F57C00',
|
||||
primaryContainer: '#FFCC80',
|
||||
inversePrimary: '#FFAB40',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FFCC80',
|
||||
primaryContainer: '#F57C00',
|
||||
inversePrimary: '#FFAB40',
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const oldThemes = {
|
||||
purple: {
|
||||
light: MD3LightTheme,
|
||||
dark: MD3DarkTheme,
|
||||
},
|
||||
red: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#D32F2F',
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#FFCDD2',
|
||||
onPrimaryContainer: '#4A0D0D',
|
||||
secondary: '#FF5252',
|
||||
onSecondary: '#FFFFFF',
|
||||
secondaryContainer: '#FFEBEE',
|
||||
onSecondaryContainer: '#5D1E1E',
|
||||
tertiary: '#C2185B',
|
||||
onTertiary: '#FFFFFF',
|
||||
tertiaryContainer: '#F8BBD0',
|
||||
onTertiaryContainer: '#550027',
|
||||
background: '#FFFFFF',
|
||||
onBackground: '#121212',
|
||||
surface: '#F5F5F5',
|
||||
onSurface: '#1E1E1E',
|
||||
error: '#B00020',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#B71C1C',
|
||||
inverseSurface: '#333333',
|
||||
inverseOnSurface: '#FAFAFA',
|
||||
inversePrimary: '#FFB4A9',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FF5252',
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#7F0000',
|
||||
onPrimaryContainer: '#FFDAD4',
|
||||
secondary: '#FF8A80',
|
||||
onSecondary: '#1E0000',
|
||||
secondaryContainer: '#4A0D0D',
|
||||
onSecondaryContainer: '#FFEBEE',
|
||||
tertiary: '#FF4081',
|
||||
onTertiary: '#1E001E',
|
||||
tertiaryContainer: '#7A001A',
|
||||
onTertiaryContainer: '#FFD9E3',
|
||||
background: '#121212',
|
||||
onBackground: '#E1E1E1',
|
||||
surface: '#1E1E1E',
|
||||
onSurface: '#FFFFFF',
|
||||
error: '#CF6679',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#FF6F61',
|
||||
inverseSurface: '#E1E1E1',
|
||||
inverseOnSurface: '#1E1E1E',
|
||||
inversePrimary: '#D32F2F',
|
||||
},
|
||||
},
|
||||
},
|
||||
blue: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#1976D2',
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#BBDEFB',
|
||||
onPrimaryContainer: '#0D47A1',
|
||||
secondary: '#64B5F6',
|
||||
onSecondary: '#FFFFFF',
|
||||
secondaryContainer: '#E3F2FD',
|
||||
onSecondaryContainer: '#1565C0',
|
||||
tertiary: '#0288D1',
|
||||
onTertiary: '#FFFFFF',
|
||||
tertiaryContainer: '#81D4FA',
|
||||
onTertiaryContainer: '#01579B',
|
||||
background: '#FFFFFF',
|
||||
onBackground: '#121212',
|
||||
surface: '#F5F5F5',
|
||||
onSurface: '#1E1E1E',
|
||||
error: '#B00020',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#1565C0',
|
||||
inverseSurface: '#333333',
|
||||
inverseOnSurface: '#FAFAFA',
|
||||
inversePrimary: '#90CAF9',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#64B5F6',
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#0D47A1',
|
||||
onPrimaryContainer: '#BBDEFB',
|
||||
secondary: '#81D4FA',
|
||||
onSecondary: '#1E1E1E',
|
||||
secondaryContainer: '#1565C0',
|
||||
onSecondaryContainer: '#E3F2FD',
|
||||
tertiary: '#29B6F6',
|
||||
onTertiary: '#1E1E1E',
|
||||
tertiaryContainer: '#01579B',
|
||||
onTertiaryContainer: '#81D4FA',
|
||||
background: '#121212',
|
||||
onBackground: '#E1E1E1',
|
||||
surface: '#1E1E1E',
|
||||
onSurface: '#FFFFFF',
|
||||
error: '#CF6679',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#64B5F6',
|
||||
inverseSurface: '#E1E1E1',
|
||||
inverseOnSurface: '#1E1E1E',
|
||||
inversePrimary: '#1976D2',
|
||||
},
|
||||
},
|
||||
},
|
||||
yellow: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#FBC02D',
|
||||
onPrimary: '#000000',
|
||||
primaryContainer: '#FFF9C4',
|
||||
onPrimaryContainer: '#5F3700',
|
||||
secondary: '#FFD54F',
|
||||
onSecondary: '#000000',
|
||||
secondaryContainer: '#FFF8E1',
|
||||
onSecondaryContainer: '#775A00',
|
||||
tertiary: '#FFB300',
|
||||
onTertiary: '#FFFFFF',
|
||||
tertiaryContainer: '#FFECB3',
|
||||
onTertiaryContainer: '#603800',
|
||||
background: '#FFFFFF',
|
||||
onBackground: '#121212',
|
||||
surface: '#F5F5F5',
|
||||
onSurface: '#1E1E1E',
|
||||
error: '#B00020',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#FF8F00',
|
||||
inverseSurface: '#333333',
|
||||
inverseOnSurface: '#FAFAFA',
|
||||
inversePrimary: '#FDD835',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FFD54F',
|
||||
onPrimary: '#000000',
|
||||
primaryContainer: '#5F3700',
|
||||
onPrimaryContainer: '#FFF9C4',
|
||||
secondary: '#FFEB3B',
|
||||
onSecondary: '#000000',
|
||||
secondaryContainer: '#775A00',
|
||||
onSecondaryContainer: '#FFF8E1',
|
||||
tertiary: '#FFB300',
|
||||
onTertiary: '#1E1E1E',
|
||||
tertiaryContainer: '#603800',
|
||||
onTertiaryContainer: '#FFECB3',
|
||||
background: '#121212',
|
||||
onBackground: '#E1E1E1',
|
||||
surface: '#1E1E1E',
|
||||
onSurface: '#FFFFFF',
|
||||
error: '#CF6679',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#FDD835',
|
||||
inverseSurface: '#E1E1E1',
|
||||
inverseOnSurface: '#1E1E1E',
|
||||
inversePrimary: '#FBC02D',
|
||||
},
|
||||
},
|
||||
},
|
||||
green: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#388E3C', // Material Green 700
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#C8E6C9',
|
||||
onPrimaryContainer: '#004D00',
|
||||
|
||||
secondary: '#66BB6A',
|
||||
onSecondary: '#FFFFFF',
|
||||
secondaryContainer: '#E8F5E9',
|
||||
onSecondaryContainer: '#1B5E20',
|
||||
|
||||
tertiary: '#2E7D32',
|
||||
onTertiary: '#FFFFFF',
|
||||
tertiaryContainer: '#A5D6A7',
|
||||
onTertiaryContainer: '#003300',
|
||||
|
||||
background: '#FFFFFF',
|
||||
onBackground: '#121212',
|
||||
surface: '#F5F5F5',
|
||||
onSurface: '#1E1E1E',
|
||||
error: '#B00020',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#388E3C',
|
||||
|
||||
inverseSurface: '#333333',
|
||||
inverseOnSurface: '#FAFAFA',
|
||||
inversePrimary: '#A5D6A7', // Soft green
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#66BB6A',
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#004D00',
|
||||
onPrimaryContainer: '#C8E6C9',
|
||||
|
||||
secondary: '#81C784',
|
||||
onSecondary: '#1E1E1E',
|
||||
secondaryContainer: '#1B5E20',
|
||||
onSecondaryContainer: '#E8F5E9',
|
||||
|
||||
tertiary: '#43A047',
|
||||
onTertiary: '#1E1E1E',
|
||||
tertiaryContainer: '#003300',
|
||||
onTertiaryContainer: '#A5D6A7',
|
||||
|
||||
background: '#121212',
|
||||
onBackground: '#E1E1E1',
|
||||
surface: '#1E1E1E',
|
||||
onSurface: '#FFFFFF',
|
||||
error: '#CF6679',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#A5D6A7',
|
||||
|
||||
inverseSurface: '#E1E1E1',
|
||||
inverseOnSurface: '#1E1E1E',
|
||||
inversePrimary: '#388E3C', // Same as light theme primary
|
||||
}
|
||||
}
|
||||
},
|
||||
orange: {
|
||||
light: {
|
||||
...MD3LightTheme,
|
||||
colors: {
|
||||
...MD3LightTheme.colors,
|
||||
primary: '#F57C00', // Material Orange 700
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#FFCC80',
|
||||
onPrimaryContainer: '#7A3300',
|
||||
|
||||
secondary: '#FFA726',
|
||||
onSecondary: '#FFFFFF',
|
||||
secondaryContainer: '#FFE0B2',
|
||||
onSecondaryContainer: '#8E4700',
|
||||
|
||||
tertiary: '#EF6C00', // Deep orange
|
||||
onTertiary: '#FFFFFF',
|
||||
tertiaryContainer: '#FFD180',
|
||||
onTertiaryContainer: '#622600',
|
||||
|
||||
background: '#FFFFFF',
|
||||
onBackground: '#121212',
|
||||
surface: '#F5F5F5',
|
||||
onSurface: '#1E1E1E',
|
||||
error: '#B00020',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#F57C00',
|
||||
|
||||
inverseSurface: '#333333',
|
||||
inverseOnSurface: '#FAFAFA',
|
||||
inversePrimary: '#FFAB40', // Soft orange
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...MD3DarkTheme,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#FFA726',
|
||||
onPrimary: '#FFFFFF',
|
||||
primaryContainer: '#b38800',
|
||||
onPrimaryContainer: '#FFCC80',
|
||||
|
||||
secondary: '#FFB74D',
|
||||
onSecondary: '#1E1E1E',
|
||||
secondaryContainer: '#5a4406',
|
||||
onSecondaryContainer: '#FFE0B2',
|
||||
|
||||
tertiary: '#FB8C00',
|
||||
onTertiary: '#1E1E1E',
|
||||
tertiaryContainer: '#623400',
|
||||
onTertiaryContainer: '#FFD180',
|
||||
|
||||
background: '#121212',
|
||||
onBackground: '#E1E1E1',
|
||||
surface: '#1E1E1E',
|
||||
onSurface: '#FFFFFF',
|
||||
error: '#CF6679',
|
||||
onError: '#FFFFFF',
|
||||
outline: '#FFAB40',
|
||||
|
||||
inverseSurface: '#E1E1E1',
|
||||
inverseOnSurface: '#1E1E1E',
|
||||
inversePrimary: '#F57C00', // Same as light theme primary
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const themePreviews = {
|
||||
purple: {
|
||||
light: '#6200EA', // Default Purple Light
|
||||
dark: '#BB86FC', // Default Purple Dark
|
||||
},
|
||||
red: {
|
||||
light: '#D32F2F',
|
||||
dark: '#FF5252',
|
||||
},
|
||||
blue: {
|
||||
light: '#1976D2',
|
||||
dark: '#64B5F6',
|
||||
},
|
||||
yellow: {
|
||||
light: '#FBC02D',
|
||||
dark: '#FFD54F',
|
||||
},
|
||||
green: {
|
||||
light: '#388E3C',
|
||||
dark: '#66BB6A',
|
||||
},
|
||||
orange: {
|
||||
light: '#F57C00',
|
||||
dark: '#FFA726',
|
||||
},
|
||||
};
|
||||
|
||||
export default themes;
|
47
components/About.tsx
Normal file
47
components/About.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { Card, Title, Paragraph, Text, Dialog} from 'react-native-paper';
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<Dialog.Content style={{ maxHeight: 300 }}>
|
||||
<ScrollView style={{ padding: 16 }}>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Paragraph style={{ fontSize: 20 }}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Pogdark</Text> is a dog park communication application that
|
||||
allows users to inform others when they are on the way or have arrived at a dog park of their choice.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>How It Works</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
1. Users select a simple status:{"\n"}
|
||||
{" "}• "On My Way"{"\n"}
|
||||
{" "}• "Arrived"{"\n\n"}
|
||||
2. The status is instantly sent via a WebSocket broker to other users who are listening.{"\n\n"}
|
||||
3. Users can click again to cancel their current status, or status messages will automatically expire, ensuring no long-term data retention.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>Key Features</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
• <Text style={{ fontWeight: 'bold' }}>No Tracking, No Storage</Text> – The system doesn’t store user data or track locations. {"\n\n"}
|
||||
• <Text style={{ fontWeight: 'bold' }}>Instant Updates</Text> – Real-time messaging ensures quick communication. {"\n\n"}
|
||||
• <Text style={{ fontWeight: 'bold' }}>No Links or Logins</Text> – Users don’t need to generate links or sign up. Profiles are stored locally, and created on initial use.{"\n\n"}
|
||||
• <Text style={{ fontWeight: 'bold' }}>Ephemeral by Design</Text> – Messages exist only for a 30 (On My Way) or 90 (Arrived) minute lifespan.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
</Dialog.Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
48
components/BottomNav.tsx
Normal file
48
components/BottomNav.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import {Appbar, Portal, Button, Dialog, Text, useTheme } from "react-native-paper";
|
||||
import { View } from "react-native";
|
||||
import styles from "@/assets/styles";
|
||||
import React, {useState} from "react";
|
||||
import Location from "@/components/Location";
|
||||
import Broken from "@/components/Broken";
|
||||
|
||||
|
||||
interface BNProps {
|
||||
isProfileActive: boolean;
|
||||
}
|
||||
const BottomNav: React.FC<BNProps> = ({ isProfileActive }) => {
|
||||
const theme = useTheme();
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
|
||||
|
||||
return (
|
||||
<View style={ isProfileActive && { display: 'none' }}>
|
||||
<View style={{ backgroundColor: theme.colors.background }}>
|
||||
<Appbar.Header style={[styles.bottomBar, { backgroundColor: theme.colors.primaryContainer }]} >
|
||||
<View style={{ alignItems: "center", flexDirection: "row", justifyContent: "space-between", padding: 10, flex: 1, paddingHorizontal: 15 }}>
|
||||
<Text style={{ color: theme.colors.primary, fontFamily: "SpaceReg" }}>Daisy Knight Dog Park</Text>
|
||||
<Button
|
||||
mode="text"
|
||||
onPress={() => setMenuVisible(true) }
|
||||
style={{ backgroundColor: theme.colors.primary, height: styles.bottomBar.height/2, justifyContent: "center"}}
|
||||
labelStyle={{ color: theme.colors.onPrimary, fontFamily: "SpaceReg"}}>
|
||||
Change
|
||||
</Button>
|
||||
</View>
|
||||
</Appbar.Header>
|
||||
<Portal>
|
||||
<Dialog visible={menuVisible} onDismiss={() => setMenuVisible(false)} style={{ backgroundColor: theme.colors.primaryContainer, maxHeight: 400 }}>
|
||||
<Dialog.Title style={{ color: theme.colors.primary, textAlign: 'center' }}>Location</Dialog.Title>
|
||||
<Broken />
|
||||
<Dialog.Actions style={{ justifyContent: "center" }}>
|
||||
<Button onPress={() => setMenuVisible(false)} mode="contained" style={{ backgroundColor: theme.colors.inversePrimary }} labelStyle={{ color: theme.colors.primary }}>
|
||||
Close
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomNav;
|
21
components/Broken.tsx
Normal file
21
components/Broken.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import {Title, useTheme} from "react-native-paper";
|
||||
import { Image, View } from "react-native";
|
||||
import React from "react";
|
||||
|
||||
|
||||
|
||||
const Broken = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<View>
|
||||
<Title style={{color: theme.colors.onBackground, fontSize: 16, textAlign: 'center', fontFamily: "Light"}}>The Internet is a Series of Tubes</Title>
|
||||
<Image
|
||||
source={require("../assets/images/broken.png")}
|
||||
style={{ alignSelf: 'center', resizeMode: "contain", height: 400/3 }}
|
||||
/>
|
||||
<Title style={{color: theme.colors.onBackground, fontSize: 16, textAlign: 'center', fontFamily: "Light"}}>And these aren't connected. {"\n"} (We're still working on this part.)</Title>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Broken;
|
10
components/Location.tsx
Normal file
10
components/Location.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import Broken from "@/components/Broken";
|
||||
|
||||
const Location = () => {
|
||||
return (
|
||||
<Broken />
|
||||
)
|
||||
}
|
||||
|
||||
export default Location;
|
88
components/Privacy.tsx
Normal file
88
components/Privacy.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import * as React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { Card, Title, Paragraph, Text, Dialog } from 'react-native-paper';
|
||||
|
||||
const Privacy = () => {
|
||||
return (
|
||||
<Dialog.Content style={{ maxHeight: 300 }}>
|
||||
<ScrollView style={{ padding: 16 }}>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>Privacy Policy</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
<Text style={{ fontStyle: 'italic' }}>Effective Date: 4/1/25</Text>
|
||||
</Paragraph>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
Thank you for using <Text style={{ fontWeight: 'bold' }}>Pogdark</Text> ("we", "our", or "us"). We are committed to protecting your privacy. This Privacy Policy explains how we handle your information when you use our application.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>1. No Tracking</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
We do <Text style={{ fontWeight: 'bold' }}>not</Text> collect or track any personal information. We do <Text style={{ fontWeight: 'bold' }}>not</Text> use analytics tools, cookies, or any third-party tracking services.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>2. Ephemeral Data Storage</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
Any data entered or generated within the app is stored only <Text style={{ fontWeight: 'bold' }}>temporarily</Text> and exists only for the duration of your session. Once the app is closed or the session ends, all data is deleted automatically. We do not retain any user data on our servers or devices beyond the current session.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>3. No Account or Registration Required</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
You are not required to create an account or provide any personal details (such as name, email address, or phone number) to use the app.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>4. No Data Sharing</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
Since we do not collect data, we do not share any data with third parties.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>5. Security</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
While we don’t store personal data, we still take standard precautions to secure ephemeral information during your use of the app.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>6. Changes to This Privacy Policy</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
If we ever make changes to this policy, we will update the date at the top and clearly communicate the changes within the app. However, any future version will always respect your privacy.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card.Content>
|
||||
<Title style={{ fontSize: 20, marginBottom: 8 }}>7. Contact</Title>
|
||||
<Paragraph style={{ fontSize: 16 }}>
|
||||
If you have any questions or concerns about this policy, feel free to contact us at <Text style={{ fontStyle: 'italic' }}>support@pogdark.com</Text>.
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
</Dialog.Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default Privacy;
|
@ -1,11 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Platform, View, TouchableOpacity, StyleSheet } from "react-native";
|
||||
import { Platform, View, TouchableOpacity } from "react-native";
|
||||
import { Button, TextInput, Dialog, Portal, Avatar, useTheme, Text } from "react-native-paper";
|
||||
import { Asset } from 'expo-asset';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { themes } from '@/app/themes';
|
||||
import { featureFlags } from '@/featureFlags';
|
||||
import themes from '@/assets/themes';
|
||||
import styles from "@/assets/styles";
|
||||
import log from "@/util/log"
|
||||
import featureFlags from '@/util/featureFlags';
|
||||
|
||||
interface ProfileScreenProps {
|
||||
visible: boolean;
|
||||
@ -20,7 +22,7 @@ interface ProfileScreenProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ProfileScreen: React.FC<ProfileScreenProps> = ({ visible, name, setName, image, setImage, setChanged, currentTheme, setTheme, onClose }) => {
|
||||
const Profile: React.FC<ProfileScreenProps> = ({ visible, name, setName, image, setImage, setChanged, currentTheme, setTheme, onClose }) => {
|
||||
const theme = useTheme();
|
||||
const isNameEmpty = !name.trim();
|
||||
const themeColors = ['red', 'blue', 'yellow', 'green', 'orange', 'purple'];
|
||||
@ -66,7 +68,7 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ visible, name, setName, i
|
||||
|
||||
const loadImage = async () => {
|
||||
if (!image || image === "") {
|
||||
console.log("Loading ", image);
|
||||
log.debug("Loading ", image);
|
||||
await loadDefaultImage();
|
||||
}
|
||||
};
|
||||
@ -78,7 +80,7 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ visible, name, setName, i
|
||||
if (!result.canceled && result.assets.length > 0) {
|
||||
if (result.assets[0].base64 !== image) { // Only update if the image actually changes
|
||||
setImage(result.assets[0].base64 || image);
|
||||
console.log("Picking Image");
|
||||
log.debug("Picking Image");
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -124,7 +126,7 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ visible, name, setName, i
|
||||
onChangeText={(newName) => {
|
||||
if (newName !== name) { // Only trigger change if it's different
|
||||
setName(newName);
|
||||
console.log("Name change");
|
||||
log.debug("Name change");
|
||||
}
|
||||
}}
|
||||
style={{ marginBottom: 15, fontFamily: "SpaceReg" }}
|
||||
@ -140,7 +142,7 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ visible, name, setName, i
|
||||
<TouchableOpacity
|
||||
key={userTheme}
|
||||
style={[styles.themeButton, { backgroundColor: themes[userTheme as keyof typeof themes]['light'].colors.primary }]}
|
||||
onPress={() => {setTheme(userTheme); console.log("Changing Theme: ", userTheme)}}
|
||||
onPress={() => {setTheme(userTheme); log.debug("Changing Theme: ", userTheme)}}
|
||||
>
|
||||
<View style={[styles.halfCircle, { backgroundColor: themes[userTheme as keyof typeof themes]['dark'].colors.primary }]} />
|
||||
</TouchableOpacity>
|
||||
@ -165,30 +167,4 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ visible, name, setName, i
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
themeContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
themeButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
},
|
||||
halfCircle: {
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default ProfileScreen;
|
||||
export default Profile;
|
235
components/Status.tsx
Normal file
235
components/Status.tsx
Normal file
@ -0,0 +1,235 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
import axios from "axios";
|
||||
import {Animated, Easing, ImageBackground, ScrollView, useColorScheme, View} from "react-native";
|
||||
import { Avatar, List, Button, useTheme, } from "react-native-paper";
|
||||
import themes from "@/assets/themes";
|
||||
import styles from "@/assets/styles";
|
||||
import log from "@/util/log"
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface StatusProps {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
currentStatus: string;
|
||||
setStatus: (currentStatus: string) => void;
|
||||
currentTheme: string;
|
||||
isProfileActive: boolean;
|
||||
}
|
||||
|
||||
const Status: React.FC<StatusProps> = ({ id, name, image, currentStatus, setStatus, currentTheme, isProfileActive }) => {
|
||||
log.debug("WebSocket URL: ", WS_URL);
|
||||
log.debug("API URL: ", API_URL);
|
||||
const theme = useTheme();
|
||||
const colorScheme = useColorScheme();
|
||||
log.debug(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(() => {
|
||||
log.debug("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
|
||||
log.debug(`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
|
||||
log.debug(`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) {
|
||||
log.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);
|
||||
log.debug("Current Status", currentStatus);
|
||||
setMessages((prev) => {
|
||||
if (newMessage.Id === id && newMessage.Status !== currentStatus) {
|
||||
log.debug("Status different, change to: ", newMessage.Status);
|
||||
setTimeout(() => {
|
||||
setStatus(newMessage.Status);
|
||||
}, 0);
|
||||
//return prev.filter((msg) => msg.Id !== newMessage.Id);
|
||||
}else{
|
||||
log.debug("Status equal, no change");
|
||||
}
|
||||
return prev.filter((msg) => msg.Id !== newMessage.Id).concat(newMessage);
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("Error parsing WebSocket message:", error);
|
||||
}
|
||||
}
|
||||
}, [lastMessage, setStatus, id]);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isProfileActive && { display: 'none' }]}>
|
||||
<ImageBackground source={require('../assets/images/bg.webp')} style={styles.imageBackground} />
|
||||
<View style={styles.listContainer}>
|
||||
<List.Section style={styles.listColumn}>
|
||||
<List.Subheader style={[styles.listSubheader, { color: theme.colors.primary }]}>On the Way</List.Subheader>
|
||||
<ScrollView>
|
||||
{messages.filter(msg => msg.Status === "On the Way")
|
||||
.sort((a, b) => new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime())
|
||||
.map(item => (
|
||||
<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" }}
|
||||
style={[styles.card, { backgroundColor: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primaryContainer }]}
|
||||
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}` }} />}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</List.Section>
|
||||
<List.Section style={styles.listColumn}>
|
||||
<List.Subheader style={[styles.listSubheader, { color: theme.colors.primary }]}>Arrived</List.Subheader>
|
||||
<ScrollView>
|
||||
{messages.filter(msg => msg.Status === "Arrived")
|
||||
.sort((a, b) => new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime())
|
||||
.map(item => (
|
||||
<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" }}
|
||||
style={[styles.card, { backgroundColor: themes[item.Theme as keyof typeof themes][colorScheme === 'dark' ? 'dark' : 'light'].colors.primaryContainer }]}
|
||||
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}` }} />}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</List.Section>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<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: "Medium", fontSize: 16 }}>
|
||||
{getButtonLabel("On the Way")}
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => handleStatusPress("Arrived")}
|
||||
style={[
|
||||
styles.actionButton,
|
||||
{ backgroundColor: currentStatus === "Arrived" ? pulseColorArrived : theme.colors.primaryContainer }
|
||||
]}
|
||||
labelStyle={{ color: theme.colors.primary, fontFamily: "Medium", fontSize: 16 }}>
|
||||
{getButtonLabel("Arrived")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Status;
|
75
components/TopNav.tsx
Normal file
75
components/TopNav.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import {Appbar, Portal, Button, Dialog, Menu, useTheme} from "react-native-paper";
|
||||
import {Image, useColorScheme, View} from "react-native";
|
||||
import React, {useState} from "react";
|
||||
import styles from "@/assets/styles";
|
||||
import About from "@/components/About";
|
||||
import Privacy from "@/components/Privacy";
|
||||
import Broken from "@/components/Broken";
|
||||
|
||||
const TopNav = ({ toggleProfile }: { toggleProfile: () => void; }) => {
|
||||
const theme = useTheme();
|
||||
const colorScheme = useColorScheme();
|
||||
const [aboutVisible, setAboutVisible] = useState(false);
|
||||
const [privacyVisible, setPrivacyVisible] = useState(false);
|
||||
const [bugVisible, setBugVisible] = useState(false);
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: theme.colors.background }}>
|
||||
<Appbar.Header style={[styles.topBar, { backgroundColor: theme.colors.primaryContainer }]}>
|
||||
<View>
|
||||
<Menu visible={menuVisible} onDismiss={() => setMenuVisible(false)} anchor={menuAnchor} style={{ backgroundColor: theme.colors.primaryContainer }}>
|
||||
<Menu.Item onPress={() => { setMenuVisible(false); setAboutVisible(true);}} title="About Us" style={{ backgroundColor: theme.colors.primaryContainer }}/>
|
||||
<Menu.Item onPress={() => { setMenuVisible(false); setPrivacyVisible(true);}} title="Privacy Policy" style={{ backgroundColor: theme.colors.primaryContainer }}/>
|
||||
<Menu.Item onPress={() => { setMenuVisible(false); setBugVisible(true);}} title="Report a Bug" style={{ backgroundColor: theme.colors.primaryContainer }}/>
|
||||
</Menu>
|
||||
<Appbar.Action icon="menu"
|
||||
onPressIn={(event) => {
|
||||
setMenuAnchor({ x: event.nativeEvent.pageX, y: event.nativeEvent.pageY + 40 });
|
||||
setMenuVisible(true);
|
||||
}}
|
||||
iconColor={theme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.logoContainer} >
|
||||
<Image source={
|
||||
colorScheme === 'dark' ?
|
||||
require("../assets/images/pogdark_logo_inverse.png") : require("../assets/images/pogdark_logo.png")
|
||||
} style={styles.logo} resizeMode={"contain"} />
|
||||
</View>
|
||||
<Appbar.Action icon="pencil" onPress={toggleProfile} iconColor={ theme.colors.primary } />
|
||||
</Appbar.Header>
|
||||
<Portal>
|
||||
<Dialog visible={aboutVisible} onDismiss={() => setAboutVisible(false)} style={{ backgroundColor: theme.colors.primaryContainer }}>
|
||||
<Dialog.Title style={{ color: theme.colors.primary, textAlign: 'center' }}>About Us</Dialog.Title>
|
||||
<About />
|
||||
<Dialog.Actions style={{ justifyContent: "center" }}>
|
||||
<Button onPress={() => setAboutVisible(false)} mode="contained" style={{ backgroundColor: theme.colors.inversePrimary }} labelStyle={{ color: theme.colors.primary }}>
|
||||
Close
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
<Dialog visible={privacyVisible} onDismiss={() => setPrivacyVisible(false)} style={{ backgroundColor: theme.colors.primaryContainer }}>
|
||||
<Dialog.Title style={{ color: theme.colors.primary, textAlign: 'center' }}>Privacy Policy</Dialog.Title>
|
||||
<Privacy />
|
||||
<Dialog.Actions style={{ justifyContent: "center" }}>
|
||||
<Button onPress={() => setPrivacyVisible(false)} mode="contained" style={{ backgroundColor: theme.colors.inversePrimary }} labelStyle={{ color: theme.colors.primary }}>
|
||||
Close
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
<Dialog visible={bugVisible} onDismiss={() => setBugVisible(false)} style={{ backgroundColor: theme.colors.primaryContainer }}>
|
||||
<Dialog.Title style={{ color: theme.colors.primary, textAlign: 'center' }}>Report A Bug</Dialog.Title>
|
||||
<Broken />
|
||||
<Dialog.Actions style={{ justifyContent: "center" }}>
|
||||
<Button onPress={() => setBugVisible(false)} mode="contained" style={{ backgroundColor: theme.colors.inversePrimary }} labelStyle={{ color: theme.colors.primary }}>
|
||||
Close
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopNav;
|
@ -3,6 +3,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { AppState } from "react-native";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import axios from "axios";
|
||||
import log from "@/util/log"
|
||||
|
||||
export const API_URL = process.env.EXPO_PUBLIC_API_URL;
|
||||
|
||||
@ -49,7 +50,7 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
||||
const storedUserName = await AsyncStorage.getItem("userName");
|
||||
const storedUserImage = await AsyncStorage.getItem("userImage");
|
||||
const storedUserTheme = await AsyncStorage.getItem("theme");
|
||||
console.log("Stored theme: ", storedUserTheme);
|
||||
log.debug("Stored theme: ", storedUserTheme);
|
||||
if (storedUserId) {
|
||||
setUserId(storedUserId);
|
||||
setUserName(storedUserName || "");
|
||||
@ -64,13 +65,13 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
||||
setProfileActive(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading user data:", error);
|
||||
log.error("Error loading user data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserData();
|
||||
void loadUserData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -82,23 +83,23 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
||||
await AsyncStorage.setItem("userName", userName);
|
||||
await AsyncStorage.setItem("userImage", userImage);
|
||||
await AsyncStorage.setItem("theme", currentTheme);
|
||||
console.log("Current theme: ", currentTheme);
|
||||
log.debug("Current theme: ", currentTheme);
|
||||
setUserDataChanged(false);
|
||||
} catch (error) {
|
||||
console.error("Error saving user data:", error);
|
||||
log.error("Error saving user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
saveUserData();
|
||||
void saveUserData();
|
||||
}, [userDataChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: string) => {
|
||||
if (appState.match(/inactive|background/) && nextAppState === "active") {
|
||||
if (!isLoading) {
|
||||
fetchCurrentStatus();
|
||||
void fetchCurrentStatus();
|
||||
} else {
|
||||
console.log("Waiting for loading to complete before fetching status...");
|
||||
log.debug("Waiting for loading to complete before fetching status...");
|
||||
}
|
||||
}
|
||||
setAppState(AppState.currentState);
|
||||
@ -120,7 +121,7 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
||||
}, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching status:", error);
|
||||
log.error("Error fetching status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
30
package-lock.json
generated
30
package-lock.json
generated
@ -13,7 +13,7 @@
|
||||
"@react-native-async-storage/async-storage": "~2.1.1",
|
||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"axios": "~1.7.9",
|
||||
"axios": "~1.8.4",
|
||||
"expo": "~52.0.25",
|
||||
"expo-blur": "~14.0.2",
|
||||
"expo-constants": "~17.0.4",
|
||||
@ -30,8 +30,8 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.77.1",
|
||||
"react-native-config": "~1.5.5",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-logs": "~5.3.0",
|
||||
"react-native-paper": "~5.13.1",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
@ -5197,9 +5197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@ -12430,20 +12430,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-config": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-config/-/react-native-config-1.5.5.tgz",
|
||||
"integrity": "sha512-dGdLnBU0cd5xL5bF0ROTmHYbsstZnQKOEPfglvZi1vStvAjpld14X25K6mY3KGPTMWAzx6TbjKeq5dR+ILuMMA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react-native-windows": ">=0.61"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-windows": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-gesture-handler": {
|
||||
"version": "2.20.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
||||
@ -12484,6 +12470,12 @@
|
||||
"react-native": ">=0.73.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-logs": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-logs/-/react-native-logs-5.3.0.tgz",
|
||||
"integrity": "sha512-tq4S0JFy06ggu1D/udYeV80qPy5koURNNcKrVJmv0Hf3x44akysctaE4ARybD5Pv7MnFD8fP1VFhycSLjqXHQw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-paper": {
|
||||
"version": "5.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.13.1.tgz",
|
||||
|
@ -34,6 +34,7 @@
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.77.1",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-logs": "~5.3.0",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
@ -42,7 +43,7 @@
|
||||
"react-use-websocket": "~4.13.0",
|
||||
"@react-native-async-storage/async-storage": "~2.1.1",
|
||||
"expo-image-picker": "~16.0.6",
|
||||
"axios": "~1.7.9",
|
||||
"axios": "~1.8.4",
|
||||
"uuid": "~11.0.5",
|
||||
"react-native-paper": "~5.13.1",
|
||||
"react-native-vector-icons": "~10.2.0",
|
||||
|
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
@ -1,3 +1,5 @@
|
||||
export const featureFlags = {
|
||||
const featureFlags = {
|
||||
enableThemeSelection: true, // Toggle this to true or false to enable/disable the feature
|
||||
};
|
||||
|
||||
export default featureFlags;
|
7
util/log.ts
Normal file
7
util/log.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { logger } from "react-native-logs";
|
||||
|
||||
const log = logger.createLogger({
|
||||
severity: "error"
|
||||
});
|
||||
|
||||
export default log;
|
Loading…
Reference in New Issue
Block a user