@propbinder/mobile-design 0.2.47 → 0.2.50
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/ng-package.json +24 -0
- package/package.json +3 -39
- package/src/animations/page-transitions.ts +165 -0
- package/src/assets/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
- package/src/components/action-list-item/ds-mobile-action-list-item.ts +102 -0
- package/src/components/action-list-item/index.ts +2 -0
- package/src/components/app-icon/ds-app-icon.ts +133 -0
- package/src/components/app-icon/index.ts +2 -0
- package/src/components/attachment-preview/ds-mobile-attachment-preview.css +139 -0
- package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +164 -0
- package/src/components/attachment-preview/index.ts +1 -0
- package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +142 -0
- package/src/components/avatar-with-badge/index.ts +2 -0
- package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +71 -0
- package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +121 -0
- package/src/components/booking-modal/ds-mobile-booking-modal.ts +598 -0
- package/src/components/booking-modal/ds-mobile-booking-summary.ts +161 -0
- package/src/components/booking-modal/index.ts +4 -0
- package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +266 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +146 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +156 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +101 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +169 -0
- package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +211 -0
- package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +578 -0
- package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +614 -0
- package/src/components/bottom-sheet/index.ts +8 -0
- package/src/components/bottom-sheet/modal-shadow-fix.ts +42 -0
- package/src/components/card-inline/ds-mobile-card-inline.ts +301 -0
- package/src/components/card-inline/index.ts +2 -0
- package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +118 -0
- package/src/components/card-inline-banner/index.ts +1 -0
- package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +120 -0
- package/src/components/card-inline-contact/index.ts +1 -0
- package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +141 -0
- package/src/components/card-inline-file/index.ts +1 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.css +159 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +105 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.ts +918 -0
- package/src/components/chat-modal/index.ts +8 -0
- package/src/components/comment/ds-mobile-comment.ts +568 -0
- package/src/components/comment/index.ts +2 -0
- package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
- package/src/components/contact-list-item/index.ts +2 -0
- package/src/components/content/ds-mobile-content.ts +139 -0
- package/src/components/content/index.ts +2 -0
- package/src/components/dropdown/ds-mobile-dropdown.css +199 -0
- package/src/components/dropdown/ds-mobile-dropdown.ts +340 -0
- package/src/components/dropdown/index.ts +2 -0
- package/src/components/ds-mobile-tabs.css +407 -0
- package/src/components/ds-mobile-tabs.ts +216 -0
- package/src/components/empty-state/ds-mobile-empty-state.ts +120 -0
- package/src/components/empty-state/index.ts +2 -0
- package/src/components/fab/ds-mobile-fab.ts +315 -0
- package/src/components/fab/index.ts +1 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +121 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +189 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +135 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +656 -0
- package/src/components/facility-creation-modal/index.ts +9 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +105 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +188 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +460 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +134 -0
- package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +69 -0
- package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +379 -0
- package/src/components/facility-detail-modal/index.ts +2 -0
- package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
- package/src/components/file-attachment/index.ts +2 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +214 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +84 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +424 -0
- package/src/components/handbook-detail-modal/index.ts +3 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +175 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +533 -0
- package/src/components/handbook-folder/index.ts +4 -0
- package/src/components/header-content/ds-mobile-header-content.ts +222 -0
- package/src/components/header-content/index.ts +2 -0
- package/src/components/illustration/ds-mobile-illustration.ts +124 -0
- package/src/components/illustration/index.ts +2 -0
- package/src/components/index.ts +124 -0
- package/src/components/inline-photo/ds-mobile-inline-photo.ts +361 -0
- package/src/components/inline-photo/index.ts +1 -0
- package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +132 -0
- package/src/components/inline-tabs/index.ts +2 -0
- package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +350 -0
- package/src/components/interactive-list-item-booking/index.ts +1 -0
- package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +321 -0
- package/src/components/interactive-list-item-inquiry/index.ts +2 -0
- package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +237 -0
- package/src/components/interactive-list-item-message/index.ts +2 -0
- package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +549 -0
- package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
- package/src/components/interactive-list-item-post/index.ts +13 -0
- package/src/components/lightbox/ds-mobile-lightbox-footer.ts +315 -0
- package/src/components/lightbox/ds-mobile-lightbox-header.ts +202 -0
- package/src/components/lightbox/ds-mobile-lightbox-image.ts +484 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.css +377 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
- package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
- package/src/components/lightbox/ds-mobile-lightbox.service.ts +296 -0
- package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
- package/src/components/lightbox/index.ts +22 -0
- package/src/components/list-item/ds-mobile-list-item.ts +603 -0
- package/src/components/list-item/index.ts +2 -0
- package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
- package/src/components/list-item-static/index.ts +2 -0
- package/src/components/loader-overlay/ds-mobile-loader-overlay.css +49 -0
- package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +77 -0
- package/src/components/loader-overlay/index.ts +1 -0
- package/src/components/logo/ds-logo.ts +95 -0
- package/src/components/logo/index.ts +2 -0
- package/src/components/message-bubble/ds-mobile-message-bubble.ts +633 -0
- package/src/components/message-bubble/index.ts +7 -0
- package/src/components/message-composer/ds-mobile-message-composer.ts +1146 -0
- package/src/components/message-composer/index.ts +7 -0
- package/src/components/modal/ds-mobile-modal.css +163 -0
- package/src/components/modal/ds-mobile-modal.service.ts +329 -0
- package/src/components/modal/index.ts +8 -0
- package/src/components/modal-base/ds-mobile-modal-base.css +378 -0
- package/src/components/modal-base/ds-mobile-modal-base.ts +261 -0
- package/src/components/modal-base/index.ts +2 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +112 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +93 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +442 -0
- package/src/components/new-inquiry-modal/index.ts +4 -0
- package/src/components/offline-banner/ds-mobile-offline-banner.ts +135 -0
- package/src/components/offline-banner/index.ts +1 -0
- package/src/components/page-details/ds-mobile-page-details.css +83 -0
- package/src/components/page-details/ds-mobile-page-details.ts +282 -0
- package/src/components/page-details/index.ts +2 -0
- package/src/components/page-main/ds-mobile-page-main.css +68 -0
- package/src/components/page-main/ds-mobile-page-main.ts +421 -0
- package/src/components/page-main/index.ts +2 -0
- package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
- package/src/components/post-composer/index.ts +2 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +390 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +108 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +722 -0
- package/src/components/post-detail-modal/index.ts +9 -0
- package/src/components/property-banner/ds-mobile-property-banner.ts +95 -0
- package/src/components/property-banner/index.ts +2 -0
- package/src/components/section/ds-mobile-section.ts +263 -0
- package/src/components/section/index.ts +2 -0
- package/src/components/shared/directives/index.ts +2 -0
- package/src/components/shared/directives/long-press.directive.ts +212 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/shared/mobile-modal-base.ts +457 -0
- package/src/components/shared/mobile-page-base.ts +204 -0
- package/src/components/swiper/ds-mobile-swiper-with-nav.ts +160 -0
- package/src/components/swiper/ds-mobile-swiper.ts +327 -0
- package/src/components/swiper/index.ts +3 -0
- package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +129 -0
- package/src/components/system-message-banner/index.ts +2 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.css +533 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.ts +735 -0
- package/src/components/tab-bar/index.ts +2 -0
- package/src/components/tabs/ds-mobile-tabs.css +25 -0
- package/src/components/tabs/ds-mobile-tabs.ts +89 -0
- package/src/components/tabs/index.ts +2 -0
- package/src/components/text-input/ds-text-input.ts +287 -0
- package/src/components/text-input/index.ts +2 -0
- package/src/examples/booking.page.ts +434 -0
- package/src/examples/community.page.ts +776 -0
- package/src/examples/handbook.page.ts +324 -0
- package/src/examples/home.page.ts +347 -0
- package/src/examples/index.ts +12 -0
- package/src/examples/inquiries.example.ts +273 -0
- package/src/examples/inquiry-detail.example.css +189 -0
- package/src/examples/inquiry-detail.example.ts +415 -0
- package/src/examples/mobile-tabs-example.component.ts +208 -0
- package/src/examples/post-create.page.ts +311 -0
- package/src/examples/post-detail.page.ts +296 -0
- package/src/examples/sign-in.page.ts +291 -0
- package/src/examples/whitelabel-demo-modal.component.ts +1094 -0
- package/src/examples/whitelabel-demo-modal.service.ts +77 -0
- package/src/models/index.ts +7 -0
- package/src/models/post.model.ts +41 -0
- package/src/pages/community.page.ts +769 -0
- package/src/pages/handbook.page.ts +388 -0
- package/src/pages/home.page.ts +303 -0
- package/src/pages/index.ts +11 -0
- package/src/pages/inquiries.example.ts +273 -0
- package/src/pages/inquiry-detail.example.css +189 -0
- package/src/pages/inquiry-detail.example.ts +415 -0
- package/src/pages/mobile-tabs-example.component.ts +179 -0
- package/src/pages/post-create.page.ts +311 -0
- package/src/pages/post-detail.page.ts +296 -0
- package/src/pages/sign-in.page.ts +291 -0
- package/src/pages/whitelabel-demo-modal.component.ts +1094 -0
- package/src/pages/whitelabel-demo-modal.service.ts +77 -0
- package/src/public-api.ts +6 -0
- package/src/services/base-modal.service.ts +101 -0
- package/src/services/index.ts +11 -0
- package/src/services/posts.service.ts +542 -0
- package/src/services/tracking-permission.service.ts +88 -0
- package/src/services/user.service.ts +60 -0
- package/src/services/whitelabel.service.ts +675 -0
- package/{styles → src/styles}/ionic.css +25 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +13 -0
- package/fesm2022/propbinder-mobile-design.mjs +0 -26136
- package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
- package/index.d.ts +0 -8154
- /package/{assets → src/assets}/fonts/Brockmann-Bold.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-BoldItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-Medium.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-MediumItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-Regular.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-RegularItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-SemiBold.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann_desktop_license.pdf +0 -0
- /package/{assets → src/assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
- /package/{assets → src/assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
- /package/{assets → src/assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
- /package/{styles → src/components/shared}/mobile-common.css +0 -0
- /package/{styles → src/components/shared}/mobile-page-base.css +0 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { Component, Input, signal, computed, CUSTOM_ELEMENTS_SCHEMA, ViewChild, AfterViewInit } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ModalController } from '@ionic/angular/standalone';
|
|
4
|
+
import { DsIconComponent, DsButtonComponent, DsDatepickerComponent } from '@propbinder/design-system';
|
|
5
|
+
import { DsMobileModalBaseComponent } from '../modal-base/ds-mobile-modal-base';
|
|
6
|
+
import { DsMobileSectionComponent } from '../section';
|
|
7
|
+
import { DsMobileSwiperComponent } from '../swiper/ds-mobile-swiper';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Date option interface for date selection
|
|
11
|
+
*/
|
|
12
|
+
export interface DateOption {
|
|
13
|
+
id: string;
|
|
14
|
+
dayName: string; // "Tue"
|
|
15
|
+
date: string; // "19"
|
|
16
|
+
monthName: string; // "Nov"
|
|
17
|
+
fullDate: Date;
|
|
18
|
+
state: 'default' | 'selected' | 'disabled';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Time slot interface for time selection
|
|
23
|
+
*/
|
|
24
|
+
export interface TimeSlot {
|
|
25
|
+
id: string;
|
|
26
|
+
startTime: string; // "14:00"
|
|
27
|
+
endTime: string; // "16:00"
|
|
28
|
+
state: 'default' | 'selected' | 'disabled';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Booking result returned when user confirms booking
|
|
33
|
+
*/
|
|
34
|
+
export interface BookingResult {
|
|
35
|
+
facilityId: string;
|
|
36
|
+
facilityTitle: string;
|
|
37
|
+
selectedDate: DateOption;
|
|
38
|
+
selectedTimeSlot: TimeSlot;
|
|
39
|
+
timestamp: Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* DsMobileBookingModalComponent
|
|
44
|
+
*
|
|
45
|
+
* Full-screen modal for booking facilities with date and time selection.
|
|
46
|
+
* Features swipeable date selection and vertical time slot list.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const modal = await modalController.create({
|
|
51
|
+
* component: DsMobileBookingModalComponent,
|
|
52
|
+
* componentProps: {
|
|
53
|
+
* facilityId: 'facility-1',
|
|
54
|
+
* facilityTitle: 'Boremaskinen'
|
|
55
|
+
* }
|
|
56
|
+
* });
|
|
57
|
+
* await modal.present();
|
|
58
|
+
* const result = await modal.onWillDismiss<BookingResult>();
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
@Component({
|
|
62
|
+
selector: 'ds-mobile-booking-modal',
|
|
63
|
+
standalone: true,
|
|
64
|
+
imports: [
|
|
65
|
+
CommonModule,
|
|
66
|
+
DsMobileModalBaseComponent,
|
|
67
|
+
DsMobileSectionComponent,
|
|
68
|
+
DsMobileSwiperComponent,
|
|
69
|
+
DsIconComponent,
|
|
70
|
+
DsButtonComponent,
|
|
71
|
+
DsDatepickerComponent
|
|
72
|
+
],
|
|
73
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
74
|
+
styles: [`
|
|
75
|
+
/* Date item styles */
|
|
76
|
+
.date-item {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
width: 80px;
|
|
82
|
+
padding: 12px 8px;
|
|
83
|
+
border-radius: 12px;
|
|
84
|
+
border: 1px solid var(--color-border, #e5e5e5);
|
|
85
|
+
background: var(--color-surface-primary, #ffffff);
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
transition: all 200ms ease;
|
|
88
|
+
-webkit-tap-highlight-color: transparent;
|
|
89
|
+
gap: 4px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.date-item .day-name {
|
|
93
|
+
font-family: 'Brockmann', sans-serif;
|
|
94
|
+
font-size: var(--font-size-xs, 12px);
|
|
95
|
+
font-weight: 400;
|
|
96
|
+
color: var(--text-color-default-secondary, #71727a);
|
|
97
|
+
text-transform: uppercase;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.date-item .date-number {
|
|
101
|
+
font-family: 'Brockmann', sans-serif;
|
|
102
|
+
font-size: var(--font-size-xl, 20px);
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
color: var(--text-color-default-primary, #202227);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.date-item .month-name {
|
|
108
|
+
font-family: 'Brockmann', sans-serif;
|
|
109
|
+
font-size: var(--font-size-xs, 12px);
|
|
110
|
+
font-weight: 400;
|
|
111
|
+
color: var(--text-color-default-secondary, #71727a);
|
|
112
|
+
text-transform: capitalize;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Selected state */
|
|
116
|
+
.date-item.selected {
|
|
117
|
+
background: var(--color-accent, #5d5fef);
|
|
118
|
+
border-color: var(--color-accent, #5d5fef);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.date-item.selected .day-name,
|
|
122
|
+
.date-item.selected .date-number,
|
|
123
|
+
.date-item.selected .month-name {
|
|
124
|
+
color: #ffffff;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Disabled state */
|
|
128
|
+
.date-item.disabled {
|
|
129
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
130
|
+
border-color: var(--color-border, #e5e5e5);
|
|
131
|
+
cursor: not-allowed;
|
|
132
|
+
opacity: 0.6;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.date-item.disabled .day-name,
|
|
136
|
+
.date-item.disabled .date-number,
|
|
137
|
+
.date-item.disabled .month-name {
|
|
138
|
+
color: var(--text-color-default-tertiary, #9a9aa2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Hover effect for non-disabled items */
|
|
142
|
+
.date-item:not(.disabled):not(.selected):hover {
|
|
143
|
+
border-color: var(--color-accent, #5d5fef);
|
|
144
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Time slots list */
|
|
148
|
+
.time-slots-list {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 8px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.time-slot-item {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
justify-content: space-between;
|
|
158
|
+
width: 100%;
|
|
159
|
+
padding: 16px 20px;
|
|
160
|
+
border-radius: 12px;
|
|
161
|
+
border: 1px solid var(--color-border, #e5e5e5);
|
|
162
|
+
background: var(--color-surface-primary, #ffffff);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
transition: all 200ms ease;
|
|
165
|
+
-webkit-tap-highlight-color: transparent;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.time-slot-item span {
|
|
169
|
+
font-family: 'Brockmann', sans-serif;
|
|
170
|
+
font-size: var(--font-size-base, 16px);
|
|
171
|
+
font-weight: 400;
|
|
172
|
+
color: var(--text-color-default-primary, #202227);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Selected state */
|
|
176
|
+
.time-slot-item.selected {
|
|
177
|
+
background: var(--color-accent, #5d5fef);
|
|
178
|
+
border-color: var(--color-accent, #5d5fef);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.time-slot-item.selected span {
|
|
182
|
+
color: #ffffff;
|
|
183
|
+
font-weight: 500;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Disabled state */
|
|
187
|
+
.time-slot-item.disabled {
|
|
188
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
189
|
+
border-color: var(--color-border, #e5e5e5);
|
|
190
|
+
cursor: not-allowed;
|
|
191
|
+
opacity: 0.6;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.time-slot-item.disabled span {
|
|
195
|
+
color: var(--text-color-default-tertiary, #9a9aa2);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Hover effect for non-disabled items */
|
|
199
|
+
.time-slot-item:not(.disabled):not(.selected):hover {
|
|
200
|
+
border-color: var(--color-accent, #5d5fef);
|
|
201
|
+
background: var(--color-background-neutral-secondary, #f5f5f5);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Fixed bottom button */
|
|
205
|
+
.booking-confirm-action {
|
|
206
|
+
width: 100%;
|
|
207
|
+
padding: 16px 20px;
|
|
208
|
+
background: var(--color-surface-primary, #ffffff);
|
|
209
|
+
border-top: 1px solid var(--color-border, #e5e5e5);
|
|
210
|
+
box-sizing: border-box;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.booking-confirm-action ::ng-deep ds-button {
|
|
214
|
+
display: block;
|
|
215
|
+
width: 100%;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.booking-confirm-action ::ng-deep ds-button button {
|
|
219
|
+
width: 100%;
|
|
220
|
+
border-radius: 100px;
|
|
221
|
+
height: 48px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Swiper slide adjustments */
|
|
225
|
+
::ng-deep .date-swiper .swiper-slide {
|
|
226
|
+
width: auto !important;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Styled to match ds-mobile-section's .section-link */
|
|
230
|
+
.calendar-link {
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
gap: 2px;
|
|
234
|
+
font-family: 'Brockmann', sans-serif;
|
|
235
|
+
font-size: var(--font-size-sm);
|
|
236
|
+
font-weight: 500;
|
|
237
|
+
color: var(--color-accent, #5d5fef);
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
white-space: nowrap;
|
|
240
|
+
user-select: none;
|
|
241
|
+
line-height: 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Ensure the CDK overlay renders above the Ionic modal stack */
|
|
245
|
+
::ng-deep .cdk-overlay-container {
|
|
246
|
+
z-index: 20001;
|
|
247
|
+
}
|
|
248
|
+
`],
|
|
249
|
+
template: `
|
|
250
|
+
<ds-mobile-modal-base
|
|
251
|
+
headerTitle="Hvornår skal det være?"
|
|
252
|
+
[showCloseButton]="true"
|
|
253
|
+
[hasFixedBottom]="true"
|
|
254
|
+
(closeClicked)="handleClose()">
|
|
255
|
+
|
|
256
|
+
<!-- Date Section -->
|
|
257
|
+
<ds-mobile-section headline="Vælg dato">
|
|
258
|
+
<ds-datepicker
|
|
259
|
+
header-action
|
|
260
|
+
[isDateDisabled]="dateDisabledFn()"
|
|
261
|
+
(dateChange)="jumpToDate($event)">
|
|
262
|
+
<span class="calendar-link">
|
|
263
|
+
{{ selectFromCalendarText }}
|
|
264
|
+
<ds-icon name="remixArrowRightSLine" size="16px" />
|
|
265
|
+
</span>
|
|
266
|
+
</ds-datepicker>
|
|
267
|
+
|
|
268
|
+
<ds-mobile-swiper
|
|
269
|
+
class="date-swiper"
|
|
270
|
+
[slideWidth]="'auto'"
|
|
271
|
+
[gap]="12">
|
|
272
|
+
@for (date of dateOptions(); track date.id) {
|
|
273
|
+
<div class="swiper-slide">
|
|
274
|
+
<button
|
|
275
|
+
class="date-item"
|
|
276
|
+
[class.selected]="date.state === 'selected'"
|
|
277
|
+
[class.disabled]="date.state === 'disabled'"
|
|
278
|
+
[disabled]="date.state === 'disabled'"
|
|
279
|
+
(click)="selectDate(date)">
|
|
280
|
+
<span class="day-name">{{ date.dayName }}</span>
|
|
281
|
+
<span class="date-number">{{ date.date }}</span>
|
|
282
|
+
<span class="month-name">{{ date.monthName }}</span>
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
}
|
|
286
|
+
</ds-mobile-swiper>
|
|
287
|
+
</ds-mobile-section>
|
|
288
|
+
|
|
289
|
+
<!-- Time Section -->
|
|
290
|
+
<ds-mobile-section headline="Vælg tidspunkt">
|
|
291
|
+
<div class="time-slots-list">
|
|
292
|
+
@for (slot of timeSlots(); track slot.id) {
|
|
293
|
+
<button
|
|
294
|
+
class="time-slot-item"
|
|
295
|
+
[class.selected]="slot.state === 'selected'"
|
|
296
|
+
[class.disabled]="slot.state === 'disabled'"
|
|
297
|
+
[disabled]="slot.state === 'disabled'"
|
|
298
|
+
(click)="selectTime(slot)">
|
|
299
|
+
<span>{{ slot.startTime }} - {{ slot.endTime }}</span>
|
|
300
|
+
@if (slot.state === 'selected') {
|
|
301
|
+
<ds-icon name="remixCheckLine" size="20px" color="#ffffff" />
|
|
302
|
+
}
|
|
303
|
+
</button>
|
|
304
|
+
}
|
|
305
|
+
</div>
|
|
306
|
+
</ds-mobile-section>
|
|
307
|
+
|
|
308
|
+
<!-- Fixed Bottom Button -->
|
|
309
|
+
<div fixed-bottom class="booking-confirm-action">
|
|
310
|
+
<ds-button
|
|
311
|
+
size="lg"
|
|
312
|
+
variant="primary"
|
|
313
|
+
[fullWidth]="true"
|
|
314
|
+
[disabled]="!canConfirm()"
|
|
315
|
+
[loading]="isConfirming()"
|
|
316
|
+
(clicked)="handleConfirm()">
|
|
317
|
+
{{ confirmBookingText }}
|
|
318
|
+
</ds-button>
|
|
319
|
+
</div>
|
|
320
|
+
</ds-mobile-modal-base>
|
|
321
|
+
`
|
|
322
|
+
})
|
|
323
|
+
export class DsMobileBookingModalComponent implements AfterViewInit {
|
|
324
|
+
@Input() facilityId!: string;
|
|
325
|
+
@Input() facilityTitle!: string;
|
|
326
|
+
/**
|
|
327
|
+
* Number of days ahead available for booking selection.
|
|
328
|
+
* Defaults to 60 (2 months). Override via componentProps when opening the modal.
|
|
329
|
+
*/
|
|
330
|
+
@Input() daysAhead: number = 60;
|
|
331
|
+
|
|
332
|
+
@Input() selectFromCalendarText: string = 'Vælg fra kalender';
|
|
333
|
+
@Input() confirmBookingText: string = 'Bekræft booking';
|
|
334
|
+
@Input() availableDates?: DateOption[];
|
|
335
|
+
@Input() availableTimeSlots?: Record<string, TimeSlot[]>;
|
|
336
|
+
|
|
337
|
+
@ViewChild(DsMobileSwiperComponent) swiperComponent?: DsMobileSwiperComponent;
|
|
338
|
+
|
|
339
|
+
// Signals for reactive state management
|
|
340
|
+
dateOptions = signal<DateOption[]>([]);
|
|
341
|
+
timeSlots = signal<TimeSlot[]>([]);
|
|
342
|
+
|
|
343
|
+
selectedDate = signal<DateOption | null>(null);
|
|
344
|
+
selectedTimeSlot = signal<TimeSlot | null>(null);
|
|
345
|
+
isConfirming = signal<boolean>(false);
|
|
346
|
+
|
|
347
|
+
// Computed property for button state
|
|
348
|
+
canConfirm = computed(() => {
|
|
349
|
+
return this.selectedDate() !== null && this.selectedTimeSlot() !== null && !this.isConfirming();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
constructor(private modalController: ModalController) {
|
|
353
|
+
this.generateMockData();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* After view init - force swiper update to fix initial positioning
|
|
358
|
+
*/
|
|
359
|
+
ngAfterViewInit(): void {
|
|
360
|
+
// Use a slightly longer timeout to ensure swiper is fully initialized
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
if (this.swiperComponent && (this.swiperComponent as any).swiperInstance) {
|
|
363
|
+
const swiperInstance = (this.swiperComponent as any).swiperInstance;
|
|
364
|
+
// Update swiper to recalculate positions
|
|
365
|
+
swiperInstance.update();
|
|
366
|
+
// Slide to first slide to ensure proper centering
|
|
367
|
+
swiperInstance.slideTo(0, 0);
|
|
368
|
+
}
|
|
369
|
+
}, 150);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Returns true if the given date should be disabled in both the swiper and the datepicker.
|
|
374
|
+
* Weekends are disabled. Index-based mock disabling (i===3, i===7) is swiper-only and
|
|
375
|
+
* not representable as a date rule, so it is intentionally excluded here.
|
|
376
|
+
*/
|
|
377
|
+
private isDateUnavailable(date: Date): boolean {
|
|
378
|
+
return date.getDay() === 0 || date.getDay() === 6;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Computed signal that returns a fresh disabled-date function whenever dateOptions()
|
|
383
|
+
* changes. Returning a new function reference causes the datepicker's own isDateDisabled
|
|
384
|
+
* input to update, which triggers its internal computed to re-run and re-render all
|
|
385
|
+
* calendar cells with the correct disabled state.
|
|
386
|
+
* The disabledSet is pre-built once per computation for O(1) per-cell lookups.
|
|
387
|
+
*/
|
|
388
|
+
dateDisabledFn = computed(() => {
|
|
389
|
+
const disabledSet = new Set(
|
|
390
|
+
this.dateOptions()
|
|
391
|
+
.filter(opt => opt.state === 'disabled')
|
|
392
|
+
.map(opt => opt.fullDate.toDateString())
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
return (date: Date): boolean => {
|
|
396
|
+
const today = new Date(); today.setHours(0, 0, 0, 0);
|
|
397
|
+
const max = new Date(today); max.setDate(today.getDate() + this.daysAhead - 1);
|
|
398
|
+
const d = new Date(date); d.setHours(0, 0, 0, 0);
|
|
399
|
+
if (d < today || d > max) return true;
|
|
400
|
+
return disabledSet.has(date.toDateString());
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Generate mock date and time data or use provided available data
|
|
406
|
+
*/
|
|
407
|
+
private generateMockData(): void {
|
|
408
|
+
if (this.availableDates && this.availableDates.length > 0) {
|
|
409
|
+
this.dateOptions.set(this.availableDates);
|
|
410
|
+
|
|
411
|
+
const firstAvailableDate = this.availableDates.find(d => d.state !== 'disabled') || this.availableDates[0];
|
|
412
|
+
if (firstAvailableDate) {
|
|
413
|
+
// Ensure strictly one selected
|
|
414
|
+
const updatedDates = this.availableDates.map(date => ({
|
|
415
|
+
...date,
|
|
416
|
+
state: date.id === firstAvailableDate.id ? 'selected' as const :
|
|
417
|
+
(date.state === 'disabled' ? 'disabled' as const : 'default' as const)
|
|
418
|
+
}));
|
|
419
|
+
this.dateOptions.set(updatedDates);
|
|
420
|
+
|
|
421
|
+
const selectedRef = updatedDates.find(d => d.id === firstAvailableDate.id);
|
|
422
|
+
if (selectedRef) {
|
|
423
|
+
this.selectedDate.set(selectedRef);
|
|
424
|
+
this.generateTimeSlots(selectedRef.fullDate, selectedRef.date);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const dates: DateOption[] = [];
|
|
431
|
+
const today = new Date();
|
|
432
|
+
|
|
433
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
434
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
435
|
+
|
|
436
|
+
let firstAvailableDate: DateOption | null = null;
|
|
437
|
+
|
|
438
|
+
for (let i = 0; i < this.daysAhead; i++) {
|
|
439
|
+
const date = new Date(today);
|
|
440
|
+
date.setDate(today.getDate() + i);
|
|
441
|
+
|
|
442
|
+
const isDisabled = this.isDateUnavailable(date) || (i === 3) || (i === 7);
|
|
443
|
+
|
|
444
|
+
const dateOption: DateOption = {
|
|
445
|
+
id: `date-${i}`,
|
|
446
|
+
dayName: dayNames[date.getDay()],
|
|
447
|
+
date: date.getDate().toString(),
|
|
448
|
+
monthName: monthNames[date.getMonth()],
|
|
449
|
+
fullDate: date,
|
|
450
|
+
state: isDisabled ? 'disabled' : 'default'
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Track the first available (non-disabled) date
|
|
454
|
+
if (!isDisabled && !firstAvailableDate) {
|
|
455
|
+
firstAvailableDate = dateOption;
|
|
456
|
+
dateOption.state = 'selected'; // Set as selected
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
dates.push(dateOption);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.dateOptions.set(dates);
|
|
463
|
+
|
|
464
|
+
// Set the first available date as selected and generate time slots for it
|
|
465
|
+
if (firstAvailableDate) {
|
|
466
|
+
this.selectedDate.set(firstAvailableDate);
|
|
467
|
+
this.generateTimeSlots(firstAvailableDate.fullDate, firstAvailableDate.date);
|
|
468
|
+
} else {
|
|
469
|
+
// Fallback to today if no available dates
|
|
470
|
+
this.generateTimeSlots(today);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Generate time slots based on selected date and dynamic available timeslots
|
|
476
|
+
*/
|
|
477
|
+
private generateTimeSlots(date: Date, dateStringKey?: string): void {
|
|
478
|
+
if (this.availableTimeSlots && dateStringKey && this.availableTimeSlots[dateStringKey]) {
|
|
479
|
+
this.timeSlots.set(this.availableTimeSlots[dateStringKey]);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const slots: TimeSlot[] = [];
|
|
484
|
+
const currentHour = new Date().getHours();
|
|
485
|
+
const isToday = date.toDateString() === new Date().toDateString();
|
|
486
|
+
|
|
487
|
+
// Generate time slots from 08:00 to 20:00 in 2-hour increments
|
|
488
|
+
const startHour = 8;
|
|
489
|
+
const endHour = 20;
|
|
490
|
+
|
|
491
|
+
for (let hour = startHour; hour < endHour; hour += 2) {
|
|
492
|
+
const startTime = `${hour.toString().padStart(2, '0')}:00`;
|
|
493
|
+
const endTime = `${(hour + 2).toString().padStart(2, '0')}:00`;
|
|
494
|
+
|
|
495
|
+
// Disable past times if today is selected
|
|
496
|
+
const isPast = isToday && hour < currentHour;
|
|
497
|
+
// Randomly disable some slots for demo purposes
|
|
498
|
+
const isRandomlyDisabled = Math.random() > 0.7;
|
|
499
|
+
const isDisabled = isPast || isRandomlyDisabled;
|
|
500
|
+
|
|
501
|
+
slots.push({
|
|
502
|
+
id: `slot-${hour}`,
|
|
503
|
+
startTime,
|
|
504
|
+
endTime,
|
|
505
|
+
state: isDisabled ? 'disabled' : 'default'
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
this.timeSlots.set(slots);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Handle date selection
|
|
514
|
+
*/
|
|
515
|
+
selectDate(selectedDate: DateOption): void {
|
|
516
|
+
if (selectedDate.state === 'disabled') return;
|
|
517
|
+
|
|
518
|
+
// Update date options
|
|
519
|
+
const updatedDates = this.dateOptions().map(date => ({
|
|
520
|
+
...date,
|
|
521
|
+
state: date.id === selectedDate.id ? 'selected' as const :
|
|
522
|
+
(date.state === 'disabled' ? 'disabled' as const : 'default' as const)
|
|
523
|
+
}));
|
|
524
|
+
|
|
525
|
+
this.dateOptions.set(updatedDates);
|
|
526
|
+
this.selectedDate.set(selectedDate);
|
|
527
|
+
|
|
528
|
+
// Regenerate time slots for the selected date
|
|
529
|
+
this.generateTimeSlots(selectedDate.fullDate, selectedDate.date);
|
|
530
|
+
|
|
531
|
+
// Reset time selection
|
|
532
|
+
this.selectedTimeSlot.set(null);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Handle time slot selection
|
|
537
|
+
*/
|
|
538
|
+
selectTime(selectedSlot: TimeSlot): void {
|
|
539
|
+
if (selectedSlot.state === 'disabled') return;
|
|
540
|
+
|
|
541
|
+
// Update time slots
|
|
542
|
+
const updatedSlots = this.timeSlots().map(slot => ({
|
|
543
|
+
...slot,
|
|
544
|
+
state: slot.id === selectedSlot.id ? 'selected' as const :
|
|
545
|
+
(slot.state === 'disabled' ? 'disabled' as const : 'default' as const)
|
|
546
|
+
}));
|
|
547
|
+
|
|
548
|
+
this.timeSlots.set(updatedSlots);
|
|
549
|
+
this.selectedTimeSlot.set(selectedSlot);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Called when the datepicker overlay emits a date selection.
|
|
554
|
+
* Finds the matching DateOption in the swiper, selects it, and auto-scrolls to it.
|
|
555
|
+
*/
|
|
556
|
+
jumpToDate(date: Date | null): void {
|
|
557
|
+
if (!date) return;
|
|
558
|
+
const options = this.dateOptions();
|
|
559
|
+
const idx = options.findIndex(
|
|
560
|
+
opt => opt.fullDate.toDateString() === date.toDateString()
|
|
561
|
+
);
|
|
562
|
+
if (idx === -1) return;
|
|
563
|
+
const option = options[idx];
|
|
564
|
+
if (option.state === 'disabled') return;
|
|
565
|
+
this.selectDate(option);
|
|
566
|
+
this.swiperComponent?.slideTo(idx);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Handle confirm button click
|
|
571
|
+
*/
|
|
572
|
+
async handleConfirm(): Promise<void> {
|
|
573
|
+
if (!this.canConfirm()) return;
|
|
574
|
+
|
|
575
|
+
// Set loading state
|
|
576
|
+
this.isConfirming.set(true);
|
|
577
|
+
|
|
578
|
+
// Simulate booking API call with 2 second delay
|
|
579
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
580
|
+
|
|
581
|
+
const result: BookingResult = {
|
|
582
|
+
facilityId: this.facilityId,
|
|
583
|
+
facilityTitle: this.facilityTitle,
|
|
584
|
+
selectedDate: this.selectedDate()!,
|
|
585
|
+
selectedTimeSlot: this.selectedTimeSlot()!,
|
|
586
|
+
timestamp: new Date()
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
await this.modalController.dismiss(result, 'confirm');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Handle close button click
|
|
594
|
+
*/
|
|
595
|
+
async handleClose(): Promise<void> {
|
|
596
|
+
await this.modalController.dismiss(null, 'cancel');
|
|
597
|
+
}
|
|
598
|
+
}
|