Add countdown.
This commit is contained in:
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
162
components/home/CountdownChangeDateModal.tsx
Normal file
162
components/home/CountdownChangeDateModal.tsx
Normal 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,
|
||||
},
|
||||
});
|
@ -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,
|
||||
},
|
||||
});
|
@ -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;
|
||||
|
Reference in New Issue
Block a user