307 lines
9.3 KiB
TypeScript

import dayjs, { Dayjs } from 'dayjs';
import React, { useEffect, useState, useRef } from 'react';
import { View, Text, FlatList, StyleSheet, Alert } from 'react-native';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';
type PrayerTime = {
name: string;
time: string;
};
type PrayerTimeDate = {
name: string;
time: Dayjs;
};
type PrayerData = {
location: string;
date: string;
times: PrayerTime[];
timesAsDates: PrayerTimeDate[];
sunriseTime: string;
hanafiAsr: string;
};
export default function PrayersScreen() {
const [prayerData, setPrayerData] = useState<PrayerData | null>(null);
const [nextPrayer, setNextPrayer] = useState<{ name: string; countdown: string } | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const getCoords = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
throw new Error('Permission to access location was denied');
}
const location = await Location.getCurrentPositionAsync({});
return location.coords;
};
const fetchPrayerTimes = async () => {
try {
const coords = await getCoords();
const today = dayjs();
const yyyy = today.year();
const mm = String(today.month() + 1).padStart(2, '0'); // month() is 0-based
const dd = String(today.date()).padStart(2, '0');
const formattedDate = `${yyyy}-${mm}-${dd}`;
const res = await fetch(
`https://api.aladhan.com/v1/timings/${formattedDate}?latitude=${coords.latitude}&longitude=${coords.longitude}&method=2`);
const hanafiRes = await fetch(
`https://api.aladhan.com/v1/timings/${formattedDate}?latitude=${coords.latitude}&longitude=${coords.longitude}&method=2&school=1`);
const data = await res.json();
const hanafiData = await hanafiRes.json();
const hanafiTiming = hanafiData.data.timings.Asr;
const timings = data.data.timings;
const formattedTimes = [
{ name: 'Fajr', time: timings.Fajr },
{ name: 'Dhuhr', time: timings.Dhuhr },
{ name: 'Asr', time: timings.Asr },
{ name: 'Maghrib', time: timings.Maghrib },
{ name: 'Isha', time: timings.Isha },
];
setPrayerData({
location: data.data.meta.timezone,
date: `${data.data.date.gregorian.date} / ${data.data.date.hijri.date}`,
times: formattedTimes,
timesAsDates: getPrayerDateTimes(formattedTimes),
sunriseTime: timings.Sunrise,
hanafiAsr: hanafiTiming
});
} catch (err) {
console.error('Error fetching prayer times:', err);
Alert.alert('Error', 'Failed to fetch prayer times.');
}
};
// Helper: convert prayer times into Date objects for today
const getPrayerDateTimes = (times: PrayerTime[]) => {
const now = dayjs();
return times.map(({ name, time }) => {
const [hours, minutes] = time.split(':').map(Number);
// Create a Dayjs object for today with set hours and minutes
const prayerTime = now.hour(hours).minute(minutes).second(0).millisecond(0);
return { name, time: prayerTime };
});
};
const schedulePrayerNotification = async (prayerName: string, prayerTime: dayjs.Dayjs) => {
const triggerTime = prayerTime.subtract(10, 'minute');
await Notifications.scheduleNotificationAsync({
content: {
title: `Upcoming Prayer: ${prayerName}`,
body: `Time for ${prayerName} is in 10 minutes.`,
sound: true,
},
trigger: {
type: 'date',
date: triggerTime.toDate(),
} as Notifications.DateTriggerInput,
});
};
const isCurrentPrayer = (prayerTimes: PrayerData, prayerName: string): boolean => {
const times = prayerTimes.timesAsDates;
const now = dayjs();
// Sort times just in case
times.sort((a, b) => a.time.valueOf() - b.time.valueOf());
for (let i = times.length - 1; i >= 0; i--) {
if (now.isAfter(times[i].time) || now.isSame(times[i].time)) {
// Check if the current prayer matches the prayerName
return times[i].name === prayerName;
}
}
// If none matched, compare to last prayer (optional fallback)
return times.length > 0 ? times[times.length - 1].name === prayerName : false;
};
// Find the next prayer based on current time
const findNextPrayer = (prayerTimes: PrayerTimeDate[]) => {
const now = dayjs();
// Find next prayer with time > now
const next = prayerTimes.find((p) => dayjs(p.time).isAfter(now));
if (next) {
return next;
}
// If none found, all prayers passed — fallback to first prayer tomorrow (Fajr)
// So add 1 day to first prayer time
const tomorrowFajr = prayerTimes[0].time.add(1, 'day');
return { name: prayerTimes[0].name, time: tomorrowFajr };
};
// Update countdown every second
const startCountdown = (prayerTimes: PrayerTimeDate[]) => {
if (intervalRef.current) clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
const now = dayjs();
const next = findNextPrayer(prayerTimes);
const diff = next.time.diff(now);
// If countdown hits zero or negative, just continue to next prayer immediately
const safeDiff = diff > 0 ? diff : 0;
const hrs = String(Math.floor(safeDiff / (1000 * 60 * 60))).padStart(2, '0');
const mins = String(Math.floor((safeDiff / (1000 * 60)) % 60)).padStart(2, '0');
const secs = String(Math.floor((safeDiff / 1000) % 60)).padStart(2, '0');
setNextPrayer({
name: next.name,
countdown: `${hrs}:${mins}:${secs}`,
});
}, 1000) as any;
};
// Initial fetch
useEffect(() => {
fetchPrayerTimes();
}, []);
// Start countdown when prayerData changes
useEffect(() => {
if (prayerData?.times) {
startCountdown(prayerData.timesAsDates);
const now = dayjs();
// Only schedule notifications for prayers that haven't passed yet
prayerData.timesAsDates
.filter(prayer => prayer.time.isAfter(now))
.forEach(prayer => {
schedulePrayerNotification(prayer.name, prayer.time);
});
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [prayerData]);
return (
<View style={styles.container}>
<Text style={styles.title}>Today's Prayer Times</Text>
{prayerData && (
<>
<Text style={styles.date}>{prayerData.date}</Text>
</>
)}
{nextPrayer ? (
<Text style={styles.nextPrayer}>
Next prayer: {nextPrayer.name} in {nextPrayer.countdown}
</Text>
) : (
<Text style={styles.nextPrayer}>Calculating next prayer...</Text>
)}
{prayerData ? (
<FlatList
data={prayerData.times}
keyExtractor={(item) => item.name}
renderItem={({ item }) => {
const isCurrent = isCurrentPrayer(prayerData, item.name);
return (
<View style={[styles.card, isCurrent && styles.currentPrayerCard]}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text style={styles.prayerName}>{item.name}</Text>
<Text style={styles.prayerTime}>{item.time}</Text>
</View>
{/* Fajr Sunrise */}
{item.name === 'Fajr' && prayerData.sunriseTime && (
<View style={styles.sunriseRow}>
<Text style={styles.subtitle}>Sunrise</Text>
<Text style={styles.subtitle}>{prayerData.sunriseTime}</Text>
</View>
)}
{/* Asr (Hanafi) */}
{item.name === 'Asr' && prayerData.hanafiAsr && (
<View style={styles.sunriseRow}>
<Text style={styles.subtitle}>Asr (Hanafi)</Text>
<Text style={styles.subtitle}>{prayerData.hanafiAsr}</Text>
</View>
)}
</View>
);
}}
contentContainerStyle={{ paddingVertical: 10 }}
/>
) : (
<Text style={{ marginTop: 20, textAlign: 'center' }}>Loading...</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, backgroundColor: '#FFEEE7' },
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
color: '#333',
marginBottom: 6, // reduce space below title
},
sunriseRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 4,
},
subtitle: {
textAlign: 'center',
fontSize: 14,
color: '#555',
},
date: {
textAlign: 'center',
fontSize: 14,
color: '#888',
marginBottom: 2, // keep this small
},
nextPrayer: {
textAlign: 'center',
fontSize: 16,
color: '#008060',
marginTop: 4, // reduce gap above next prayer
marginBottom: 10,
fontWeight: '600',
},
card: {
padding: 15,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
marginBottom: 12,
backgroundColor: '#fff', // changed to white
},
currentPrayerCard: {
backgroundColor: '#fff', // keep current prayer tab white too
borderColor: '#4CAF50',
borderWidth: 2,
},
specialPrayerName: {
color: '#00796b', // Teal
},
specialPrayerTime: {
fontWeight: 'bold',
},
prayerName: { fontSize: 16, fontWeight: '600' },
prayerTime: { fontSize: 16 },
});