Add countdown.

This commit is contained in:
2024-10-29 11:23:29 -05:00
parent e84597883b
commit 4fb9e60c6c
13 changed files with 511 additions and 171 deletions

View File

@ -1,8 +1,10 @@
import React from 'react';
import * as AppleAuthentication from 'expo-apple-authentication';
import { StyleSheet, Alert } from 'react-native';
import { ThemedView } from '@/components/theme/Theme';
import { StyleSheet, Alert, ImageBackground } from 'react-native';
import { ThemedText, ThemedView } from '@/components/theme/Theme';
import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context';
import { useColorScheme } from '@/hooks/useColorScheme';
import { Colors } from '@/constants/Colors';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { saveUser, saveInitialData } from '@/components/services/SecureStore';
@ -13,6 +15,7 @@ import {
} from '@/constants/APIs';
import type { InitialData, User } from '@/constants/Types';
const SignInScreen = ({onSignIn}: {onSignIn: () => void}) => {
const scheme = useColorScheme() ?? 'dark';
@ -86,16 +89,27 @@ const SignInScreen = ({onSignIn}: {onSignIn: () => void}) => {
return (
<ThemedView style={styles.container}>
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={(scheme === 'light') ?
AppleAuthentication.AppleAuthenticationButtonStyle.BLACK :
AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
}
cornerRadius={5}
style={styles.button}
onPress={handleAppleSignIn}
/>
<ImageBackground
source={require('@/assets/images/splash.png')} resizeMode="cover"
style={styles.background}
>
<ThemedText style={[
styles.title,
{textShadowColor: Colors[scheme].background}
]}>
Wavelength
</ThemedText>
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={(scheme === 'light') ?
AppleAuthentication.AppleAuthenticationButtonStyle.BLACK :
AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
}
cornerRadius={5}
style={styles.button}
onPress={handleAppleSignIn}
/>
</ImageBackground>
</ThemedView>
);
};
@ -104,9 +118,23 @@ export default SignInScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
background: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 48,
lineHeight: 48,
fontWeight: 'bold',
marginBottom: 80,
textShadowOffset: {
width: 1,
height: 1,
},
textShadowRadius: 5,
},
button: {
width: 200,
height: 45,

View File

@ -1,7 +1,207 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
import { ThemedText, ThemedView } from '@/components/theme/Theme';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Countdown, Relationship, User } from '@/constants/Types';
import { getCountdown } from '@/constants/APIs';
import TextButton from '@/components/theme/buttons/TextButton';
import {
getCountdown as getCountdownFromSecureStore,
getUser,
getRelationship,
saveCountdown,
} from '@/components/services/SecureStore';
import CountdownChangeDateModal from '@/components/home/CountdownChangeDateModal';
const CountdownView = () => {
const [countdownData, setCountdownData] = useState({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});
const [countdown, setCountdown] = useState<Countdown | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isDateModalOpen, setIsDateModalOpen] = useState(false);
const [user, setUser] = useState<User | null>(null);
const [relationship, setRelationship] = useState<Relationship | null>(null);
useEffect(() => {
const loadData = async () => {
setIsLoading(true);
const userData = await getUser();
setUser(userData);
const relationshipData = await getRelationship();
setRelationship(relationshipData);
const countdownFromSecureStore = await getCountdownFromSecureStore();
if (countdownFromSecureStore) {
setCountdown(countdownFromSecureStore);
} else if (userData) {
const countdownFromServer = await getCountdown(userData.id);
if (countdownFromServer) {
setCountdown(countdownFromServer);
await saveCountdown(countdownFromServer);
}
}
setIsLoading(false);
};
loadData();
}, []);
useEffect(() => {
if (countdown === null) return;
const interval = setInterval(() => {
const now = new Date();
const diff = new Date(countdown.date).getTime() - now.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
setCountdownData({ days, hours, minutes, seconds });
if (diff <= 0) {
clearInterval(interval);
setCountdownData({ days: 0, hours: 0, minutes: 0, seconds: 0 });
}
}, 1000);
return () => clearInterval(interval);
}, [countdown]);
const handleCountdownUpdate = async (newDate: Date) => {
if (relationship) {
const newCountdown: Countdown = countdown
? { ...countdown, date: newDate }
: {
id: 0, // This will be set by the server
relationshipId: relationship.id,
title: 'Countdown to Next Visit',
date: newDate,
createdAt: new Date(),
};
setCountdown(newCountdown);
await saveCountdown(newCountdown);
}
};
if (isLoading) {
return (
<ThemedView style={styles.container}>
<ActivityIndicator size='large' color='#0000ff' />
</ThemedView>
);
}
if (!relationship) {
return (
<ThemedView style={styles.container}>
<ThemedText>You are not in a relationship yet.</ThemedText>
</ThemedView>
);
}
if (!countdown) {
return (
<ThemedView style={styles.container}>
<ThemedText>No countdown set yet.</ThemedText>
<TextButton
width={320} height={68}
text='Set Countdown'
fontSize={24}
onPress={() => setIsDateModalOpen(true)}
/>
</ThemedView>
);
}
return (
<ThemedView style={styles.innerContainer}>
<ThemedText style={styles.title}>
{countdown?.title ?? 'Countdown til Next Visit'}
</ThemedText>
<ThemedView style={styles.countdownContainer}>
<CountdownItem
value={countdownData.days}
label={countdownData.days === 1 ? 'Day' : 'Days'}
/>
<CountdownItem
value={countdownData.hours}
label={countdownData.hours === 1 ? 'Hour' : 'Hours'}
/>
<CountdownItem
value={countdownData.minutes}
label={countdownData.minutes === 1 ? 'Minute' : 'Minutes'}
/>
<CountdownItem
value={countdownData.seconds}
label={countdownData.seconds === 1 ? 'Second' : 'Seconds'}
/>
</ThemedView>
<TextButton
width={320} height={68}
text='Change Date'
fontSize={24}
onPress={() => setIsDateModalOpen(true)}
/>
{user && countdown && (
<CountdownChangeDateModal
user={user}
isVisible={isDateModalOpen}
onClose={() => setIsDateModalOpen(false)}
onDateChange={handleCountdownUpdate}
currentCountdown={countdown}
/>
)}
</ThemedView>
);
};
export default CountdownView;
const CountdownItem = ({value, label}: { value: number, label: string }) => {
return (
<ThemedView style={styles.countdownItem}>
<ThemedText style={styles.countdownValue}>{value}</ThemedText>
<ThemedText style={styles.countdownLabel}>{label}</ThemedText>
</ThemedView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'transparent',
},
innerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 10,
backgroundColor: 'transparent',
},
title: {
fontSize: 32,
lineHeight: 32,
fontWeight: '600',
textAlign: 'center',
marginHorizontal: 'auto',
},
countdownContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
width: '100%',
backgroundColor: 'transparent',
marginVertical: 20,
},
countdownItem: {
alignItems: 'center',
marginHorizontal: 10,
backgroundColor: 'transparent',
},
countdownValue: {
fontSize: 32,
lineHeight: 42,
fontWeight: 'bold',
},
countdownLabel: {
fontSize: 18,
lineHeight: 24,
},
});

