@mounaji_npm/booking-calendar 0.1.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.
Files changed (2) hide show
  1. package/package.json +35 -0
  2. package/src/index.jsx +159 -0
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@mounaji_npm/booking-calendar",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight month-grid calendar component for rental bookings management",
5
+ "keywords": ["calendar", "booking", "rentals", "react", "saas", "mounaji"],
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "registry": "https://registry.npmjs.org/"
11
+ },
12
+ "main": "./dist/mounajibookingcalendar.umd.cjs",
13
+ "module": "./dist/mounajibookingcalendar.es.js",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/mounajibookingcalendar.es.js",
17
+ "require": "./dist/mounajibookingcalendar.umd.cjs"
18
+ }
19
+ },
20
+ "files": ["dist", "src", "DOCS.md"],
21
+ "scripts": {
22
+ "build": "vite build",
23
+ "dev": "vite build --watch"
24
+ },
25
+ "peerDependencies": {
26
+ "react": ">=17.0.0",
27
+ "react-dom": ">=17.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@vitejs/plugin-react": "^4.3.4",
31
+ "react": "^19.0.0",
32
+ "react-dom": "^19.0.0",
33
+ "vite": "^6.0.0"
34
+ }
35
+ }
package/src/index.jsx ADDED
@@ -0,0 +1,159 @@
1
+ import React, { useMemo, useState, useCallback } from 'react';
2
+
3
+ /**
4
+ * BookingCalendar
5
+ * A lightweight month-grid calendar for rental bookings.
6
+ *
7
+ * Props:
8
+ * - bookings: Array<{ id, unit_id?, check_in, check_out, status, guest_name? }>
9
+ * - units?: Array<{ id, name }> (optional; when provided shows a unit column header)
10
+ * - month?: Date (defaults to today)
11
+ * - onMonthChange? (next: Date) => void
12
+ * - onSelectDay? (date: Date) => void
13
+ * - onSelectBooking?(booking) => void
14
+ * - statuses?: color map for status -> css color
15
+ */
16
+
17
+ const WEEK = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
18
+
19
+ const STATUS_COLORS = {
20
+ confirmed: '#16a34a',
21
+ pending: '#f59e0b',
22
+ blocked: '#6b7280',
23
+ cancelled: '#ef4444'
24
+ };
25
+
26
+ const fmt = (d) => {
27
+ const y = d.getFullYear();
28
+ const m = String(d.getMonth() + 1).padStart(2, '0');
29
+ const day = String(d.getDate()).padStart(2, '0');
30
+ return `${y}-${m}-${day}`;
31
+ };
32
+
33
+ const parse = (s) => (s ? new Date(s + 'T00:00:00') : null);
34
+
35
+ function dayKey(d) { return fmt(d); }
36
+
37
+ /** Returns true if booking range overlaps a given date. */
38
+ function bookingOnDate(booking, date) {
39
+ const start = parse(typeof booking.check_in === 'string' ? booking.check_in.slice(0, 10) : booking.check_in);
40
+ const end = parse(typeof booking.check_out === 'string' ? booking.check_out.slice(0, 10) : booking.check_out);
41
+ if (!start || !end) return false;
42
+ const cur = fmt(date);
43
+ return cur >= fmt(start) && cur <= fmt(end);
44
+ }
45
+
46
+ export function BookingCalendar({
47
+ bookings = [],
48
+ units,
49
+ month: monthProp,
50
+ onMonthChange,
51
+ onSelectDay,
52
+ onSelectBooking,
53
+ statuses = STATUS_COLORS
54
+ }) {
55
+ const [month, setMonth] = useState(monthProp ?? new Date());
56
+
57
+ const cells = useMemo(() => {
58
+ const first = new Date(month.getFullYear(), month.getMonth(), 1);
59
+ const startDay = first.getDay();
60
+ const daysInMonth = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
61
+ const arr = [];
62
+ for (let i = 0; i < startDay; i++) arr.push(null);
63
+ for (let d = 1; d <= daysInMonth; d++) arr.push(new Date(month.getFullYear(), month.getMonth(), d));
64
+ while (arr.length % 7 !== 0) arr.push(null);
65
+ return arr;
66
+ }, [month]);
67
+
68
+ const bookingsByDay = useMemo(() => {
69
+ const map = new Map();
70
+ for (const date of cells.filter(Boolean)) {
71
+ const k = dayKey(date);
72
+ map.set(k, bookings.filter(b => bookingOnDate(b, date)));
73
+ }
74
+ return map;
75
+ }, [cells, bookings]);
76
+
77
+ const prevMonth = useCallback(() => {
78
+ const m = new Date(month);
79
+ m.setMonth(m.getMonth() - 1);
80
+ setMonth(m);
81
+ onMonthChange?.(m);
82
+ }, [month, onMonthChange]);
83
+
84
+ const nextMonth = useCallback(() => {
85
+ const m = new Date(month);
86
+ m.setMonth(m.getMonth() + 1);
87
+ setMonth(m);
88
+ onMonthChange?.(m);
89
+ }, [month, onMonthChange]);
90
+
91
+ const label = useMemo(
92
+ () => month.toLocaleDateString('es-AR', { month: 'long', year: 'numeric' }),
93
+ [month]
94
+ );
95
+
96
+ return (
97
+ <div className="mn-booking-calendar" style={{ fontFamily: 'system-ui, sans-serif' }}>
98
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
99
+ <button onClick={prevMonth} aria-label="Mes anterior">‹</button>
100
+ <strong style={{ textTransform: 'capitalize' }}>{label}</strong>
101
+ <button onClick={nextMonth} aria-label="Mes siguiente">›</button>
102
+ </div>
103
+
104
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4 }}>
105
+ {WEEK.map(w => (
106
+ <div key={w} style={{ textAlign: 'center', fontWeight: 600, padding: '4px 0', fontSize: 12, color: '#6b7280' }}>{w}</div>
107
+ ))}
108
+
109
+ {cells.map((date, i) => {
110
+ if (!date) return <div key={i} style={{ minHeight: 72, background: '#f9fafb', borderRadius: 6 }} />;
111
+ const k = dayKey(date);
112
+ const dayBookings = bookingsByDay.get(k) ?? [];
113
+ const isToday = fmt(date) === fmt(new Date());
114
+ return (
115
+ <div
116
+ key={i}
117
+ onClick={() => onSelectDay?.(date)}
118
+ style={{
119
+ minHeight: 72,
120
+ border: '1px solid #e5e7eb',
121
+ borderRadius: 6,
122
+ padding: 4,
123
+ cursor: 'pointer',
124
+ background: isToday ? '#eff6ff' : '#fff'
125
+ }}
126
+ >
127
+ <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>{date.getDate()}</div>
128
+ {dayBookings.slice(0, 2).map(b => (
129
+ <div
130
+ key={b.id}
131
+ onClick={(e) => { e.stopPropagation(); onSelectBooking?.(b); }}
132
+ title={b.guest_name || b.status}
133
+ style={{
134
+ fontSize: 10,
135
+ color: '#fff',
136
+ background: statuses[b.status] || '#3b82f6',
137
+ borderRadius: 4,
138
+ padding: '1px 4px',
139
+ marginBottom: 2,
140
+ overflow: 'hidden',
141
+ textOverflow: 'ellipsis',
142
+ whiteSpace: 'nowrap'
143
+ }}
144
+ >
145
+ {b.guest_name || b.status}
146
+ </div>
147
+ ))}
148
+ {dayBookings.length > 2 && (
149
+ <div style={{ fontSize: 10, color: '#6b7280' }}>+{dayBookings.length - 2} más</div>
150
+ )}
151
+ </div>
152
+ );
153
+ })}
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ export default BookingCalendar;