@asafarim/booking-calendar 0.2.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ # License
2
+
3
+ This project is licensed under the **Creative Commons Attribution 4.0 International (CC BY 4.0)** license.
4
+
5
+ You are free to:
6
+
7
+ - **Share** — copy and redistribute the material in any medium or format
8
+ - **Adapt** — remix, transform, and build upon the material for any purpose, even commercially
9
+
10
+ Under the following terms:
11
+
12
+ - **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
13
+ You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
14
+
15
+ ## 📌 Attribution Requirement
16
+
17
+ If you use or modify this project, please include the following attribution:
18
+
19
+ > Based on work by [Ali Safari](https://github.com/AliSafari-IT/asafarim-dot-be)
20
+
21
+ ## 🔗 Full License Text
22
+
23
+ For full details, see the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/).
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @asafarim/booking-calendar
2
+
3
+ Google Calendar-style booking UI for FreelanceToolkit.Api
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @asafarim/booking-calendar
9
+ # or
10
+ pnpm add @asafarim/booking-calendar
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```tsx
16
+ import { BookingCalendar } from '@asafarim/booking-calendar';
17
+ import '@asafarim/booking-calendar/styles';
18
+
19
+ function App() {
20
+ const [bookings, setBookings] = useState([]);
21
+
22
+ const handleCreate = async (dto) => {
23
+ const response = await fetch('/api/calendar/bookings', {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify(dto),
27
+ });
28
+ const newBooking = await response.json();
29
+ setBookings([...bookings, newBooking]);
30
+ };
31
+
32
+ return (
33
+ <BookingCalendar
34
+ bookings={bookings}
35
+ onCreateBooking={handleCreate}
36
+ initialView="week"
37
+ />
38
+ );
39
+ }
40
+ ```
41
+
42
+ ## Features
43
+
44
+ - ✅ Month/Week/Day views
45
+ - ✅ Click empty slot to create booking
46
+ - ✅ Click booking to edit
47
+ - ✅ Drag to move bookings
48
+ - ✅ Drag resize bookings
49
+ - ✅ Status badges (Pending, Confirmed, etc.)
50
+ - ✅ Delivery status indicators
51
+ - ✅ Availability checking
52
+ - ✅ Client details display
53
+ - ✅ Responsive design
54
+
55
+ ## API Integration
56
+
57
+ The component expects these FreelanceToolkit.Api endpoints:
58
+
59
+ ```
60
+ GET /api/calendar/bookings
61
+ POST /api/calendar/bookings
62
+ PUT /api/calendar/bookings/{id}
63
+ DELETE /api/calendar/bookings/{id}
64
+ POST /api/calendar/bookings/check-availability
65
+ ```
66
+
67
+ ## Props
68
+
69
+ | Prop | Type | Required | Description |
70
+ |------|------|----------|-------------|
71
+ | `bookings` | `BookingEvent[]` | Yes | Array of bookings |
72
+ | `onCreateBooking` | `(dto) => Promise<void>` | No | Create handler |
73
+ | `onUpdateBooking` | `(id, dto) => Promise<void>` | No | Update handler |
74
+ | `onDeleteBooking` | `(id) => Promise<void>` | No | Delete handler |
75
+ | `onCheckAvailability` | `(dto) => Promise<AvailabilityResponse>` | No | Availability check |
76
+ | `initialView` | `'month' \| 'week' \| 'day'` | No | Default: 'week' |
77
+ | `initialDate` | `Date` | No | Default: today |
78
+
79
+ ## Booking Type
80
+
81
+ ```typescript
82
+ interface BookingEvent {
83
+ id: string;
84
+ title: string;
85
+ description?: string;
86
+ startTime: Date;
87
+ endTime: Date;
88
+ durationMinutes: number;
89
+ status: "Pending" | "Confirmed" | "Cancelled" | "Completed" | "NoShow";
90
+ meetingLink?: string;
91
+ location?: string;
92
+ clientName: string;
93
+ clientEmail: string;
94
+ clientPhone?: string;
95
+ deliveryStatus?: "Pending" | "Sent" | "Failed" | "Retrying";
96
+ retryCount: number;
97
+ createdAt: Date;
98
+ updatedAt: Date;
99
+ }
100
+ ```
101
+
102
+ ## Styling
103
+
104
+ Override CSS variables:
105
+
106
+ ```css
107
+ .booking-calendar {
108
+ --calendar-bg: #ffffff;
109
+ --calendar-border: #e5e7eb;
110
+ --event-pending: #fbbf24;
111
+ --event-confirmed: #10b981;
112
+ --event-cancelled: #ef4444;
113
+ --event-completed: #3b82f6;
114
+ }
115
+ ```
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ # Install dependencies
121
+ pnpm install
122
+
123
+ # Run dev server
124
+ pnpm dev
125
+
126
+ # Build package
127
+ pnpm build
128
+
129
+ # Publish
130
+ npm publish --access public
131
+ ```
132
+
133
+ ## License
134
+
135
+ MIT © ASafariM
@@ -0,0 +1,3 @@
1
+ import { BookingCalendarProps } from './types';
2
+
3
+ export default function BookingCalendar({ bookings, onCreateBooking, onUpdateBooking, onDeleteBooking, onSendConfirmation, initialView, initialDate, className, }: BookingCalendarProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { BookingEvent } from './types';
2
+
3
+ interface BookingModalProps {
4
+ booking: BookingEvent | null;
5
+ initialSlot: {
6
+ start: Date;
7
+ end: Date;
8
+ } | null;
9
+ onClose: () => void;
10
+ onCreate?: (dto: any) => Promise<void>;
11
+ onUpdate?: (id: string, dto: any) => Promise<void>;
12
+ onDelete?: (id: string) => Promise<void>;
13
+ onSendConfirmation?: (id: string) => Promise<void>;
14
+ }
15
+ export default function BookingModal({ booking, initialSlot, onClose, onCreate, onUpdate, onDelete, onSendConfirmation, }: BookingModalProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -0,0 +1,8 @@
1
+ import { BookingStatus } from './types';
2
+
3
+ interface BookingStatusBadgeProps {
4
+ status: BookingStatus;
5
+ size?: 'xs' | 'sm' | 'md';
6
+ }
7
+ export default function BookingStatusBadge({ status, size }: BookingStatusBadgeProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,10 @@
1
+ import { BookingEvent } from './types';
2
+
3
+ interface DayViewProps {
4
+ currentDate: Date;
5
+ bookings: BookingEvent[];
6
+ onSlotClick: (start: Date, end: Date) => void;
7
+ onBookingClick: (booking: BookingEvent) => void;
8
+ }
9
+ export default function DayView({ currentDate, bookings, onSlotClick, onBookingClick }: DayViewProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,8 @@
1
+ import { DeliveryStatus } from './types';
2
+
3
+ interface DeliveryStatusBadgeProps {
4
+ status: DeliveryStatus;
5
+ size?: "xs" | "sm" | "md";
6
+ }
7
+ export default function DeliveryStatusBadge({ status, size, }: DeliveryStatusBadgeProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,10 @@
1
+ import { BookingEvent } from './types';
2
+
3
+ interface MonthViewProps {
4
+ currentDate: Date;
5
+ bookings: BookingEvent[];
6
+ onSlotClick: (start: Date, end: Date) => void;
7
+ onBookingClick: (booking: BookingEvent) => void;
8
+ }
9
+ export default function MonthView({ currentDate, bookings, onSlotClick, onBookingClick }: MonthViewProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,10 @@
1
+ import { BookingEvent } from './types';
2
+
3
+ interface WeekViewProps {
4
+ currentDate: Date;
5
+ bookings: BookingEvent[];
6
+ onSlotClick: (start: Date, end: Date) => void;
7
+ onBookingClick: (booking: BookingEvent) => void;
8
+ }
9
+ export default function WeekView({ currentDate, bookings, onSlotClick, onBookingClick, }: WeekViewProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare function useCalendarGrid(): {
2
+ getWeekDays: (date: Date) => Date[];
3
+ getMonthDays: (date: Date) => Date[];
4
+ getTimeSlots: (intervalMinutes?: number) => {
5
+ hour: number;
6
+ minute: number;
7
+ }[];
8
+ };
@@ -0,0 +1,19 @@
1
+ interface DragState {
2
+ isDragging: boolean;
3
+ draggedEventId: string | null;
4
+ startX: number;
5
+ startY: number;
6
+ currentX: number;
7
+ currentY: number;
8
+ }
9
+ export declare function useDragInteractions(): {
10
+ dragState: DragState;
11
+ handleDragStart: (eventId: string, clientX: number, clientY: number) => void;
12
+ handleDragMove: (clientX: number, clientY: number) => void;
13
+ handleDragEnd: () => void;
14
+ getDragOffset: () => {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ };
19
+ export {};
@@ -0,0 +1,11 @@
1
+ import { BookingEvent } from '../types';
2
+
3
+ export declare function useEventLayout(): {
4
+ getEventPosition: (event: BookingEvent, day: Date) => {
5
+ top: number;
6
+ height: number;
7
+ left: number;
8
+ width: number;
9
+ };
10
+ detectOverlaps: (events: BookingEvent[], day: Date) => BookingEvent[][];
11
+ };
package/dist/index.css ADDED
@@ -0,0 +1 @@
1
+ :root{--bc-bg: #0b0b10;--bc-surface: #111827;--bc-grid-line: #1f2933;--bc-text: #e5e7eb;--bc-muted: #9ca3af;--bc-accent: #3b82f6;--bc-danger: #ef4444;--bc-success: #22c55e;--bc-warning: #eab308;--bc-border-radius-sm: 4px;--bc-border-radius-md: 8px}.booking-calendar{background:var(--bc-bg);color:var(--bc-text);height:100vh;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.booking-calendar-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;background:var(--bc-surface);border-bottom:1px solid var(--bc-grid-line)}.booking-calendar-nav{display:flex;gap:.5rem;align-items:center}.nav-btn{background:transparent;border:1px solid var(--bc-grid-line);color:var(--bc-text);padding:.5rem 1rem;border-radius:var(--bc-border-radius-sm);cursor:pointer;font-size:14px}.nav-btn:hover{background:var(--bc-grid-line)}.current-date{margin:0 1rem;font-size:1.25rem;font-weight:600}.booking-calendar-view-switcher{display:flex;gap:.25rem}.booking-calendar-view-switcher button{background:transparent;border:1px solid var(--bc-grid-line);color:var(--bc-muted);padding:.5rem 1rem;cursor:pointer;font-size:14px}.booking-calendar-view-switcher button:first-child{border-radius:var(--bc-border-radius-sm) 0 0 var(--bc-border-radius-sm)}.booking-calendar-view-switcher button:last-child{border-radius:0 var(--bc-border-radius-sm) var(--bc-border-radius-sm) 0}.booking-calendar-view-switcher button.active{background:var(--bc-accent);color:#fff;border-color:var(--bc-accent)}.booking-calendar-body{flex:1;overflow:auto;position:relative}.bc-month-view{height:100%;display:flex;flex-direction:column}.bc-month-header{display:grid;grid-template-columns:repeat(7,1fr);background:var(--bc-surface);border-bottom:1px solid var(--bc-grid-line)}.bc-month-header-cell{padding:.75rem;text-align:center;font-weight:600;font-size:14px;color:var(--bc-muted)}.bc-month-grid{flex:1;display:flex;flex-direction:column}.bc-month-week{display:grid;grid-template-columns:repeat(7,1fr);flex:1;border-bottom:1px solid var(--bc-grid-line)}.bc-month-day{border-right:1px solid var(--bc-grid-line);padding:.5rem;cursor:pointer;overflow:hidden;position:relative}.bc-month-day:hover{background:#3b82f61a}.bc-month-day.bc-other-month{opacity:.4}.bc-month-day.bc-today{background:#3b82f626}.bc-month-day-number{font-weight:600;margin-bottom:.25rem;font-size:14px}.bc-month-day-events{display:flex;flex-direction:column;gap:2px}.bc-month-event{background:var(--bc-surface);padding:2px 4px;border-radius:var(--bc-border-radius-sm);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:4px}.bc-month-event:hover{background:var(--bc-grid-line)}.bc-month-event-title{overflow:hidden;text-overflow:ellipsis}.bc-month-event-more{color:var(--bc-muted);font-size:11px;padding:2px 4px}.bc-week-view{height:100%;display:flex;flex-direction:column}.bc-week-header{display:grid;grid-template-columns:60px repeat(7,1fr);background:var(--bc-surface);border-bottom:2px solid var(--bc-grid-line)}.bc-week-time-gutter{border-right:1px solid var(--bc-grid-line)}.bc-week-day-header{text-align:center;padding:.75rem;border-right:1px solid var(--bc-grid-line)}.bc-week-day-header.bc-today{background:#3b82f626}.bc-week-day-name{font-size:12px;color:var(--bc-muted);text-transform:uppercase}.bc-week-day-date{font-size:18px;font-weight:600;margin-top:4px}.bc-week-body{flex:1;display:grid;grid-template-columns:60px 1fr;overflow:auto}.bc-week-time-column{border-right:1px solid var(--bc-grid-line)}.bc-week-time-slot{height:60px;padding:4px 8px;font-size:11px;color:var(--bc-muted);border-bottom:1px solid var(--bc-grid-line);text-align:right}.bc-week-grid-container{display:grid;grid-template-columns:repeat(7,1fr);position:relative}.bc-week-day-column{border-right:1px solid var(--bc-grid-line);position:relative}.bc-week-day-column.bc-today{background:#3b82f60d}.bc-week-grid-slot{height:60px;border-bottom:1px solid var(--bc-grid-line);cursor:pointer}.bc-week-grid-slot:hover{background:#3b82f61a}.bc-week-event{position:absolute;background:var(--bc-accent);border-radius:var(--bc-border-radius-sm);padding:4px 8px;cursor:pointer;overflow:hidden;box-shadow:0 2px 4px #0003;z-index:10}.bc-week-event:hover{box-shadow:0 4px 8px #0000004d}.bc-week-event-title{font-weight:600;font-size:12px;margin-bottom:2px}.bc-week-event-time{font-size:10px;opacity:.9}.bc-week-now-line{position:absolute;left:0;right:0;height:2px;background:var(--bc-danger);z-index:20;pointer-events:none}.bc-week-now-dot{position:absolute;left:-4px;top:-3px;width:8px;height:8px;background:var(--bc-danger);border-radius:50%}.bc-day-view{height:100%;display:flex;flex-direction:column}.bc-day-header{padding:1rem 1.5rem;background:var(--bc-surface);border-bottom:1px solid var(--bc-grid-line)}.bc-day-title{font-size:1.25rem;font-weight:600}.bc-day-body{flex:1;display:grid;grid-template-columns:80px 1fr;overflow:auto}.bc-day-time-column{border-right:1px solid var(--bc-grid-line)}.bc-day-time-slot{height:60px;padding:4px 12px;font-size:12px;color:var(--bc-muted);border-bottom:1px solid var(--bc-grid-line);text-align:right}.bc-day-grid-container,.bc-day-column{position:relative}.bc-day-grid-slot{height:60px;border-bottom:1px solid var(--bc-grid-line);cursor:pointer}.bc-day-grid-slot:hover{background:#3b82f61a}.bc-day-event{position:absolute;background:var(--bc-accent);border-radius:var(--bc-border-radius-md);padding:8px 12px;cursor:pointer;box-shadow:0 2px 6px #0003;z-index:10;left:10px;right:10px}.bc-day-event:hover{box-shadow:0 4px 12px #0000004d}.bc-day-event-title{font-weight:600;font-size:14px;margin-bottom:4px}.bc-day-event-client{font-size:12px;opacity:.9;margin-bottom:4px}.bc-day-event-time{font-size:11px;opacity:.8}.bc-day-now-line{position:absolute;left:0;right:0;height:2px;background:var(--bc-danger);z-index:20;pointer-events:none}.bc-day-now-dot{position:absolute;left:-4px;top:-3px;width:8px;height:8px;background:var(--bc-danger);border-radius:50%}.bc-modal-overlay{position:fixed;inset:0;background:#000000b3;display:flex;align-items:center;justify-content:center;z-index:1000;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.bc-modal{background:var(--bc-surface);border-radius:var(--bc-border-radius-md);width:90%;max-width:600px;max-height:90vh;overflow:auto;box-shadow:0 20px 25px -5px #00000080}.bc-modal-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;border-bottom:1px solid var(--bc-grid-line)}.bc-modal-header h2{margin:0;font-size:1.5rem;font-weight:600}.bc-modal-close{background:transparent;border:none;color:var(--bc-muted);font-size:2rem;cursor:pointer;padding:0;width:32px;height:32px;display:flex;align-items:center;justify-content:center}.bc-modal-close:hover{color:var(--bc-text)}.bc-modal-body{padding:1.5rem}.bc-form-group{margin-bottom:1rem}.bc-form-group label{display:block;margin-bottom:.5rem;font-size:14px;font-weight:500;color:var(--bc-text)}.bc-form-group input,.bc-form-group textarea,.bc-form-group select{width:100%;padding:.625rem;background:var(--bc-bg);border:1px solid var(--bc-grid-line);border-radius:var(--bc-border-radius-sm);color:var(--bc-text);font-size:14px}.bc-form-group input:focus,.bc-form-group textarea:focus,.bc-form-group select:focus{outline:none;border-color:var(--bc-accent)}.bc-form-group input.bc-input-error{border-color:var(--bc-danger)}.bc-error-text{display:block;margin-top:.25rem;color:var(--bc-danger);font-size:12px}.bc-form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.bc-modal-footer{display:flex;justify-content:space-between;padding:1.5rem;border-top:1px solid var(--bc-grid-line)}.bc-modal-actions-left,.bc-modal-actions-right{display:flex;gap:.5rem}.bc-btn{padding:.625rem 1.25rem;border-radius:var(--bc-border-radius-sm);font-size:14px;font-weight:500;cursor:pointer;border:none;transition:all .2s}.bc-btn:disabled{opacity:.5;cursor:not-allowed}.bc-btn-primary{background:var(--bc-accent);color:#fff}.bc-btn-primary:hover:not(:disabled){background:#2563eb}.bc-btn-secondary{background:transparent;color:var(--bc-text);border:1px solid var(--bc-grid-line)}.bc-btn-secondary:hover:not(:disabled){background:var(--bc-grid-line)}.bc-btn-danger{background:var(--bc-danger);color:#fff}.bc-btn-danger:hover:not(:disabled){background:#dc2626}.bc-status-badge,.bc-delivery-badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.bc-badge-xs{padding:1px 4px;font-size:9px}.bc-badge-sm{padding:2px 6px;font-size:10px}.bc-badge-md{padding:3px 8px;font-size:11px}.bc-status-pending{background:#eab30833;color:var(--bc-warning)}.bc-status-confirmed{background:#22c55e33;color:var(--bc-success)}.bc-status-cancelled{background:#ef444433;color:var(--bc-danger)}.bc-status-completed{background:#3b82f633;color:var(--bc-accent)}.bc-status-noshow,.bc-delivery-pending{background:#9ca3af33;color:var(--bc-muted)}.bc-delivery-sent{background:#22c55e33;color:var(--bc-success)}.bc-delivery-failed{background:#ef444433;color:var(--bc-danger)}.bc-delivery-retrying{background:#eab30833;color:var(--bc-warning)}
@@ -0,0 +1,11 @@
1
+ export { default as BookingCalendar } from './BookingCalendar';
2
+ export { default as BookingModal } from './BookingModal';
3
+ export { default as MonthView } from './MonthView';
4
+ export { default as WeekView } from './WeekView';
5
+ export { default as DayView } from './DayView';
6
+ export { default as BookingStatusBadge } from './BookingStatusBadge';
7
+ export { default as DeliveryStatusBadge } from './DeliveryStatusBadge';
8
+ export { useCalendarGrid } from './hooks/useCalendarGrid';
9
+ export { useEventLayout } from './hooks/useEventLayout';
10
+ export { useDragInteractions } from './hooks/useDragInteractions';
11
+ export * from './types';