ramadan-cal 1.0.0

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # ๐ŸŒ™ ramadan-cal
2
+
3
+ A beautiful terminal Ramadan calendar with prayer times, daily progress, and Hijri date tracking.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g ramadan-cal
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ ramadan # Full display: prayer times + calendar
15
+ ramadan times # Prayer times only
16
+ ramadan calendar # Calendar only
17
+ ramadan check # Check prayer times for any city
18
+ ramadan eid 2026-03-19 # Set the Eid date when announced
19
+ ramadan reset # Clear config and start fresh
20
+ ```
21
+
22
+ ## Setup
23
+
24
+ First run auto-detects your location and asks one question โ€” when the moon was sighted. That's it.
25
+
26
+ Prayer times are calculated offline using the [adhan](https://github.com/batoulapps/adhan-js) library. No API calls needed after setup.
27
+
28
+ ## Claude Code
29
+
30
+ During installation you'll be asked if you want to set up Claude Code integration. If enabled, type `ramadan` inside any Claude Code session to see your calendar โ€” zero tokens used.
31
+
32
+ If you skipped during install, run `npm rebuild ramadan-cal` to set it up later.
33
+
34
+ ## License
35
+
36
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { getConfig, resetConfig, setEidDate } from '../src/config.js';
4
+ import { render } from '../src/display.js';
5
+ import { checkLocation } from '../src/check-location.js';
6
+
7
+ const args = process.argv.slice(2);
8
+ const command = args[0]?.toLowerCase();
9
+
10
+ async function main() {
11
+ // Handle reset
12
+ if (command === 'reset') {
13
+ resetConfig();
14
+ console.log('\nโœ… Config cleared. Run `ramadan` to set up again.\n');
15
+ process.exit(0);
16
+ }
17
+
18
+ // Handle eid date setting
19
+ if (command === 'eid') {
20
+ const dateStr = args[1];
21
+ if (!dateStr || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
22
+ console.log('\nโŒ Please provide a date: ramadan eid YYYY-MM-DD\n');
23
+ process.exit(1);
24
+ }
25
+ setEidDate(dateStr);
26
+ console.log(`\nโœ… Eid date set to ${dateStr}\n`);
27
+ process.exit(0);
28
+ }
29
+
30
+ // Handle location check
31
+ if (command === 'check') {
32
+ await checkLocation();
33
+ process.exit(0);
34
+ }
35
+
36
+ // Get or create config (will prompt on first run)
37
+ const config = await getConfig();
38
+
39
+ // Determine which view to show
40
+ const view = command === 'times' ? 'times' : command === 'calendar' ? 'calendar' : 'full';
41
+
42
+ render(config, view);
43
+ }
44
+
45
+ main().catch((err) => {
46
+ console.error('\nโŒ Something went wrong:', err.message, '\n');
47
+ process.exit(1);
48
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "ramadan-cal",
3
+ "version": "1.0.0",
4
+ "description": "๐ŸŒ™ A beautiful terminal Ramadan calendar with prayer times, daily progress, and Hijri date",
5
+ "type": "module",
6
+ "bin": {
7
+ "ramadan": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/cli.js",
11
+ "link": "npm link",
12
+ "postinstall": "node scripts/postinstall.js"
13
+ },
14
+ "keywords": [
15
+ "ramadan",
16
+ "islam",
17
+ "prayer-times",
18
+ "hijri",
19
+ "calendar",
20
+ "cli"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "scripts",
28
+ "README.md"
29
+ ],
30
+ "dependencies": {
31
+ "adhan": "^4.4.3",
32
+ "chalk": "^5.3.0",
33
+ "conf": "^12.0.0",
34
+ "prompts": "^2.4.2"
35
+ }
36
+ }
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script โ€” runs automatically after npm install.
5
+ * Interactively sets up Claude Code integration if the user wants it.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ import { execSync } from 'child_process';
12
+ import { createInterface } from 'readline';
13
+
14
+ const home = homedir();
15
+ const claudeDir = join(home, '.claude');
16
+ const claudeMd = join(claudeDir, 'CLAUDE.md');
17
+
18
+ const MARKER = '<!-- ramadan-cal -->';
19
+ const CLAUDE_ENTRY = `
20
+ ${MARKER}
21
+ ## Ramadan CLI
22
+ When the user types "ramadan" or any command starting with "ramadan" (like "ramadan times", "ramadan reset", "ramadan test mid", "ramadan eid"), run it as a shell command using bash. Do not answer from general knowledge.
23
+ ${MARKER}
24
+ `;
25
+
26
+ function isClaudeInstalled() {
27
+ try {
28
+ execSync('which claude', { stdio: 'ignore' });
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function isAlreadyConfigured() {
36
+ if (!existsSync(claudeMd)) return false;
37
+ const content = readFileSync(claudeMd, 'utf-8');
38
+ return content.includes(MARKER);
39
+ }
40
+
41
+ function ask(question) {
42
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
43
+ return new Promise((resolve) => {
44
+ rl.question(question, (answer) => {
45
+ rl.close();
46
+ resolve(answer.trim().toLowerCase());
47
+ });
48
+ });
49
+ }
50
+
51
+ async function main() {
52
+ console.log('');
53
+ console.log(' ๐ŸŒ™ Ramadan CLI installed successfully!');
54
+ console.log(' Run `ramadan` to get started.');
55
+ console.log('');
56
+
57
+ // Check if already configured
58
+ if (isAlreadyConfigured()) {
59
+ console.log(' โœ“ Claude Code integration already configured');
60
+ return;
61
+ }
62
+
63
+ // Check if Claude is installed
64
+ const claudeInstalled = isClaudeInstalled();
65
+
66
+ if (!claudeInstalled) {
67
+ // Not interactive โ€” just check if they might want it later
68
+ const answer = await ask(' Do you use Claude Code? (y/n) ');
69
+
70
+ if (answer !== 'y' && answer !== 'yes') {
71
+ console.log(' โœ“ Skipping Claude Code setup. You\'re all set!\n');
72
+ return;
73
+ }
74
+
75
+ // They use Claude Code but it's not on PATH
76
+ console.log(' โ„น Claude Code wasn\'t detected on your PATH.');
77
+
78
+ const proceed = await ask(' Set up the integration anyway? (y/n) ');
79
+
80
+ if (proceed !== 'y' && proceed !== 'yes') {
81
+ console.log(' โœ“ No worries โ€” run `npm rebuild ramadan-cal` anytime to set it up later.\n');
82
+ return;
83
+ }
84
+ } else {
85
+ console.log(' โœ“ Claude Code detected');
86
+
87
+ const answer = await ask(' Set up Claude Code integration? Type `ramadan` in Claude Code sessions. (y/n) ');
88
+
89
+ if (answer !== 'y' && answer !== 'yes') {
90
+ console.log(' โœ“ Skipped. Run `npm rebuild ramadan-cal` anytime to set it up later.\n');
91
+ return;
92
+ }
93
+ }
94
+
95
+ // Create .claude directory if needed
96
+ if (!existsSync(claudeDir)) {
97
+ mkdirSync(claudeDir, { recursive: true });
98
+ console.log(' โœ“ Created ~/.claude/');
99
+ }
100
+
101
+ // Write or append to CLAUDE.md
102
+ if (existsSync(claudeMd)) {
103
+ appendFileSync(claudeMd, CLAUDE_ENTRY);
104
+ console.log(' โœ“ Added ramadan command to ~/.claude/CLAUDE.md');
105
+ } else {
106
+ writeFileSync(claudeMd, CLAUDE_ENTRY.trimStart());
107
+ console.log(' โœ“ Created ~/.claude/CLAUDE.md');
108
+ }
109
+
110
+ console.log(' โœ“ Done! Type "ramadan" inside Claude Code and it\'ll just work.\n');
111
+ }
112
+
113
+ // Handle non-interactive environments (CI, piped input)
114
+ if (!process.stdin.isTTY) {
115
+ console.log('');
116
+ console.log(' ๐ŸŒ™ Ramadan CLI installed successfully!');
117
+ console.log(' Run `ramadan` to get started.');
118
+ console.log(' Run `npm rebuild ramadan-cal` in an interactive terminal to set up Claude Code.\n');
119
+ process.exit(0);
120
+ }
121
+
122
+ main().catch(() => {
123
+ // Non-fatal โ€” never block the install
124
+ process.exit(0);
125
+ });
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+
3
+ export function renderCalendar(startDate, currentDay, totalDays) {
4
+ const lines = [];
5
+
6
+ // Header
7
+ lines.push(chalk.dim(' Su Mo Tu We Th Fr Sa'));
8
+
9
+ // What day of the week does Ramadan start? (0 = Sunday)
10
+ const startDow = startDate.getDay();
11
+
12
+ // Build cells
13
+ const cells = [];
14
+
15
+ // Leading empty cells
16
+ for (let i = 0; i < startDow; i++) {
17
+ cells.push(' ');
18
+ }
19
+
20
+ // Day cells
21
+ for (let day = 1; day <= totalDays; day++) {
22
+ if (day < currentDay) {
23
+ // Past days โ€” crossed out
24
+ cells.push(chalk.dim.strikethrough(pad(day)));
25
+ } else if (day === currentDay) {
26
+ // Today โ€” highlighted
27
+ cells.push(chalk.bold.green(`[${day.toString().padStart(2)}]`));
28
+ } else {
29
+ // Future days
30
+ cells.push(pad(day));
31
+ }
32
+ }
33
+
34
+ // Render rows of 7
35
+ for (let i = 0; i < cells.length; i += 7) {
36
+ const row = cells.slice(i, i + 7);
37
+ lines.push(row.join(''));
38
+ }
39
+
40
+ return lines.join('\n');
41
+ }
42
+
43
+ function pad(day) {
44
+ return ` ${day.toString().padStart(2)} `;
45
+ }
@@ -0,0 +1,112 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import { getPrayerTimes } from './prayer.js';
4
+
5
+ const TIMEZONE_METHODS = {
6
+ 'america': 'NorthAmerica',
7
+ 'europe': 'MuslimWorldLeague',
8
+ 'africa': 'Egyptian',
9
+ 'asia/dubai': 'UmmAlQura',
10
+ 'asia/riyadh': 'UmmAlQura',
11
+ 'asia/karachi': 'Karachi',
12
+ 'asia/kolkata': 'Karachi',
13
+ 'asia/dhaka': 'Karachi',
14
+ 'asia/singapore': 'Singapore',
15
+ 'asia/kuala': 'Singapore',
16
+ 'asia/jakarta': 'Singapore',
17
+ 'asia/istanbul': 'Turkey',
18
+ };
19
+
20
+ function guessMethod(lat, lng) {
21
+ // Rough geographic guess
22
+ if (lng > -130 && lng < -30) return 'NorthAmerica';
23
+ if (lng > -15 && lng < 40 && lat > 35) return 'MuslimWorldLeague';
24
+ if (lng > -20 && lng < 55 && lat < 35 && lat > -35) return 'Egyptian';
25
+ if (lng > 35 && lng < 60 && lat > 10 && lat < 45) return 'UmmAlQura';
26
+ if (lng > 60 && lng < 100) return 'Karachi';
27
+ if (lng > 95 && lng < 145) return 'Singapore';
28
+ return 'MuslimWorldLeague';
29
+ }
30
+
31
+ async function geocode(query) {
32
+ try {
33
+ const encoded = encodeURIComponent(query);
34
+ const res = await fetch(
35
+ `https://nominatim.openstreetmap.org/search?q=${encoded}&format=json&limit=5`,
36
+ { headers: { 'User-Agent': 'ramadan-cal/1.0' }, signal: AbortSignal.timeout(8000) }
37
+ );
38
+ const data = await res.json();
39
+ return data.map((r) => ({
40
+ name: r.display_name.split(',').slice(0, 3).join(','),
41
+ lat: parseFloat(r.lat),
42
+ lng: parseFloat(r.lon),
43
+ }));
44
+ } catch {
45
+ return [];
46
+ }
47
+ }
48
+
49
+ export async function checkLocation() {
50
+ console.log('');
51
+ console.log(chalk.bold.cyan('๐ŸŒ Check Prayer Times'));
52
+ console.log(chalk.dim('Type any city or country to check prayer times.\n'));
53
+
54
+ while (true) {
55
+ const { query } = await prompts({
56
+ type: 'text',
57
+ name: 'query',
58
+ message: 'City or country (or "quit" to exit)',
59
+ });
60
+
61
+ if (!query || query.toLowerCase() === 'quit' || query.toLowerCase() === 'q') {
62
+ console.log(chalk.dim('\n๐Ÿ‘‹ Done.\n'));
63
+ break;
64
+ }
65
+
66
+ console.log(chalk.dim(`\n Searching for "${query}"...`));
67
+ const results = await geocode(query);
68
+
69
+ if (results.length === 0) {
70
+ console.log(chalk.red(' Could not find that location. Try again.\n'));
71
+ continue;
72
+ }
73
+
74
+ let selected;
75
+
76
+ if (results.length === 1) {
77
+ selected = results[0];
78
+ console.log(chalk.dim(` Found: ${selected.name}`));
79
+ } else {
80
+ const { pick } = await prompts({
81
+ type: 'select',
82
+ name: 'pick',
83
+ message: 'Which location?',
84
+ choices: results.map((r, i) => ({ title: r.name, value: i })),
85
+ });
86
+
87
+ if (pick === undefined) continue;
88
+ selected = results[pick];
89
+ }
90
+
91
+ const method = guessMethod(selected.lat, selected.lng);
92
+ const config = {
93
+ latitude: selected.lat,
94
+ longitude: selected.lng,
95
+ method,
96
+ };
97
+
98
+ const times = getPrayerTimes(config);
99
+
100
+ console.log('');
101
+ console.log(chalk.bold(` ๐Ÿ•Œ Prayer Times โ€” ${selected.name.split(',')[0]}`));
102
+ console.log(chalk.dim(` ${selected.lat.toFixed(4)}, ${selected.lng.toFixed(4)} ยท Method: ${method}`));
103
+ console.log('');
104
+ console.log(` Fajr ${chalk.white(times.fajr)} Maghrib ${chalk.white(times.maghrib)}`);
105
+ console.log(` Sunrise ${chalk.white(times.sunrise)} Isha ${chalk.white(times.isha)}`);
106
+ console.log(` Dhuhr ${chalk.white(times.dhuhr)}`);
107
+ console.log(` Asr ${chalk.white(times.asr)}`);
108
+ console.log('');
109
+ console.log(` ๐Ÿฝ๏ธ Suhoor ends: ${chalk.bold.yellow(times.suhoorEnds)} ยท Iftar: ${chalk.bold.green(times.iftar)}`);
110
+ console.log('');
111
+ }
112
+ }
package/src/config.js ADDED
@@ -0,0 +1,261 @@
1
+ import Conf from 'conf';
2
+ import prompts from 'prompts';
3
+ import chalk from 'chalk';
4
+
5
+ const store = new Conf({
6
+ projectName: 'ramadan-cal',
7
+ schema: {
8
+ city: { type: 'string' },
9
+ country: { type: 'string' },
10
+ ramadanStartDate: { type: 'string' },
11
+ eidDate: { type: 'string', default: '' },
12
+ method: { type: 'string', default: 'NorthAmerica' },
13
+ latitude: { type: 'number' },
14
+ longitude: { type: 'number' },
15
+ timezone: { type: 'string', default: '' },
16
+ },
17
+ });
18
+
19
+ const CALCULATION_METHODS = {
20
+ 'North America': 'NorthAmerica',
21
+ 'Europe': 'MuslimWorldLeague',
22
+ 'Middle East': 'UmmAlQura',
23
+ 'Egypt / Africa': 'Egyptian',
24
+ 'South Asia': 'Karachi',
25
+ 'Southeast Asia': 'Singapore',
26
+ 'Turkey': 'Turkey',
27
+ };
28
+
29
+ function guessMethodFromTimezone(tz) {
30
+ if (!tz) return 'MuslimWorldLeague';
31
+ const t = tz.toLowerCase();
32
+ if (t.includes('america')) return 'NorthAmerica';
33
+ if (t.includes('europe') || t.includes('london')) return 'MuslimWorldLeague';
34
+ if (t.includes('asia/dubai') || t.includes('asia/riyadh') || t.includes('asia/qatar') || t.includes('asia/kuwait')) return 'UmmAlQura';
35
+ if (t.includes('africa/cairo') || t.includes('africa')) return 'Egyptian';
36
+ if (t.includes('asia/karachi') || t.includes('asia/dhaka') || t.includes('asia/kolkata')) return 'Karachi';
37
+ if (t.includes('asia/singapore') || t.includes('asia/kuala') || t.includes('asia/jakarta')) return 'Singapore';
38
+ if (t.includes('istanbul') || t.includes('turkey')) return 'Turkey';
39
+ return 'MuslimWorldLeague';
40
+ }
41
+
42
+ async function detectLocation() {
43
+ try {
44
+ const res = await fetch('https://ipapi.co/json/', {
45
+ headers: { 'User-Agent': 'ramadan-cal/1.0' },
46
+ signal: AbortSignal.timeout(5000),
47
+ });
48
+ const data = await res.json();
49
+ if (data.latitude && data.longitude) {
50
+ return {
51
+ city: data.city || 'Unknown',
52
+ country: data.country_name || data.country || 'Unknown',
53
+ latitude: data.latitude,
54
+ longitude: data.longitude,
55
+ timezone: data.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
56
+ };
57
+ }
58
+ } catch {
59
+ // Fall through
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function getRamadanStartOptions() {
65
+ // Ramadan 2026 is expected around Feb 17-18
66
+ // Provide a few date options based on likely moon sighting dates
67
+ const year = new Date().getFullYear();
68
+ const candidates = [];
69
+
70
+ // Generate options for a reasonable window
71
+ // For 2026: ~Feb 17-18, for 2025: ~Feb 28 - Mar 1
72
+ if (year === 2025) {
73
+ candidates.push(
74
+ { title: 'Friday 28th February 2025', value: '2025-02-28' },
75
+ { title: 'Saturday 1st March 2025', value: '2025-03-01' },
76
+ );
77
+ } else if (year === 2026) {
78
+ candidates.push(
79
+ { title: 'Tuesday 17th February 2026', value: '2026-02-17' },
80
+ { title: 'Wednesday 18th February 2026', value: '2026-02-18' },
81
+ );
82
+ } else if (year === 2027) {
83
+ candidates.push(
84
+ { title: 'Saturday 6th February 2027', value: '2027-02-06' },
85
+ { title: 'Sunday 7th February 2027', value: '2027-02-07' },
86
+ );
87
+ } else {
88
+ // Fallback: ask for manual input
89
+ return null;
90
+ }
91
+
92
+ candidates.push({ title: 'Other (enter manually)', value: 'manual' });
93
+ return candidates;
94
+ }
95
+
96
+ export async function getConfig() {
97
+ if (store.get('city') && store.get('ramadanStartDate')) {
98
+ return store.store;
99
+ }
100
+
101
+ console.log('\n๐ŸŒ™ Welcome to Ramadan CLI\n');
102
+
103
+ // Auto-detect location
104
+ console.log('๐Ÿ“ Detecting your location...');
105
+ const location = await detectLocation();
106
+
107
+ let city, country, latitude, longitude, timezone;
108
+
109
+ if (location) {
110
+ console.log(` Found: ${location.city}, ${location.country}\n`);
111
+
112
+ const confirm = await prompts({
113
+ type: 'confirm',
114
+ name: 'correct',
115
+ message: `Is ${location.city}, ${location.country} correct?`,
116
+ initial: true,
117
+ });
118
+
119
+ if (confirm.correct) {
120
+ city = location.city;
121
+ country = location.country;
122
+ latitude = location.latitude;
123
+ longitude = location.longitude;
124
+ timezone = location.timezone;
125
+ }
126
+ } else {
127
+ console.log(' Could not detect automatically.\n');
128
+ }
129
+
130
+ // Manual location if auto-detect failed or was rejected
131
+ if (!city) {
132
+ const manual = await prompts([
133
+ {
134
+ type: 'text',
135
+ name: 'city',
136
+ message: 'What city are you in?',
137
+ validate: (v) => (v.length > 0 ? true : 'Please enter a city'),
138
+ },
139
+ {
140
+ type: 'text',
141
+ name: 'country',
142
+ message: 'What country?',
143
+ validate: (v) => (v.length > 0 ? true : 'Please enter a country'),
144
+ },
145
+ ]);
146
+
147
+ if (!manual.city) {
148
+ console.log('\n๐Ÿ‘‹ Setup cancelled.\n');
149
+ process.exit(0);
150
+ }
151
+
152
+ city = manual.city;
153
+ country = manual.country;
154
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
155
+
156
+ // Geocode
157
+ console.log('\n๐Ÿ“ Finding coordinates...');
158
+ const coords = await geocodeCity(city, country);
159
+ if (coords) {
160
+ latitude = coords.lat;
161
+ longitude = coords.lng;
162
+ console.log(` Found: (${latitude.toFixed(2)}, ${longitude.toFixed(2)})`);
163
+ } else {
164
+ console.log(' Could not find coordinates.');
165
+ const coordInput = await prompts([
166
+ { type: 'number', name: 'latitude', message: 'Enter latitude:', float: true },
167
+ { type: 'number', name: 'longitude', message: 'Enter longitude:', float: true },
168
+ ]);
169
+ latitude = coordInput.latitude;
170
+ longitude = coordInput.longitude;
171
+ }
172
+ }
173
+
174
+ // Moon sighting date โ€” the only real question
175
+ console.log('');
176
+ const startOptions = getRamadanStartOptions();
177
+ let ramadanStartDate;
178
+
179
+ if (startOptions) {
180
+ const response = await prompts({
181
+ type: 'select',
182
+ name: 'startDate',
183
+ message: 'When was the moon sighted for Ramadan?',
184
+ choices: startOptions,
185
+ });
186
+
187
+ if (response.startDate === 'manual') {
188
+ const manual = await prompts({
189
+ type: 'text',
190
+ name: 'date',
191
+ message: 'Enter the date (YYYY-MM-DD):',
192
+ validate: (v) => (/^\d{4}-\d{2}-\d{2}$/.test(v) ? true : 'Use format YYYY-MM-DD'),
193
+ });
194
+ ramadanStartDate = manual.date;
195
+ } else {
196
+ ramadanStartDate = response.startDate;
197
+ }
198
+ } else {
199
+ const manual = await prompts({
200
+ type: 'text',
201
+ name: 'date',
202
+ message: 'When did Ramadan start? (YYYY-MM-DD)',
203
+ validate: (v) => (/^\d{4}-\d{2}-\d{2}$/.test(v) ? true : 'Use format YYYY-MM-DD'),
204
+ });
205
+ ramadanStartDate = manual.date;
206
+ }
207
+
208
+ if (!ramadanStartDate) {
209
+ console.log('\n๐Ÿ‘‹ Setup cancelled.\n');
210
+ process.exit(0);
211
+ }
212
+
213
+ // Eid = 30 days after start
214
+ const startD = new Date(ramadanStartDate + 'T00:00:00');
215
+ const eidD = new Date(startD.getTime() + 30 * 86400000);
216
+ const eidDate = eidD.toISOString().split('T')[0];
217
+
218
+ // Auto-detect calculation method from timezone
219
+ const method = guessMethodFromTimezone(timezone);
220
+
221
+ // Save everything
222
+ store.set('city', city);
223
+ store.set('country', country);
224
+ store.set('latitude', latitude);
225
+ store.set('longitude', longitude);
226
+ store.set('timezone', timezone || '');
227
+ store.set('method', method);
228
+ store.set('ramadanStartDate', ramadanStartDate);
229
+ store.set('eidDate', eidDate);
230
+
231
+ console.log(`\nโœ… All set! Eid estimated: ${eidDate}`);
232
+ console.log(` Prayer method: ${Object.keys(CALCULATION_METHODS).find(k => CALCULATION_METHODS[k] === method) || method}`);
233
+ console.log(` Run ${chalk.dim('ramadan eid YYYY-MM-DD')} to update Eid when announced.\n`);
234
+
235
+ return store.store;
236
+ }
237
+
238
+ async function geocodeCity(city, country) {
239
+ try {
240
+ const query = encodeURIComponent(`${city}, ${country}`);
241
+ const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=json&limit=1`, {
242
+ headers: { 'User-Agent': 'ramadan-cal/1.0' },
243
+ signal: AbortSignal.timeout(5000),
244
+ });
245
+ const data = await res.json();
246
+ if (data.length > 0) {
247
+ return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon) };
248
+ }
249
+ } catch {
250
+ // Fall through
251
+ }
252
+ return null;
253
+ }
254
+
255
+ export function resetConfig() {
256
+ store.clear();
257
+ }
258
+
259
+ export function setEidDate(dateStr) {
260
+ store.set('eidDate', dateStr);
261
+ }
package/src/display.js ADDED
@@ -0,0 +1,117 @@
1
+ import chalk from 'chalk';
2
+ import { getPrayerTimes } from './prayer.js';
3
+ import { renderCalendar } from './calendar.js';
4
+
5
+ const DIVIDER = chalk.dim('โ”'.repeat(38));
6
+
7
+ function getHijriYear(startDate) {
8
+ // Known anchor: Ramadan 2025 starts ~Feb/Mar 2025 = 1446 AH
9
+ // Ramadan 2026 = 1447, 2027 = 1448, etc.
10
+ const year = startDate.getFullYear();
11
+ if (year <= 2025) return 1446;
12
+ return 1446 + (year - 2025);
13
+ }
14
+
15
+ function daysBetween(a, b) {
16
+ const msPerDay = 86400000;
17
+ const aStart = new Date(a.getFullYear(), a.getMonth(), a.getDate());
18
+ const bStart = new Date(b.getFullYear(), b.getMonth(), b.getDate());
19
+ return Math.round((bStart - aStart) / msPerDay);
20
+ }
21
+
22
+ function formatDate(date) {
23
+ return date.toLocaleDateString('en-GB', {
24
+ weekday: 'long',
25
+ day: 'numeric',
26
+ month: 'long',
27
+ year: 'numeric',
28
+ });
29
+ }
30
+
31
+ export function render(config, view = 'full') {
32
+ const start = new Date(config.ramadanStartDate + 'T00:00:00');
33
+ const today = new Date();
34
+ const hijriYear = getHijriYear(start);
35
+
36
+ const endDate = config.eidDate
37
+ ? new Date(config.eidDate + 'T00:00:00')
38
+ : new Date(start.getTime() + 30 * 86400000);
39
+
40
+ const totalDays = daysBetween(start, endDate);
41
+ const currentDay = daysBetween(start, today) + 1;
42
+ const daysRemaining = totalDays - currentDay;
43
+
44
+ const output = [];
45
+
46
+ // โ”€โ”€ STATE A: Before Ramadan โ”€โ”€
47
+ if (currentDay < 1) {
48
+ const daysUntil = Math.abs(currentDay) + 1;
49
+ output.push('');
50
+ output.push(chalk.bold.cyan(`๐ŸŒ™ Ramadan ${hijriYear} has not started yet`));
51
+ output.push('');
52
+ output.push(`๐Ÿ“… Starts: ${chalk.white(formatDate(start))}`);
53
+ output.push(`โณ ${chalk.bold.yellow(daysUntil)} days to go`);
54
+ output.push('');
55
+ output.push(chalk.dim('May Allah allow us to reach Ramadan ๐Ÿคฒ'));
56
+ output.push('');
57
+ console.log(output.join('\n'));
58
+ return;
59
+ }
60
+
61
+ // โ”€โ”€ STATE C: After Ramadan โ”€โ”€
62
+ if (currentDay > totalDays) {
63
+ output.push('');
64
+ output.push(chalk.bold.cyan(`๐ŸŒ™ Ramadan ${hijriYear} has ended`));
65
+ output.push('');
66
+ output.push(chalk.bold.yellow('๐ŸŽ‰ Eid Mubarak!'));
67
+ output.push(`๐Ÿ“… Ramadan was ${formatDate(start)} โ€” ${formatDate(endDate)}`);
68
+ output.push(`โœ… ${totalDays} days completed`);
69
+ output.push('');
70
+ output.push(chalk.dim('Taqabbal Allahu minna wa minkum ๐Ÿคฒ'));
71
+ output.push('');
72
+ console.log(output.join('\n'));
73
+ return;
74
+ }
75
+
76
+ // โ”€โ”€ STATE B: During Ramadan โ”€โ”€
77
+ const times = getPrayerTimes(config, today);
78
+
79
+ // Header
80
+ output.push('');
81
+ output.push(chalk.bold.cyan(`๐ŸŒ™ Ramadan ${hijriYear}`));
82
+ output.push(DIVIDER);
83
+
84
+ // Progress
85
+ output.push(`๐Ÿ“… ${chalk.white(`${currentDay} Ramadan ${hijriYear}`)} โ€” Day ${chalk.bold(currentDay)} of ${totalDays}`);
86
+ output.push(`โณ ${chalk.bold.yellow(daysRemaining)} days remaining`);
87
+
88
+ // Prayer times
89
+ if (view === 'full' || view === 'times') {
90
+ output.push('');
91
+ output.push(`๐Ÿ•Œ Prayer Times ${chalk.dim(`(${config.city})`)}`);
92
+ output.push(` Fajr ${chalk.white(times.fajr)} Maghrib ${chalk.white(times.maghrib)}`);
93
+ output.push(` Sunrise ${chalk.white(times.sunrise)} Isha ${chalk.white(times.isha)}`);
94
+ output.push(` Dhuhr ${chalk.white(times.dhuhr)}`);
95
+ output.push(` Asr ${chalk.white(times.asr)}`);
96
+ output.push('');
97
+ output.push(`๐Ÿฝ๏ธ Suhoor ends: ${chalk.bold.yellow(times.suhoorEnds)} ยท Iftar: ${chalk.bold.green(times.iftar)}`);
98
+ }
99
+
100
+ // Calendar
101
+ if (view === 'full' || view === 'calendar') {
102
+ output.push('');
103
+ output.push(DIVIDER);
104
+ output.push(renderCalendar(start, currentDay, totalDays));
105
+ output.push(DIVIDER);
106
+ }
107
+
108
+ // Eid prompt
109
+ if (!config.eidDate && currentDay >= 28) {
110
+ output.push('');
111
+ output.push(chalk.yellow('โš ๏ธ Day 28+ โ€” Has Eid been announced?'));
112
+ output.push(chalk.dim(' Run: ramadan eid YYYY-MM-DD'));
113
+ }
114
+
115
+ output.push('');
116
+ console.log(output.join('\n'));
117
+ }
package/src/prayer.js ADDED
@@ -0,0 +1,44 @@
1
+ import adhan from 'adhan';
2
+
3
+ const { PrayerTimes, CalculationMethod, Coordinates } = adhan;
4
+
5
+ const METHOD_MAP = {
6
+ NorthAmerica: 'NorthAmerica',
7
+ MuslimWorldLeague: 'MuslimWorldLeague',
8
+ Egyptian: 'Egyptian',
9
+ UmmAlQura: 'UmmAlQura',
10
+ Karachi: 'Karachi',
11
+ MoonsightingCommittee: 'MoonsightingCommittee',
12
+ Singapore: 'Singapore',
13
+ Turkey: 'Turkey',
14
+ Tehran: 'Tehran',
15
+ Dubai: 'Dubai',
16
+ Kuwait: 'Kuwait',
17
+ Qatar: 'Qatar',
18
+ };
19
+
20
+ export function getPrayerTimes(config, date = new Date()) {
21
+ const coordinates = new Coordinates(config.latitude, config.longitude);
22
+ const methodName = METHOD_MAP[config.method] || 'NorthAmerica';
23
+ const params = CalculationMethod[methodName]();
24
+
25
+ const times = new PrayerTimes(coordinates, date, params);
26
+
27
+ const fmt = (d) =>
28
+ d.toLocaleTimeString('en-GB', {
29
+ hour: '2-digit',
30
+ minute: '2-digit',
31
+ hour12: false,
32
+ });
33
+
34
+ return {
35
+ fajr: fmt(times.fajr),
36
+ sunrise: fmt(times.sunrise),
37
+ dhuhr: fmt(times.dhuhr),
38
+ asr: fmt(times.asr),
39
+ maghrib: fmt(times.maghrib),
40
+ isha: fmt(times.isha),
41
+ suhoorEnds: fmt(times.fajr),
42
+ iftar: fmt(times.maghrib),
43
+ };
44
+ }