From 0f8c9b5e557d333b3869d1da3afadeafe73d5ea0 Mon Sep 17 00:00:00 2001 From: Nurmuhammet Allanov Date: Sat, 16 Aug 2025 22:03:11 +0500 Subject: [PATCH] salah times done maybe --- components/PrayerTimeCard.tsx | 120 +++++++++-- package-lock.json | 6 + package.json | 1 + utils/prayerTimeCalculator.ts | 364 ++++++++++++++++++++++++++++++++++ 4 files changed, 474 insertions(+), 17 deletions(-) create mode 100644 utils/prayerTimeCalculator.ts diff --git a/components/PrayerTimeCard.tsx b/components/PrayerTimeCard.tsx index 3c613c5..4b660f5 100644 --- a/components/PrayerTimeCard.tsx +++ b/components/PrayerTimeCard.tsx @@ -1,19 +1,84 @@ -import React from 'react'; -import { View, Text, StyleSheet, ImageBackground } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { View, Text, StyleSheet, ImageBackground, Pressable } from 'react-native'; import Colors from '@/constants/Colors'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import prayerTimeCalculator, { cities } from '@/utils/prayerTimeCalculator'; -const prayerTimes = [ - { name: 'Al-fajr', time: '04:48 AM', icon: 'weather-sunset-up' }, - { name: 'Sunrise', time: '06:20 AM', icon: 'weather-sunny' }, - { name: 'Dhohr', time: '01:06 PM', icon: 'weather-partly-cloudy' }, - { name: 'Asr', time: '04:50 PM', icon: 'weather-cloudy' }, - { name: 'Maghreb', time: '07:51 PM', icon: 'weather-sunset-down' }, - { name: 'Isha', time: '09:02 PM', icon: 'weather-night' }, -]; +type Prayer = { + name: string; + time: string; + icon: string; +}; + +const prayerIconMapping: { [key: string]: string } = { + fajr: 'weather-sunset-up', + sunrise: 'weather-sunny', + dhuhr: 'weather-partly-cloudy', + asr: 'weather-cloudy', + maghrib: 'weather-sunset-down', + isha: 'weather-night', +}; export default function PrayerTimeCard() { - const colorScheme = 'dark'; + const [prayerTimes, setPrayerTimes] = useState([]); + const [nextPrayer, setNextPrayer] = useState<{ name: string; time: string } | null>(null); + const [remainingTime, setRemainingTime] = useState(''); + const [selectedCity, setSelectedCity] = useState<'Makkah' | 'Medina' | 'Jeddah'>('Makkah'); + + useEffect(() => { + const city = cities[selectedCity]; + const times = prayerTimeCalculator.getTimes(new Date(), city.coords, city.timezone, 0, '24h'); + + const prayers: Prayer[] = Object.keys(prayerIconMapping).map(key => ({ + name: key.charAt(0).toUpperCase() + key.slice(1), + time: times[key] || '-----', + icon: prayerIconMapping[key], + })); + + setPrayerTimes(prayers); + + // Find next prayer and calculate remaining time + const now = new Date(); + let nextPrayerInfo = null; + for (const prayer of prayers) { + const [hours, minutes] = prayer.time.split(':').map(Number); + const prayerDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes); + if (prayerDate > now) { + nextPrayerInfo = { name: prayer.name, time: prayer.time }; + break; + } + } + + if (!nextPrayerInfo && prayers.length > 0) { + // If all prayers for today have passed, next prayer is Fajr of tomorrow + nextPrayerInfo = { name: prayers[0].name, time: prayers[0].time }; + } + + setNextPrayer(nextPrayerInfo); + + }, [selectedCity]); + + useEffect(() => { + if (!nextPrayer) return; + + const interval = setInterval(() => { + const now = new Date(); + const [hours, minutes] = nextPrayer.time.split(':').map(Number); + let prayerDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes); + + if (prayerDate < now) { + prayerDate.setDate(prayerDate.getDate() + 1); + } + + const diff = prayerDate.getTime() - now.getTime(); + const h = Math.floor(diff / (1000 * 60 * 60)); + const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const s = Math.floor((diff % (1000 * 60)) / 1000); + setRemainingTime(`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`); + }, 1000); + + return () => clearInterval(interval); + }, [nextPrayer]); return ( - - Mashhad, Iran + + {Object.keys(cities).map(city => ( + setSelectedCity(city as any)} style={[styles.cityButton, selectedCity === city && styles.activeCity]}> + {city} + + ))} + - Left on Maghreb prayer - 49:44 + + {nextPrayer ? `Left on ${nextPrayer.name} prayer` : 'Prayer Times'} + + {remainingTime} {prayerTimes.map((prayer, index) => ( @@ -56,10 +128,24 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', marginBottom: 20, + justifyContent: 'center', }, - location: { + citySelector: { + flexDirection: 'row', + backgroundColor: Colors.dark.secondary, + borderRadius: 10, + }, + cityButton: { + paddingVertical: 8, + paddingHorizontal: 15, + borderRadius: 10, + }, + activeCity: { + backgroundColor: Colors.dark.tint, + }, + cityButtonText: { color: Colors.dark.text, - marginLeft: 10, + fontWeight: 'bold', }, remainingTimeLabel: { color: Colors.dark.textSecondary, diff --git a/package-lock.json b/package-lock.json index 3c8a49d..2ca7522 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "expo-status-bar": "~2.2.3", "expo-system-ui": "~5.0.10", "expo-web-browser": "~14.2.0", + "hijri-date": "^0.2.2", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", @@ -5549,6 +5550,11 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hijri-date": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/hijri-date/-/hijri-date-0.2.2.tgz", + "integrity": "sha512-LiuslQTPqePwh/KguBB2KH/hzISQd7nQtfVPwdCKgfFv0GDzwjIeTQX/LkBKLI8rPeuXasr3sQMuJL35g8HEAw==" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", diff --git a/package.json b/package.json index 33d15e3..f2b76a3 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "expo-status-bar": "~2.2.3", "expo-system-ui": "~5.0.10", "expo-web-browser": "~14.2.0", + "hijri-date": "^0.2.2", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", diff --git a/utils/prayerTimeCalculator.ts b/utils/prayerTimeCalculator.ts new file mode 100644 index 0000000..f0cd71f --- /dev/null +++ b/utils/prayerTimeCalculator.ts @@ -0,0 +1,364 @@ +// PrayTimes-JS: Prayer Times Calculator (ver 2.3) +// https://github.com/abodeo/prayertimes + +//--------------------- Copyright Block ---------------------- +/* + + PrayTimes-JS: Prayer Times Calculator (ver 2.3) + Copyright (C) 2007-2011 PrayTimes.org + + JS Code By: Hussain Ali Khan + Original JS Code By: Hamid Zarrabi-Zadeh + + Original C++ Code By: Hamid Zarrabi-Zadeh + + License: GNU LGPL v3.0 + + TERMS OF USE: + Permission is granted to use this code, with or without + modification, in any website or application provided that + this copyright block is reproduced in its entirety. This + notice must be placed at the beginning of the code block + and may not be altered in any way. This notice must remain + as part of the code block at all times. +*/ + +import HijriDate from 'hijri-date'; + +type PrayerTimes = { + fajr: string; + sunrise: string; + dhuhr: string; + asr: string; + sunset: string; + maghrib: string; + isha: string; + imsak?: string; + midnight?: string; + [key: string]: string | undefined; +}; + +type Coordinates = [number, number]; + +type DateComponents = { + year: number; + month: number; + day: number; +}; + +type CalculationMethod = { + name: string; + params: { + fajr: number | string; + isha: number | string; + maghrib?: number | string; + midnight?: string; + }; +}; + +type TimeNames = + | 'imsak' + | 'fajr' + | 'sunrise' + | 'dhuhr' + | 'asr' + | 'sunset' + | 'maghrib' + | 'isha' + | 'midnight'; + +class PrayTimes { + private lat: number = 0; + private lng: number = 0; + private elv: number = 0; + private timeZone: number = 0; + private jDate: number = 0; + + private calcMethod: string = 'MWL'; + private setting: { [key: string]: any } = {}; + + private methods: { [key: string]: CalculationMethod } = { + MWL: { + name: 'Muslim World League', + params: { fajr: 18, isha: 17 }, + }, + ISNA: { + name: 'Islamic Society of North America (ISNA)', + params: { fajr: 15, isha: 15 }, + }, + Egypt: { + name: 'Egyptian General Authority of Survey', + params: { fajr: 19.5, isha: 17.5 }, + }, + Makkah: { + name: 'Umm Al-Qura University, Makkah', + params: { fajr: 18.5, isha: '90 min' }, + }, + Karachi: { + name: 'University of Islamic Sciences, Karachi', + params: { fajr: 18, isha: 18 }, + }, + Tehran: { + name: 'Institute of Geophysics, University of Tehran', + params: { fajr: 17.7, isha: 14, maghrib: 4.5, midnight: 'Jafari' }, + }, + Jafari: { + name: 'Shia Ithna-Ashari, Leva Institute, Qum', + params: { fajr: 16, isha: 14, maghrib: 4, midnight: 'Jafari' }, + }, + }; + + private defaultParams = { + maghrib: '0 min', + midnight: 'Standard', + }; + + private timeSuffixes: string[] = ['am', 'pm']; + private invalidTime: string = '-----'; + + private numIterations: number = 1; + private offset: { [key: string]: number } = {}; + + private timeNames: { [key in TimeNames]: string } = { + imsak: 'Imsak', + fajr: 'Fajr', + sunrise: 'Sunrise', + dhuhr: 'Dhuhr', + asr: 'Asr', + sunset: 'Sunset', + maghrib: 'Maghrib', + isha: 'Isha', + midnight: 'Midnight', + }; + + constructor(method: string = 'Makkah') { + this.setMethod(method); + } + + setMethod(method: string): void { + if (this.methods[method]) { + this.calcMethod = method; + const params = this.methods[method].params; + this.setting = { ...this.defaultParams, ...params }; + } + } + + getTimes( + date: Date, + coords: Coordinates, + timezone: number, + elv: number = 0, + format: string = '24h', + ): PrayerTimes { + this.lat = coords[0]; + this.lng = coords[1]; + this.elv = elv; + this.timeZone = timezone; + this.jDate = this.julian(date.getFullYear(), date.getMonth() + 1, date.getDate()) - this.lng / (15 * 24); + + return this.computeTimes(format); + } + + private julian(year: number, month: number, day: number): number { + if (month <= 2) { + year -= 1; + month += 12; + } + const A = Math.floor(year / 100); + const B = 2 - A + Math.floor(A / 4); + return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + B - 1524.5; + } + + private computeTimes(format: string): PrayerTimes { + const times: { [key: string]: number } = { + imsak: 5, + fajr: 5, + sunrise: 6, + dhuhr: 12, + asr: 13, + sunset: 18, + maghrib: 18, + isha: 18, + }; + + const dayPortion = this.sunPosition(this.jDate + 0.5); + + times.dhuhr = this.midDay(this.jDate); + times.sunrise = this.sunAngleTime(this.jDate, this.riseSetAngle(), 'ccw'); + times.sunset = this.sunAngleTime(this.jDate, this.riseSetAngle(), 'cw'); + times.asr = this.asrTime(this.jDate, 1); + times.maghrib = this.sunAngleTime(this.jDate, this.eval(this.setting.maghrib), 'cw'); + + if (this.calcMethod === 'Makkah') { + const gDate = new Date((this.jDate - 2440587.5) * 86400000); + const hijriDate = new HijriDate(gDate.getTime()); + const isRamadan = hijriDate.getMonth() === 9; // Ramadan is the 9th month in Hijri + // For Umm al-Qura, Isha is 90 minutes after Maghrib, 120 during Ramadan + times.isha = times.maghrib + (isRamadan ? 2 : 1.5); + } else { + times.isha = this.sunAngleTime(this.jDate, this.eval(this.setting.isha), 'cw'); + } + + times.fajr = this.sunAngleTime(this.jDate, this.eval(this.setting.fajr), 'ccw'); + + const result = this.adjustTimes(times); + return this.formatTimes(result, format); + } + + private sunPosition(jd: number): [number, number] { + const D = jd - 2451545.0; + const g = this.fixAngle(357.529 + 0.98560028 * D); + const q = this.fixAngle(280.459 + 0.98564736 * D); + const L = this.fixAngle(q + 1.915 * this.dSin(g) + 0.02 * this.dSin(2 * g)); + + const R = 1.00014 - 0.01671 * this.dCos(g) - 0.00014 * this.dCos(2 * g); + const e = 23.439 - 0.00000036 * D; + + const RA = this.dAtan2(this.dCos(e) * this.dSin(L), this.dCos(L)) / 15; + const dec = this.dAsin(this.dSin(e) * this.dSin(L)); + const EqT = q / 15 - this.fixHour(RA); + + return [dec, EqT]; + } + + private sunAngleTime(jd: number, angle: number, direction: string): number { + const [dec, EqT] = this.sunPosition(jd); + const t = + (1 / 15) * + this.dACos( + (-this.dSin(angle) - this.dSin(dec) * this.dSin(this.lat)) / + (this.dCos(dec) * this.dCos(this.lat)), + ); + return this.midDay(jd) + (direction === 'cw' ? t : -t) - EqT; + } + + private asrTime(jd: number, factor: number): number { + const [dec] = this.sunPosition(jd); + const angle = -this.dACot(factor + this.dTan(Math.abs(dec - this.lat))); + return this.sunAngleTime(jd, angle, 'cw'); + } + + private riseSetAngle(): number { + return 0.833 + 0.0347 * Math.sqrt(this.elv); + } + + private midDay(jd: number): number { + const [, EqT] = this.sunPosition(jd); + return 12 - EqT; + } + + private adjustTimes(times: { [key: string]: number }): { [key: string]: number } { + const adjustedTimes = { ...times }; + for (const i in adjustedTimes) { + adjustedTimes[i] += this.timeZone - this.lng / 15; + } + return adjustedTimes; + } + + private formatTimes(times: { [key: string]: number }, format: string): PrayerTimes { + const formattedTimes: PrayerTimes = {} as PrayerTimes; + for (const i in times) { + if (format === '24h') { + formattedTimes[i] = this.floatToTime24(times[i]); + } else { + formattedTimes[i] = this.floatToTime12(times[i]); + } + } + return formattedTimes; + } + + private floatToTime24(time: number): string { + if (isNaN(time)) { + return this.invalidTime; + } + time = this.fixHour(time + 0.5 / 60); // add 0.5 minutes to round + const hours = Math.floor(time); + const minutes = Math.floor((time - hours) * 60); + return this.twoDigitsFormat(hours) + ':' + this.twoDigitsFormat(minutes); + } + + private floatToTime12(time: number, noSuffix: boolean = false): string { + if (isNaN(time)) { + return this.invalidTime; + } + time = this.fixHour(time + 0.5 / 60); // add 0.5 minutes to round + let hours = Math.floor(time); + const minutes = Math.floor((time - hours) * 60); + const suffix = hours >= 12 ? 'pm' : 'am'; + hours = (hours + 12 - 1) % 12 + 1; + return hours + ':' + this.twoDigitsFormat(minutes) + (noSuffix ? '' : ' ' + suffix); + } + + private twoDigitsFormat(num: number): string { + return num < 10 ? '0' + num : num.toString(); + } + + private eval(str: string | number | undefined): number { + if (str === undefined) { + return 0; + } + if (typeof str === 'number') { + return str; + } + const [value, unit] = str.split(' '); + return unit === 'min' ? parseInt(value) / 60 : parseInt(value); + } + + private dSin(d: number): number { + return Math.sin(this.dToR(d)); + } + private dCos(d: number): number { + return Math.cos(this.dToR(d)); + } + private dTan(d: number): number { + return Math.tan(this.dToR(d)); + } + private dAsin(d: number): number { + return this.rToD(Math.asin(d)); + } + private dACos(d: number): number { + return this.rToD(Math.acos(d)); + } + private dAtan(d: number): number { + return this.rToD(Math.atan(d)); + } + private dAtan2(y: number, x: number): number { + return this.rToD(Math.atan2(y, x)); + } + private dACot(x: number): number { + return this.rToD(Math.atan(1 / x)); + } + + private dToR(d: number): number { + return (d * Math.PI) / 180.0; + } + private rToD(r: number): number { + return (r * 180.0) / Math.PI; + } + + private fixAngle(a: number): number { + a = a - 360.0 * Math.floor(a / 360.0); + return a < 0 ? a + 360.0 : a; + } + private fixHour(a: number): number { + a = a - 24.0 * Math.floor(a / 24.0); + return a < 0 ? a + 24.0 : a; + } +} + +export const cities = { + Makkah: { + coords: [21.4225, 39.8262] as Coordinates, + timezone: 3, + }, + Medina: { + coords: [24.4667, 39.6] as Coordinates, + timezone: 3, + }, + Jeddah: { + coords: [21.5433, 39.1728] as Coordinates, + timezone: 3, + }, +}; + +export default new PrayTimes('Makkah');