View File

@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { StyleSheet, Modal } from 'react-native';
import { ThemedText, ThemedView } from '@/components/theme/Theme';
import DateTimePicker from '@react-native-community/datetimepicker';
import { Countdown, User } from '@/constants/Types';
import { setCountdown } from '@/constants/APIs';
import TextButton from '@/components/theme/buttons/TextButton';
type ChangeDateModalProps = {
user: User;
isVisible: boolean;
onClose: () => void;
onDateChange: (date: Date) => void;
currentCountdown?: Countdown;
};
const ChangeDateModal = ({
user, isVisible, onClose, onDateChange, currentCountdown
}: ChangeDateModalProps) => {
const [date, setDate] = useState(currentCountdown ? new Date(currentCountdown.date) : new Date());
const handleDateChange = (event: any, selectedDate?: Date) => {
const currentDate = selectedDate ?? date;
setDate(currentDate);
};
const handleTimeChange = (event: any, selectedTime?: Date) => {
const currentTime = selectedTime ?? date;
setDate(currentTime);
};
const handleSave = async () => {
try {
let updatedCountdown: Countdown;
if (currentCountdown) {
updatedCountdown = { ...currentCountdown, date: date };
await setCountdown(user.id, updatedCountdown);
onDateChange(date);
onClose();
}
} catch (error) {
console.error('Error saving countdown:', error);
}
};
return (
<Modal
animationType="slide"
transparent={true}
visible={isVisible}
onRequestClose={onClose}
>
<ThemedView style={styles.centeredView}>
<ThemedView style={styles.modalView}>
<ThemedText style={styles.modalText}>
Set New Countdown
</ThemedText>
<ThemedView style={styles.container}>
<ThemedView style={styles.dateContainer}>
<DateTimePicker
testID="datePicker"
value={date}
mode="date"
is24Hour={true}
display="default"
onChange={handleDateChange}
/>
</ThemedView>
<ThemedView style={styles.timeContainer}>
<DateTimePicker
testID="timePicker"
value={date}
mode="time"
is24Hour={true}
display="default"
onChange={handleTimeChange}
/>
</ThemedView>
</ThemedView>
<ThemedView style={styles.buttonContainer}>
<TextButton
width={120}
height={60}
text='Save'
fontSize={18}
onPress={handleSave}
/>
<TextButton
width={120}
height={60}
text='Cancel'
fontSize={18}
onPress={onClose}
/>
</ThemedView>
</ThemedView>
</ThemedView>
</Modal>
);
};
export default ChangeDateModal;
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
},
modalView: {
margin: 10,
padding: 35,
borderRadius: 40,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
width: '80%',
},
modalText: {
marginBottom: 20,
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
lineHeight: 32,
},
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
margin: 'auto',
marginBottom: 20,
backgroundColor: 'transparent',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
margin: 'auto',
backgroundColor: 'transparent',
},
dateContainer: {
width: '50%',
justifyContent: 'center',
alignItems: 'center',
margin: 'auto',
backgroundColor: 'transparent',
},
timeContainer: {
width: '50%',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
minWidth: 120,
},
});

