salat 5.0.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ProgressBar.js +7 -0
- package/dist/components/RamadanInfo.js +11 -0
- package/dist/components/TimesApp.js +9 -26
- package/dist/components/TimesApp.test.js +52 -0
- package/dist/hooks/usePrayerTimes.js +9 -4
- package/dist/services/utils/api.js +3 -3
- package/dist/services/utils/time.js +48 -1
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export const ProgressBar = ({ progress, width }) => {
|
|
4
|
+
const filledWidth = Math.max(0, Math.min(width, Math.round(progress * width)));
|
|
5
|
+
const emptyWidth = width - filledWidth;
|
|
6
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "█".repeat(filledWidth) }), _jsx(Text, { color: "gray", children: "░".repeat(emptyWidth) })] }));
|
|
7
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ProgressBar } from "#components/ProgressBar";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
export const RamadanInfo = ({ data }) => {
|
|
5
|
+
if (!data)
|
|
6
|
+
return null;
|
|
7
|
+
if (data.type === "fasting") {
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "\u231B Iftar Progress" }), _jsx(Box, { marginTop: 1, children: _jsx(ProgressBar, { progress: data.progress, width: 50 }) }), _jsxs(Box, { justifyContent: "space-between", width: 50, children: [_jsx(Text, { color: "gray", dimColor: true, children: "Fajr" }), _jsxs(Box, { children: [_jsxs(Text, { color: "yellow", bold: true, children: [Math.round(data.progress * 100), "%"] }), _jsx(Text, { color: "gray", children: " to Maghrib" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "Iftar" })] })] }));
|
|
9
|
+
}
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "\uD83C\uDF19 Time to Imsak (Suhoor ends)" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", bold: true, children: data.timeLeft }) }), _jsx(Text, { dimColor: true, color: "gray", children: "until fast begins" })] }));
|
|
11
|
+
};
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ProgressBar } from "#components/ProgressBar";
|
|
3
|
+
import { RamadanInfo } from "#components/RamadanInfo";
|
|
2
4
|
import { useHijriDate } from "#hooks/useHijriDate";
|
|
3
5
|
import { usePrayerTimes } from "#hooks/usePrayerTimes";
|
|
4
|
-
import { getImsakTime, getNextPrayer, tConv24 } from "#services/utils/time";
|
|
5
|
-
import {
|
|
6
|
+
import { getImsakTime, getNextPrayer, getPrayerProgress, getRamadanData, tConv24 } from "#services/utils/time";
|
|
7
|
+
import { format } from "date-fns";
|
|
6
8
|
import { Box, Text, useApp, useInput } from "ink";
|
|
7
9
|
import { useEffect, useState } from "react";
|
|
8
|
-
const ProgressBar = ({ progress, width, }) => {
|
|
9
|
-
const filledWidth = Math.max(0, Math.min(width, Math.round(progress * width)));
|
|
10
|
-
const emptyWidth = width - filledWidth;
|
|
11
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "█".repeat(filledWidth) }), _jsx(Text, { color: "gray", children: "░".repeat(emptyWidth) })] }));
|
|
12
|
-
};
|
|
13
10
|
const App = ({ cityNameArg, once, onReset }) => {
|
|
14
11
|
const { exit } = useApp();
|
|
15
|
-
const { prayerTimes, error, loading, resolvedCityName } = usePrayerTimes({
|
|
12
|
+
const { prayerTimes, tomorrowTimes, error, loading, resolvedCityName } = usePrayerTimes({
|
|
16
13
|
cityNameArg,
|
|
17
14
|
});
|
|
18
15
|
const { hijriDate } = useHijriDate();
|
|
@@ -47,23 +44,9 @@ const App = ({ cityNameArg, once, onReset }) => {
|
|
|
47
44
|
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "red", bold: true, children: "Could not fetch prayer times." }) }));
|
|
48
45
|
}
|
|
49
46
|
const nextPrayerData = getNextPrayer(prayerTimes, currentTime);
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const prevIndex = (nextIndex - 1 + prayerOrder.length) % prayerOrder.length;
|
|
54
|
-
const prevPrayerName = prayerOrder[prevIndex];
|
|
55
|
-
let prevDate = parse(prayerTimes[prevPrayerName], "HH:mm", currentTime);
|
|
56
|
-
let nextDate = parse(nextPrayerData.time, "HH:mm", currentTime);
|
|
57
|
-
if (nextPrayerData.prayer === "Fajr" && currentTime.getHours() >= 12) {
|
|
58
|
-
nextDate = parse(nextPrayerData.time, "HH:mm", new Date(currentTime.getTime() + 24 * 60 * 60 * 1000));
|
|
59
|
-
}
|
|
60
|
-
else if (nextPrayerData.prayer === "Fajr" && currentTime.getHours() < 12) {
|
|
61
|
-
prevDate = subDays(parse(prayerTimes.Ishae, "HH:mm", currentTime), 1);
|
|
62
|
-
}
|
|
63
|
-
const totalSeconds = differenceInSeconds(nextDate, prevDate);
|
|
64
|
-
const elapsedSeconds = differenceInSeconds(currentTime, prevDate);
|
|
65
|
-
const progress = Math.max(0, Math.min(1, elapsedSeconds / totalSeconds));
|
|
66
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, borderStyle: "round", borderColor: "green", width: 60, children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", gap: 1, children: [_jsx(Box, { borderStyle: "single", borderColor: "green", children: _jsxs(Text, { bold: true, color: "green", children: ["\uD83C\uDDF2\uD83C\uDDE6 ", resolvedCityName, ", Morocco \uD83C\uDDF2\uD83C\uDDE6"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "white", bold: true, children: ["\uD83D\uDCC5 ", format(currentTime, "EEEE, MMMM do yyyy")] }) }), hijriDate && (_jsxs(Text, { color: "gray", dimColor: true, children: ["\uD83D\uDD4C ", hijriDate] }))] }), _jsx(Box, { flexDirection: "column", marginY: 1, children: Object.entries({
|
|
47
|
+
const progress = getPrayerProgress(prayerTimes, currentTime, nextPrayerData.prayer);
|
|
48
|
+
const ramadanData = getRamadanData(prayerTimes, tomorrowTimes, currentTime);
|
|
49
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, borderStyle: "round", borderColor: "green", width: 60, children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", gap: 1, children: [_jsx(Box, { borderStyle: "single", borderColor: "green", children: _jsxs(Text, { bold: true, color: "green", children: ["\uD83C\uDDF2\uD83C\uDDE6 ", resolvedCityName, ", Morocco \uD83C\uDDF2\uD83C\uDDE6"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "white", bold: true, children: ["\uD83D\uDCC5 ", format(currentTime, "EEEE, MMMM do yyyy")] }) }), hijriDate && (_jsxs(Text, { color: "gray", dimColor: true, children: ["\uD83D\uDD4C ", hijriDate] }))] }), _jsx(Box, { flexDirection: "column", alignItems: "center", marginY: 1, padding: 1, borderStyle: "single", borderColor: "yellow", children: _jsx(RamadanInfo, { data: ramadanData }) }), _jsx(Box, { flexDirection: "column", marginY: 1, children: Object.entries({
|
|
67
50
|
"Imsak *": getImsakTime(prayerTimes.Fajr),
|
|
68
51
|
...prayerTimes,
|
|
69
52
|
}).map(([prayer, time]) => {
|
|
@@ -30,6 +30,7 @@ describe("TimesApp", () => {
|
|
|
30
30
|
it("should render loading state", () => {
|
|
31
31
|
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
32
32
|
prayerTimes: null,
|
|
33
|
+
tomorrowTimes: null,
|
|
33
34
|
error: null,
|
|
34
35
|
loading: true,
|
|
35
36
|
resolvedCityName: "Marrakech",
|
|
@@ -40,6 +41,7 @@ describe("TimesApp", () => {
|
|
|
40
41
|
it("should render error state", () => {
|
|
41
42
|
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
42
43
|
prayerTimes: null,
|
|
44
|
+
tomorrowTimes: null,
|
|
43
45
|
error: "Failed to fetch",
|
|
44
46
|
loading: false,
|
|
45
47
|
resolvedCityName: "",
|
|
@@ -50,6 +52,7 @@ describe("TimesApp", () => {
|
|
|
50
52
|
it("should render null prayerTimes state", () => {
|
|
51
53
|
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
52
54
|
prayerTimes: null,
|
|
55
|
+
tomorrowTimes: null,
|
|
53
56
|
error: null,
|
|
54
57
|
loading: false,
|
|
55
58
|
resolvedCityName: "",
|
|
@@ -68,6 +71,7 @@ describe("TimesApp", () => {
|
|
|
68
71
|
};
|
|
69
72
|
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
70
73
|
prayerTimes: mockPrayerTimes,
|
|
74
|
+
tomorrowTimes: null,
|
|
71
75
|
error: null,
|
|
72
76
|
loading: false,
|
|
73
77
|
resolvedCityName: "Marrakech",
|
|
@@ -77,4 +81,52 @@ describe("TimesApp", () => {
|
|
|
77
81
|
expect(lastFrame()).toContain("Fajr");
|
|
78
82
|
expect(lastFrame()).toContain("05:00 AM");
|
|
79
83
|
});
|
|
84
|
+
it("should render Iftar progress when fasting", () => {
|
|
85
|
+
const mockPrayerTimes = {
|
|
86
|
+
Fajr: "05:00",
|
|
87
|
+
Chorouq: "06:30",
|
|
88
|
+
Dhuhr: "12:30",
|
|
89
|
+
Asr: "15:45",
|
|
90
|
+
Maghrib: "18:20",
|
|
91
|
+
Ishae: "19:50",
|
|
92
|
+
};
|
|
93
|
+
// Use a fixed date for the test
|
|
94
|
+
const mockNow = new Date("2026-02-18T12:00:00");
|
|
95
|
+
vi.setSystemTime(mockNow);
|
|
96
|
+
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
97
|
+
prayerTimes: mockPrayerTimes,
|
|
98
|
+
tomorrowTimes: mockPrayerTimes,
|
|
99
|
+
error: null,
|
|
100
|
+
loading: false,
|
|
101
|
+
resolvedCityName: "Marrakech",
|
|
102
|
+
});
|
|
103
|
+
const { lastFrame } = render(_jsx(TimesApp, {}));
|
|
104
|
+
expect(lastFrame()).toContain("Iftar Progress");
|
|
105
|
+
// (12:00 - 05:00) / (18:20 - 05:00) = 7h / 13h 20m = 420m / 800m = 0.525 = 53%
|
|
106
|
+
expect(lastFrame()).toContain("53%");
|
|
107
|
+
});
|
|
108
|
+
it("should render Time to Imsak when not fasting (after Maghrib)", () => {
|
|
109
|
+
const mockPrayerTimes = {
|
|
110
|
+
Fajr: "05:00",
|
|
111
|
+
Chorouq: "06:30",
|
|
112
|
+
Dhuhr: "12:30",
|
|
113
|
+
Asr: "15:45",
|
|
114
|
+
Maghrib: "18:20",
|
|
115
|
+
Ishae: "19:50",
|
|
116
|
+
};
|
|
117
|
+
// Use a fixed date for the test (9:00 PM)
|
|
118
|
+
const mockNow = new Date("2026-02-18T21:00:00");
|
|
119
|
+
vi.setSystemTime(mockNow);
|
|
120
|
+
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
121
|
+
prayerTimes: mockPrayerTimes,
|
|
122
|
+
tomorrowTimes: mockPrayerTimes, // Tomorrow's Fajr 05:00 -> Imsak 04:50
|
|
123
|
+
error: null,
|
|
124
|
+
loading: false,
|
|
125
|
+
resolvedCityName: "Marrakech",
|
|
126
|
+
});
|
|
127
|
+
const { lastFrame } = render(_jsx(TimesApp, {}));
|
|
128
|
+
expect(lastFrame()).toContain("Time to Imsak");
|
|
129
|
+
// From 21:00 to 04:50 is 7 hours and 50 minutes
|
|
130
|
+
expect(lastFrame()).toContain("07:50:00");
|
|
131
|
+
});
|
|
80
132
|
});
|
|
@@ -3,21 +3,26 @@ import { getCityId, getCityName } from "#services/utils/city";
|
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import citiesData from "../data/cities.json" with { type: "json" };
|
|
5
5
|
const cities = citiesData;
|
|
6
|
+
import { addDays } from "date-fns";
|
|
6
7
|
export const usePrayerTimes = ({ cityNameArg }) => {
|
|
7
8
|
const resolvedCityName = getCityName(cityNameArg, cities);
|
|
8
9
|
const query = useQuery({
|
|
9
10
|
queryKey: ["prayerTimes", resolvedCityName],
|
|
10
11
|
queryFn: async () => {
|
|
11
|
-
// Fetch fresh data
|
|
12
12
|
const cityId = getCityId(resolvedCityName, cities);
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const [today, tomorrow] = await Promise.all([
|
|
15
|
+
getData(cityId, now),
|
|
16
|
+
getData(cityId, addDays(now, 1)),
|
|
17
|
+
]);
|
|
18
|
+
return { today, tomorrow };
|
|
15
19
|
},
|
|
16
20
|
staleTime: 1000 * 60 * 60 * 24, // 24 hours
|
|
17
21
|
gcTime: 1000 * 60 * 60 * 24, // garbage collection after 24 hours
|
|
18
22
|
});
|
|
19
23
|
return {
|
|
20
|
-
prayerTimes: query.data ?? null,
|
|
24
|
+
prayerTimes: query.data?.today ?? null,
|
|
25
|
+
tomorrowTimes: query.data?.tomorrow ?? null,
|
|
21
26
|
error: query.error?.message ?? null,
|
|
22
27
|
loading: query.isPending,
|
|
23
28
|
resolvedCityName,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PRIERE_API_URL } from "#services/constants";
|
|
2
2
|
// import fetch from "node-fetch";
|
|
3
|
-
export const getData = async (cityId) => {
|
|
4
|
-
const day =
|
|
5
|
-
const month =
|
|
3
|
+
export const getData = async (cityId, date = new Date()) => {
|
|
4
|
+
const day = date.getDate();
|
|
5
|
+
const month = date.getMonth() + 1;
|
|
6
6
|
const url = PRIERE_API_URL(cityId, day, month);
|
|
7
7
|
const response = await fetch(url);
|
|
8
8
|
const data = (await response.json());
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addDays, format, intervalToDuration, parse, subMinutes } from "date-fns";
|
|
1
|
+
import { addDays, differenceInSeconds, format, intervalToDuration, parse, subDays, subMinutes } from "date-fns";
|
|
2
2
|
export function tConv24(time24) {
|
|
3
3
|
const date = parse(time24, "HH:mm", new Date());
|
|
4
4
|
return format(date, "hh:mm a");
|
|
@@ -29,3 +29,50 @@ export function getNextPrayer(prayerTimes, now) {
|
|
|
29
29
|
timeLeft,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
export function getPrayerProgress(prayerTimes, currentTime, nextPrayer) {
|
|
33
|
+
const prayerOrder = ["Fajr", "Chorouq", "Dhuhr", "Asr", "Maghrib", "Ishae"];
|
|
34
|
+
const nextIndex = prayerOrder.indexOf(nextPrayer);
|
|
35
|
+
const prevIndex = (nextIndex - 1 + prayerOrder.length) % prayerOrder.length;
|
|
36
|
+
const prevPrayerName = prayerOrder[prevIndex];
|
|
37
|
+
let prevDate = parse(prayerTimes[prevPrayerName], "HH:mm", currentTime);
|
|
38
|
+
let nextDate = parse(prayerTimes[nextPrayer], "HH:mm", currentTime);
|
|
39
|
+
if (nextPrayer === "Fajr" && currentTime.getHours() >= 12) {
|
|
40
|
+
nextDate = addDays(nextDate, 1);
|
|
41
|
+
}
|
|
42
|
+
else if (nextPrayer === "Fajr" && currentTime.getHours() < 12) {
|
|
43
|
+
prevDate = subDays(parse(prayerTimes.Ishae, "HH:mm", currentTime), 1);
|
|
44
|
+
}
|
|
45
|
+
const totalSeconds = differenceInSeconds(nextDate, prevDate);
|
|
46
|
+
const elapsedSeconds = differenceInSeconds(currentTime, prevDate);
|
|
47
|
+
const progress = Math.max(0, Math.min(1, elapsedSeconds / totalSeconds));
|
|
48
|
+
return progress;
|
|
49
|
+
}
|
|
50
|
+
export function getRamadanData(prayerTimes, tomorrowTimes, currentTime) {
|
|
51
|
+
const fajrToday = parse(prayerTimes.Fajr, "HH:mm", currentTime);
|
|
52
|
+
const maghribToday = parse(prayerTimes.Maghrib, "HH:mm", currentTime);
|
|
53
|
+
const isFasting = currentTime >= fajrToday && currentTime < maghribToday;
|
|
54
|
+
if (isFasting) {
|
|
55
|
+
const totalFastingSeconds = differenceInSeconds(maghribToday, fajrToday);
|
|
56
|
+
const elapsedFastingSeconds = differenceInSeconds(currentTime, fajrToday);
|
|
57
|
+
const fastingProgress = Math.max(0, Math.min(1, elapsedFastingSeconds / totalFastingSeconds));
|
|
58
|
+
return { type: "fasting", progress: fastingProgress };
|
|
59
|
+
}
|
|
60
|
+
if (tomorrowTimes) {
|
|
61
|
+
let nextImsakDate;
|
|
62
|
+
const imsakStr = getImsakTime(prayerTimes.Fajr);
|
|
63
|
+
const todayImsak = parse(imsakStr, "HH:mm", currentTime);
|
|
64
|
+
if (currentTime < todayImsak) {
|
|
65
|
+
nextImsakDate = todayImsak;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const tomorrowImsakStr = getImsakTime(tomorrowTimes.Fajr);
|
|
69
|
+
nextImsakDate = parse(tomorrowImsakStr, "HH:mm", addDays(currentTime, 1));
|
|
70
|
+
}
|
|
71
|
+
const duration = intervalToDuration({ start: currentTime, end: nextImsakDate });
|
|
72
|
+
const timeLeft = [duration.hours, duration.minutes, duration.seconds]
|
|
73
|
+
.map((v) => String(v ?? 0).padStart(2, "0"))
|
|
74
|
+
.join(":");
|
|
75
|
+
return { type: "imsak", timeLeft };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|