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.
@@ -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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 { differenceInSeconds, format, parse, subDays } from "date-fns";
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
- // Calculate progress
51
- const prayerOrder = ["Fajr", "Chorouq", "Dhuhr", "Asr", "Maghrib", "Ishae"];
52
- const nextIndex = prayerOrder.indexOf(nextPrayerData.prayer);
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 data = await getData(cityId);
14
- return data;
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 = new Date().getDate();
5
- const month = new Date().getMonth() + 1;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salat",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "imports": {
5
5
  "#*": "./dist/*.js"
6
6
  },