View File

@ -1,74 +0,0 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet, Alert } from 'react-native';
import { ThemedText } from '@/components/theme/Theme';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import Button from '@/components/theme/buttons/DefaultButton';
import { getUser } from '@/components/services/SecureStore';
import { sendPushNotification } from '@/components/services/notifications/PushNotificationManager';
import type { NotificationMessage } from '@/constants/Types';
const TestPush = () => {
const scheme = useColorScheme() ?? 'dark';
const [pushToken, setPushToken] = useState<string | null>(null);
useEffect(() => {
const fetchUserData = async () => {
const user = await getUser();
if (user) {
setPushToken(user.pushToken);
}
};
fetchUserData();
}, []);
const message: NotificationMessage = {
sound: 'default',
title: 'Test push notification',
body: 'This is a test push notification',
data: {
test: 'test',
},
};
const handleSendPushNotification = async () => {
try {
await sendPushNotification(pushToken, message);
Alert.alert('Success', 'Push notification sent successfully.');
} catch (error) {
Alert.alert('Error', 'Failed to send push notification.');
}
};
return (
<Button
width={220} height={60}
onPress={handleSendPushNotification}
>
<FontAwesome
name='bell' size={18}
color={Colors[scheme].background}
style={styles.buttonIcon}
/>
<ThemedText
style={[
styles.buttonLabel,
{color: Colors[scheme].background}
]}
>
Send Push Notification
</ThemedText>
</Button>
);
};
export default TestPush;
const styles = StyleSheet.create({
buttonLabel: {
fontSize: 16,
},
buttonIcon: {
paddingRight: 8,
},
});

View File

@ -2,6 +2,7 @@ import { StyleSheet, Pressable } from "react-native";
import { ThemedView } from "@/components/theme/Theme";
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { StyleProp, ViewStyle } from 'react-native';
const DEFAULT_WIDTH = 320;
const DEFAULT_HEIGHT = 68;