@wix/himalaya-cli 0.1582.0 → 0.1584.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/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +8 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +26 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validator.d.ts +18 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/extensions.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/comments-v1/src/extensions.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/events/src/index.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/groups/src/extensions.d.ts +15 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/loyalty/src/extensions.d.ts +13 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-v1/src/index.d.ts +19 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +5 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +5 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-comments.mjs +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +4 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-groups.mjs +5 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-loyalty.mjs +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members.mjs +10 -3
- package/dist/cli.mjs +1098 -702
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +25 -0
- package/guides/component-reference.json +4 -3
- package/guides/component-reference.md +24 -2
- package/guides/one-app-to-himi-app.md +52 -1
- package/guides/sdk-reference.json +37 -9
- package/guides/wix-api-usage.md +9 -0
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +27 -1
- package/templates/beauty-salon/dev/index.ts +2285 -757
- package/templates/beauty-salon/himalaya.content.json +3 -0
- package/templates/beauty-salon/tier5-src/_shared/booking-location.ts +147 -0
- package/templates/beauty-salon/tier5-src/_shared/booking-session.ts +222 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/beauty-salon/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/beauty-salon/tier5-src/_shared/owner-editor-value.ts +164 -0
- package/templates/beauty-salon/tier5-src/_shared/store-product-info.ts +50 -0
- package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +277 -14
- package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +351 -92
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +25 -5
- package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
- package/templates/beauty-salon/tier5-src/himi-branded-lite-category/index.ts +3 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +2 -1
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +14 -3
- package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
- package/templates/beauty-salon/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/beauty-salon-owner/dev/index.ts +129 -66
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
- package/templates/branded-lite/config.ts +29 -1
- package/templates/branded-lite/dev/index.ts +8371 -5565
- package/templates/branded-lite/himalaya.content.json +3 -0
- package/templates/branded-lite/l10n/en.json +2 -2
- package/templates/branded-lite/tier5-src/_shared/booking-location.ts +147 -0
- package/templates/branded-lite/tier5-src/_shared/booking-session.ts +222 -0
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/branded-lite/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/branded-lite/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/branded-lite/tier5-src/_shared/owner-editor-value.ts +164 -0
- package/templates/branded-lite/tier5-src/_shared/store-product-info.ts +50 -0
- package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +277 -14
- package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +351 -92
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +25 -5
- package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
- package/templates/branded-lite/tier5-src/himi-branded-lite-badges/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-category/index.ts +3 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-feed/index.ts +4 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +115 -49
- package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-podcasts/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/facts.ts +52 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +21 -11
- package/templates/branded-lite/tier5-src/himi-branded-lite-video/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/branded-lite/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +13 -3
- package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +3 -2
- package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +53 -37
- package/templates/branded-lite/tier5-src/wix-spaces-members/index.ts +100 -34
- package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
- package/templates/branded-lite/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/community-club/config.ts +36 -1
- package/templates/community-club/dev/index.ts +1930 -401
- package/templates/community-club/himalaya.content.json +6 -0
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/community-club/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/community-club/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/community-club/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/community-club/tier5-src/himi-branded-lite-feed/index.ts +4 -3
- package/templates/community-club/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
- package/templates/community-club/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/community-club/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/community-club/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/community-club-owner/dev/index.ts +53 -1
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/community-club-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/community-club-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/events-meetups/config.ts +36 -1
- package/templates/events-meetups/dev/index.ts +1930 -401
- package/templates/events-meetups/himalaya.content.json +6 -0
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/events-meetups/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/events-meetups/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/events-meetups/tier5-src/himi-branded-lite-feed/index.ts +4 -3
- package/templates/events-meetups/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
- package/templates/events-meetups/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/events-meetups/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/events-meetups/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/events-meetups-owner/dev/index.ts +6 -0
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/gym-fitness/config.ts +27 -1
- package/templates/gym-fitness/dev/index.ts +2803 -844
- package/templates/gym-fitness/himalaya.content.json +3 -0
- package/templates/gym-fitness/tier5-src/_shared/booking-location.ts +147 -0
- package/templates/gym-fitness/tier5-src/_shared/booking-session.ts +222 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/gym-fitness/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/gym-fitness/tier5-src/_shared/owner-editor-value.ts +164 -0
- package/templates/gym-fitness/tier5-src/_shared/store-product-info.ts +50 -0
- package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +277 -14
- package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +351 -92
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +25 -5
- package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
- package/templates/gym-fitness/tier5-src/himi-branded-lite-category/index.ts +3 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +2 -1
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/gym-fitness/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +14 -3
- package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
- package/templates/gym-fitness/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/gym-fitness-owner/dev/index.ts +132 -66
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
- package/templates/nonprofit-cause/config.ts +36 -1
- package/templates/nonprofit-cause/dev/index.ts +1925 -396
- package/templates/nonprofit-cause/himalaya.content.json +6 -0
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/nonprofit-cause/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-feed/index.ts +4 -3
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +2 -1
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/nonprofit-cause/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
- package/templates/nonprofit-cause/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +85 -65
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-rich-guard.ts +27 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
- package/templates/owner-lite/dev/index.ts +132 -66
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/owner-lite/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/owner-lite/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/owner-lite/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/owner-lite/tier5-src/himi-owner-lite-service/index.ts +38 -7
- package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
- package/templates/owner-lite/tier5-src/himi-owner-lite-services/index.ts +4 -2
- package/templates/podcast-creator/config.ts +27 -1
- package/templates/podcast-creator/dev/index.ts +1051 -158
- package/templates/podcast-creator/himalaya.content.json +3 -0
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/podcast-creator/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/podcast-creator/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/podcast-creator/tier5-src/himi-branded-lite-feed/index.ts +4 -3
- package/templates/podcast-creator/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +2 -1
- package/templates/podcast-creator/tier5-src/himi-branded-lite-podcasts/index.ts +2 -1
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/podcast-creator/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
- package/templates/podcast-creator/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/podcast-creator-owner/dev/index.ts +53 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/portfolio-creative/config.ts +27 -1
- package/templates/portfolio-creative/dev/index.ts +1305 -468
- package/templates/portfolio-creative/himalaya.content.json +3 -0
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/portfolio-creative/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/portfolio-creative/tier5-src/_shared/owner-editor-value.ts +164 -0
- package/templates/portfolio-creative/tier5-src/_shared/store-product-info.ts +50 -0
- package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-category/index.ts +3 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio/index.ts +2 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -2
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/portfolio-creative/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +14 -3
- package/templates/portfolio-creative/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/portfolio-creative-owner/dev/index.ts +129 -66
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
- package/templates/restaurant/config.ts +27 -1
- package/templates/restaurant/dev/index.ts +1051 -158
- package/templates/restaurant/himalaya.content.json +3 -0
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/restaurant/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/restaurant/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/restaurant/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/restaurant/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/restaurant/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/restaurant/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/restaurant-owner/dev/index.ts +132 -66
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
- package/templates/retail-store/config.ts +27 -1
- package/templates/retail-store/dev/index.ts +597 -191
- package/templates/retail-store/himalaya.content.json +3 -0
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/retail-store/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/retail-store/tier5-src/_shared/owner-editor-value.ts +164 -0
- package/templates/retail-store/tier5-src/_shared/store-product-info.ts +50 -0
- package/templates/retail-store/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/retail-store/tier5-src/himi-branded-lite-category/index.ts +3 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +13 -3
- package/templates/retail-store/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/retail-store-owner/dev/index.ts +50 -1
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
- package/templates/yoga-studio/config.ts +27 -1
- package/templates/yoga-studio/dev/index.ts +2996 -877
- package/templates/yoga-studio/himalaya.content.json +3 -0
- package/templates/yoga-studio/tier5-src/_shared/booking-location.ts +147 -0
- package/templates/yoga-studio/tier5-src/_shared/booking-session.ts +222 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +1 -1
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/yoga-studio/tier5-src/_shared/event-registration.ts +86 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-restaurants.ts +2 -1
- package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +21 -8
- package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +277 -14
- package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +351 -92
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +25 -5
- package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
- package/templates/yoga-studio/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
- package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +2 -1
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/facts.ts +52 -0
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +21 -11
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +12 -86
- package/templates/yoga-studio/tier5-src/himi-events-list/index.ts +47 -6
- package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
- package/templates/yoga-studio/tier5-src/wix-spaces-whats-new/index.ts +2 -1
- package/templates/yoga-studio-owner/dev/index.ts +85 -65
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +19 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-rich-guard.ts +27 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The member's chosen booking location, shared by the Services, Agenda and Choose Time
|
|
2
|
+
// screens (Branded Lite bookings). Each screen has its own worker, so the choice lives in the
|
|
3
|
+
// on-device store: one `booking_prefs` row, read when a screen loads and written when the member
|
|
4
|
+
// picks a location. A bundle in an app that has not granted `local_database` (or has no
|
|
5
|
+
// `booking_prefs` entity) falls back to "All locations" on every screen — every call is wrapped.
|
|
6
|
+
//
|
|
7
|
+
// Keys: a business location's id (the same id a Wix availability slot carries in
|
|
8
|
+
// `location.id`), else "name|address" for a location with no id. "" is "All locations".
|
|
9
|
+
|
|
10
|
+
import { get, upsert, type DbEntity } from "@himalaya/local-db";
|
|
11
|
+
import { copyParam, copyTemplate } from "./himi-copy.js";
|
|
12
|
+
|
|
13
|
+
export const BOOKING_PREFS_ENTITY = "booking_prefs";
|
|
14
|
+
const LOCATION_ROW = "location";
|
|
15
|
+
|
|
16
|
+
/** The shared choice: a location key ("" = every location) and the label it was chosen under. */
|
|
17
|
+
export interface SharedLocation { id: string; label: string }
|
|
18
|
+
|
|
19
|
+
export const ALL_LOCATIONS: SharedLocation = { id: "", label: "" };
|
|
20
|
+
|
|
21
|
+
/** How long a screen's load waits for the store before it shows every location. */
|
|
22
|
+
const READ_TIMEOUT_MS = 800;
|
|
23
|
+
|
|
24
|
+
/** `promise`, or a rejection after `ms` where the host has a timer: a store call the native bridge
|
|
25
|
+
* never answers must not hang a screen's load. Timer-less hosts get the raw promise. */
|
|
26
|
+
function bounded<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
27
|
+
const host = globalThis as { setTimeout?: (fn: () => void, ms: number) => unknown; clearTimeout?: (timer: unknown) => void };
|
|
28
|
+
if (typeof host.setTimeout !== "function") return promise;
|
|
29
|
+
return new Promise<T>((resolve, reject) => {
|
|
30
|
+
const timer = host.setTimeout!(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
31
|
+
promise.then(
|
|
32
|
+
(v) => { try { host.clearTimeout?.(timer); } catch { /* best-effort */ } resolve(v); },
|
|
33
|
+
(e) => { try { host.clearTimeout?.(timer); } catch { /* best-effort */ } reject(e); },
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The shared choice, or `undefined` when the store could not answer (no capability, no entity, a
|
|
39
|
+
* bridge that never replied) — distinct from a stored "All locations", so a screen can keep its own
|
|
40
|
+
* choice when the shared one is unknown rather than reset it. */
|
|
41
|
+
export async function readSharedLocationResult(): Promise<SharedLocation | undefined> {
|
|
42
|
+
try {
|
|
43
|
+
const row = (await bounded(get(BOOKING_PREFS_ENTITY, LOCATION_ROW), READ_TIMEOUT_MS)) as { locationId?: unknown; locationLabel?: unknown } | null;
|
|
44
|
+
const id = typeof row?.locationId === "string" ? row.locationId : "";
|
|
45
|
+
const label = typeof row?.locationLabel === "string" ? row.locationLabel : "";
|
|
46
|
+
return { id, label };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn("booking_prefs: the shared location could not be read", err);
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The shared choice, "All locations" when it cannot be read. */
|
|
54
|
+
export async function readSharedLocation(): Promise<SharedLocation> {
|
|
55
|
+
return (await readSharedLocationResult()) ?? ALL_LOCATIONS;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function writeSharedLocation(location: SharedLocation): Promise<void> {
|
|
59
|
+
try {
|
|
60
|
+
// Bounded like the read: a bridge that never answers must not hold a screen's action open.
|
|
61
|
+
await bounded(upsert(BOOKING_PREFS_ENTITY, { id: LOCATION_ROW, locationId: location.id, locationLabel: location.label } as DbEntity), READ_TIMEOUT_MS);
|
|
62
|
+
} catch { /* capability not granted: the choice stays on this screen */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One location a screen can filter by. */
|
|
66
|
+
export interface LocationChoice { key: string; label: string; sub: string }
|
|
67
|
+
|
|
68
|
+
type WixLocationLike = { id?: unknown; name?: unknown; formattedAddress?: unknown; address?: unknown; locationType?: unknown };
|
|
69
|
+
|
|
70
|
+
/** A Wix location (a slot's `location`, or a service location's `business` / `custom` part) as a
|
|
71
|
+
* choice. A location with neither a name nor an address has no key: it is not offered.
|
|
72
|
+
*
|
|
73
|
+
* The key is what every bookings screen compares by, so it must come out the same from a service
|
|
74
|
+
* location and from a slot at it: a BUSINESS location by its id (a slot carries that id); any other
|
|
75
|
+
* place by its address, or its name when it has none — a custom place's slot carries no id, so the
|
|
76
|
+
* service's custom id would match none of its sessions. `business` defaults to the slot's own
|
|
77
|
+
* `locationType`. */
|
|
78
|
+
export function locationChoice(loc: WixLocationLike | undefined, business?: boolean): LocationChoice {
|
|
79
|
+
const name = typeof loc?.name === "string" ? loc.name.trim() : "";
|
|
80
|
+
const rawAddress = loc?.formattedAddress ?? (loc?.address as { formattedAddress?: unknown } | undefined)?.formattedAddress;
|
|
81
|
+
const address = typeof rawAddress === "string" ? rawAddress.trim() : "";
|
|
82
|
+
const id = typeof loc?.id === "string" ? loc.id : "";
|
|
83
|
+
if (!name && !address) return { key: "", label: "", sub: "" };
|
|
84
|
+
const isBusiness = business ?? (loc?.locationType === "OWNER_BUSINESS" || loc?.locationType === "BUSINESS");
|
|
85
|
+
const key = isBusiness && id ? id : address ? `addr:${address.toLowerCase()}` : `name:${name.toLowerCase()}`;
|
|
86
|
+
return { key, label: name || address, sub: name && address ? address : "" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The keys of a Wix service's own locations (business and custom), in the service's order. */
|
|
90
|
+
export function serviceLocationChoices(locations: unknown): LocationChoice[] {
|
|
91
|
+
if (!Array.isArray(locations)) return [];
|
|
92
|
+
const out: LocationChoice[] = [];
|
|
93
|
+
for (const raw of locations) {
|
|
94
|
+
const l = (raw ?? {}) as { type?: unknown; business?: Record<string, unknown>; custom?: Record<string, unknown>; calculatedAddress?: Record<string, unknown> };
|
|
95
|
+
const business = l.type === "BUSINESS" || !!l.business;
|
|
96
|
+
const part = business ? l.business : l.custom;
|
|
97
|
+
const choice = locationChoice(part ? { ...part, address: part.address ?? l.calculatedAddress } : undefined, business);
|
|
98
|
+
if (choice.key && !out.some((c) => c.key === choice.key)) out.push(choice);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Distinct choices in first-seen order. */
|
|
104
|
+
export function distinctChoices(choices: Iterable<LocationChoice>): LocationChoice[] {
|
|
105
|
+
const out: LocationChoice[] = [];
|
|
106
|
+
for (const c of choices) if (c.key && !out.some((o) => o.key === c.key)) out.push(c);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A row of the location sheet ("All locations" first). `id` "" is "All locations". */
|
|
111
|
+
export interface LocationOptionRow {
|
|
112
|
+
id: string; label: string; sub: string; hasSub: boolean; noSub: boolean;
|
|
113
|
+
selected: boolean; notSelected: boolean; a11y: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The location menu's bound fields for a screen offering `choices` with `selectedKey` chosen.
|
|
117
|
+
* Two or more choices show the menu; a selected key that is no longer offered reads as
|
|
118
|
+
* "All locations" (the caller filters nothing for it either — see `effectiveLocation`). */
|
|
119
|
+
export function locationMenu(choices: LocationChoice[], selectedKey: string): {
|
|
120
|
+
locationOptions: LocationOptionRow[]; locationLabel: string; locationA11y: string;
|
|
121
|
+
hasLocationMenu: boolean; noLocationMenu: boolean; locationActive: boolean; noLocationActive: boolean;
|
|
122
|
+
} {
|
|
123
|
+
// bookingCommon.*: every bookings screen forwards that prefix, so a copy pack reaches each picker.
|
|
124
|
+
const all = copyParam("bookingCommon.allLocations", "All locations");
|
|
125
|
+
const key = effectiveLocation(choices, selectedKey);
|
|
126
|
+
const label = key ? (choices.find((c) => c.key === key)?.label ?? all) : all;
|
|
127
|
+
const menu = choices.length >= 2;
|
|
128
|
+
return {
|
|
129
|
+
locationOptions: menu ? [{ key: "", label: all, sub: "" }, ...choices].map((c) => {
|
|
130
|
+
const selected = c.key === key;
|
|
131
|
+
return { id: c.key, label: c.label, sub: c.sub, hasSub: !!c.sub, noSub: !c.sub, selected, notSelected: !selected,
|
|
132
|
+
a11y: [c.label, c.sub, selected ? copyParam("bookingCommon.selected", "Selected") : ""].filter(Boolean).join(", ") };
|
|
133
|
+
}) : [],
|
|
134
|
+
locationLabel: label,
|
|
135
|
+
locationA11y: copyTemplate("bookingCommon.locationLabel", "Location, {label}", { label }),
|
|
136
|
+
hasLocationMenu: menu,
|
|
137
|
+
noLocationMenu: !menu,
|
|
138
|
+
locationActive: menu && !!key,
|
|
139
|
+
noLocationActive: !(menu && !!key),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The key a screen filters by: the chosen one while the screen offers it (and offers a choice
|
|
144
|
+
* at all), else "" — a location from another screen this one does not have filters nothing. */
|
|
145
|
+
export function effectiveLocation(choices: LocationChoice[], selectedKey: string): string {
|
|
146
|
+
return selectedKey && choices.length >= 2 && choices.some((c) => c.key === selectedKey) ? selectedKey : "";
|
|
147
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// One bookable session of a CLASS or COURSE service, and the member's place in it — shared by the
|
|
2
|
+
// Agenda (one row per session) and the service screen opened from an Agenda row, so the two show the
|
|
3
|
+
// same state and offer the same control for the same session.
|
|
4
|
+
import { addDays, dayKey } from "@himalaya/time";
|
|
5
|
+
import { listWaitlists, queryBookings, type WixAvailabilityEntry, type WixBooking, type WixService } from "@himalaya/wix-bookings";
|
|
6
|
+
import { copyParam, copyTemplate } from "./himi-copy.js";
|
|
7
|
+
import { slotStartInstant } from "./himi-bookings.js";
|
|
8
|
+
import { formatServicePrice } from "./wix-service-price.js";
|
|
9
|
+
import { assertMemberAccess, memberAccess, type MemberAccess } from "./member-access.js";
|
|
10
|
+
import { locationChoice } from "./booking-location.js";
|
|
11
|
+
|
|
12
|
+
/** A booking in one of these no longer holds a place. */
|
|
13
|
+
const RELEASED = new Set(["CANCELED", "CANCELLED", "DECLINED"]);
|
|
14
|
+
|
|
15
|
+
export type Place = "booked" | "waitlisted" | "";
|
|
16
|
+
|
|
17
|
+
/** One session as the screen needs it. `key` is the row's identity: the Wix session id when the
|
|
18
|
+
* entry has one — two sessions of a class at the same time are two rows — else service + start. */
|
|
19
|
+
export interface Session {
|
|
20
|
+
key: string;
|
|
21
|
+
/** The Wix session id — what a waitlist registers against. "" when the entry has none. */
|
|
22
|
+
sessionId: string;
|
|
23
|
+
serviceId: string;
|
|
24
|
+
/** A class session is booked on its own; a course session belongs to a course booked whole. */
|
|
25
|
+
kind: "class" | "course";
|
|
26
|
+
title: string;
|
|
27
|
+
/** The service's fixed price ("₪60"), "" when it has none to show (varied, custom, free). */
|
|
28
|
+
price: string;
|
|
29
|
+
start: string; // ISO instant with offset
|
|
30
|
+
end: string;
|
|
31
|
+
day: string; // LOCAL day key
|
|
32
|
+
staff: string;
|
|
33
|
+
location: string;
|
|
34
|
+
/** The location as a filter choice (`booking-location.ts`): key "" when it has no name or address. */
|
|
35
|
+
locationKey: string;
|
|
36
|
+
locationSub: string;
|
|
37
|
+
total: number;
|
|
38
|
+
open: number;
|
|
39
|
+
bookable: boolean;
|
|
40
|
+
/** The session's waitlist: its size (0 = it has none) and the places left on it. */
|
|
41
|
+
waitlistTotal: number;
|
|
42
|
+
/** A booking policy closes it (too early, too late, online booking off): no Book, no Waitlist. */
|
|
43
|
+
policyClosed: boolean;
|
|
44
|
+
/** The ONLY policy reason is that booking has not opened yet ("Not open yet", not "Closed"). */
|
|
45
|
+
tooEarly: boolean;
|
|
46
|
+
waitlistOpen: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MemberPlace { bookingId: string; revision: string; place: Place; registrationId?: string; confirmed?: boolean }
|
|
50
|
+
|
|
51
|
+
const count = (v: unknown): number => {
|
|
52
|
+
const n = Number(v ?? 0);
|
|
53
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** A slot's instant as an ISO string WITH an offset — iOS parses wire dates strictly. */
|
|
57
|
+
function instantIso(slot: { startDate?: string; timezone?: unknown }, field: "startDate" | "endDate"): string {
|
|
58
|
+
const ms = slotStartInstant({ startDate: (slot as Record<string, unknown>)[field] as string | undefined, timezone: slot.timezone });
|
|
59
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const sessionKey = (serviceId: string, startIso: string): string => `${serviceId}|${Date.parse(startIso)}`;
|
|
63
|
+
|
|
64
|
+
/** A raw availability entry → a Session, or null when it has no usable start. */
|
|
65
|
+
export function sessionOf(entry: WixAvailabilityEntry, service: WixService): Session | null {
|
|
66
|
+
const slot = (entry.slot ?? {}) as Record<string, unknown> & { startDate?: string; endDate?: string; timezone?: unknown };
|
|
67
|
+
const start = instantIso(slot, "startDate");
|
|
68
|
+
if (!start) return null;
|
|
69
|
+
const end = instantIso(slot, "endDate") || start;
|
|
70
|
+
const raw = entry as unknown as {
|
|
71
|
+
totalSpots?: number; openSpots?: number; waitingList?: { totalSpots?: number; openSpots?: number };
|
|
72
|
+
bookingPolicyViolations?: { tooEarlyToBook?: boolean; tooLateToBook?: boolean; bookOnlineDisabled?: boolean; validSubmissionRequired?: boolean };
|
|
73
|
+
};
|
|
74
|
+
const policy = raw.bookingPolicyViolations;
|
|
75
|
+
const tooLate = policy?.tooLateToBook === true, offline = policy?.bookOnlineDisabled === true;
|
|
76
|
+
const tooEarly = policy?.tooEarlyToBook === true;
|
|
77
|
+
// A session that also needs a form submitted is not just "not open yet": it never opens for the app.
|
|
78
|
+
const needsForm = policy?.validSubmissionRequired === true;
|
|
79
|
+
const resource = slot.resource as { name?: string } | undefined;
|
|
80
|
+
const location = slot.location as { id?: string; name?: string; formattedAddress?: string } | undefined;
|
|
81
|
+
const choice = locationChoice(location);
|
|
82
|
+
const serviceId = String(slot.serviceId ?? service.id ?? "");
|
|
83
|
+
const sessionId = typeof slot.sessionId === "string" ? slot.sessionId : "";
|
|
84
|
+
return {
|
|
85
|
+
key: sessionId || sessionKey(serviceId, start),
|
|
86
|
+
sessionId,
|
|
87
|
+
serviceId,
|
|
88
|
+
kind: ((service as WixService & { type?: string }).type ?? "").toUpperCase() === "COURSE" ? "course" : "class",
|
|
89
|
+
title: service.name ?? "",
|
|
90
|
+
price: formatServicePrice(service),
|
|
91
|
+
start, end,
|
|
92
|
+
day: dayKey(start),
|
|
93
|
+
staff: resource?.name?.trim() ?? "",
|
|
94
|
+
location: (location?.name || location?.formattedAddress || "").trim(),
|
|
95
|
+
locationKey: choice.key,
|
|
96
|
+
locationSub: choice.sub,
|
|
97
|
+
total: count(raw.totalSpots),
|
|
98
|
+
open: count(raw.openSpots),
|
|
99
|
+
bookable: entry.bookable !== false,
|
|
100
|
+
waitlistTotal: count(raw.waitingList?.totalSpots),
|
|
101
|
+
waitlistOpen: count(raw.waitingList?.openSpots),
|
|
102
|
+
policyClosed: tooEarly || tooLate || offline || needsForm,
|
|
103
|
+
tooEarly: tooEarly && !tooLate && !offline && !needsForm,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The member's bookings in the window → their place, keyed by the booking's session id when it
|
|
108
|
+
* names one, else by service + start. `bySession` turns that into each row's own key. */
|
|
109
|
+
export function placesOf(bookings: WixBooking[], email: string): Map<string, MemberPlace> {
|
|
110
|
+
const out = new Map<string, MemberPlace>();
|
|
111
|
+
for (const b of bookings) {
|
|
112
|
+
if (!b.id || b.contactDetails?.email?.trim().toLowerCase() !== email) continue;
|
|
113
|
+
const status = (b.status || "").toUpperCase();
|
|
114
|
+
if (RELEASED.has(status)) continue;
|
|
115
|
+
const slot = b.bookedEntity?.slot;
|
|
116
|
+
const serviceId = slot?.serviceId ?? "";
|
|
117
|
+
const start = b.startDate || (slot ? instantIso(slot, "startDate") : "");
|
|
118
|
+
if (!serviceId || !start) continue;
|
|
119
|
+
// Its session when the booking names one, so a parallel session is not marked too.
|
|
120
|
+
const bookedSession = (slot as { sessionId?: unknown } | undefined)?.sessionId;
|
|
121
|
+
out.set(typeof bookedSession === "string" && bookedSession ? bookedSession : sessionKey(serviceId, start), {
|
|
122
|
+
bookingId: b.id,
|
|
123
|
+
revision: String((b as WixBooking & { revision?: string | number }).revision ?? ""),
|
|
124
|
+
place: status === "WAITING_LIST" ? "waitlisted" : "booked",
|
|
125
|
+
confirmed: status === "CONFIRMED",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Each session's place, under the session's own key: by its session id, else by service + start. */
|
|
132
|
+
export function bySession(found: Map<string, MemberPlace>, inWindow: Session[]): Map<string, MemberPlace> {
|
|
133
|
+
const out = new Map<string, MemberPlace>();
|
|
134
|
+
for (const x of inWindow) {
|
|
135
|
+
const place = (x.sessionId ? found.get(x.sessionId) : undefined) ?? found.get(sessionKey(x.serviceId, x.start));
|
|
136
|
+
if (place) out.set(x.key, place);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** "4 spots left", "1 spot left", or "Full". Empty when the site reports no capacity, and for a
|
|
142
|
+
* course: a session's open spots do not say whether the course has room. */
|
|
143
|
+
export function spotsLabel(s: Session): string {
|
|
144
|
+
if (s.kind === "course" || s.total <= 0) return "";
|
|
145
|
+
if (s.open <= 0) return copyParam("agenda.full", "Full");
|
|
146
|
+
return s.open === 1 ? copyParam("agenda.spotLeft", "1 spot left") : copyTemplate("agenda.spotsLeft", "{n} spots left", { n: s.open });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The one control a session offers the member, as the Agenda row shows it. */
|
|
150
|
+
export type SessionControl = "book" | "waitlist" | "course" | "booked" | "waitlisted" | "closed";
|
|
151
|
+
|
|
152
|
+
/** Which control `s` offers to a member holding `place`, and — for "closed" — why ("Full",
|
|
153
|
+
* "Not open yet", "Closed"). A course is booked whole on its service screen: "course". */
|
|
154
|
+
export function sessionControl(s: Session, place: MemberPlace | undefined): { control: SessionControl; closedLabel: string; full: boolean } {
|
|
155
|
+
const mine = place?.place ?? "";
|
|
156
|
+
const course = s.kind === "course";
|
|
157
|
+
const full = s.total > 0 && s.open <= 0;
|
|
158
|
+
const closedLabel = full ? copyParam("agenda.full", "Full")
|
|
159
|
+
: s.tooEarly ? copyParam("agenda.notOpenYet", "Not open yet") : copyParam("agenda.closed", "Closed");
|
|
160
|
+
if (mine === "booked") return { control: "booked", closedLabel, full };
|
|
161
|
+
if (mine === "waitlisted") return { control: "waitlisted", closedLabel, full };
|
|
162
|
+
if (course) return { control: "course", closedLabel, full };
|
|
163
|
+
if (s.bookable && !full && !s.policyClosed) return { control: "book", closedLabel, full };
|
|
164
|
+
// Availability is capacity AND policy: a full session the policy closes is not joinable either.
|
|
165
|
+
if (full && s.waitlistOpen > 0 && !!s.sessionId && !s.policyClosed) return { control: "waitlist", closedLabel, full };
|
|
166
|
+
return { control: "closed", closedLabel, full };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Mark the places that are WAITLIST registrations, with the registration id leaving needs.
|
|
170
|
+
* A registration's booking is an ordinary pending booking, so the booking alone cannot say
|
|
171
|
+
* "on the waitlist" — only the session's waitlist can, and only sessions with one are asked. */
|
|
172
|
+
export function markWaitlisted(found: Map<string, MemberPlace>, sessionsByKey: Map<string, Session>, lists: { waitingResource?: string; registrations?: { id?: string; bookingId?: string }[] }[]): void {
|
|
173
|
+
const byBooking = new Map<string, string>();
|
|
174
|
+
for (const list of lists) for (const r of list.registrations ?? []) if (r.bookingId && r.id) byBooking.set(r.bookingId, r.id);
|
|
175
|
+
for (const [key, place] of found) {
|
|
176
|
+
const registrationId = byBooking.get(place.bookingId);
|
|
177
|
+
if (registrationId && sessionsByKey.has(key)) found.set(key, { ...place, place: "waitlisted", registrationId });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The signed-in member's bookings in the window, with the member they were read for: the two
|
|
182
|
+
* are committed together, so a write checks against the member its place belongs to. A visitor
|
|
183
|
+
* has none; a read that fails fails the load. */
|
|
184
|
+
export async function placesIn(start: string, end: string, inWindow: Session[], attempt = 0): Promise<{ who: MemberAccess | undefined; found: Map<string, MemberPlace> }> {
|
|
185
|
+
const who = await memberAccess().catch(() => undefined);
|
|
186
|
+
if (!who?.email) return { who, found: new Map() };
|
|
187
|
+
try {
|
|
188
|
+
const from = new Date(`${start}T00:00:00`).toISOString();
|
|
189
|
+
const to = new Date(`${dayKey(addDays(end, 1))}T00:00:00`).toISOString();
|
|
190
|
+
// Every page, not the first 100: a booking past it would read Book again. Bounded.
|
|
191
|
+
const bookings: WixBooking[] = [];
|
|
192
|
+
for (let page = 0; page < 5; page++) {
|
|
193
|
+
const res = await queryBookings({ query: { filter: { "contactDetails.email": { $eq: who.email }, startDate: { $gte: from, $lt: to } }, paging: { limit: 100, offset: page * 100 } } } as never);
|
|
194
|
+
const batch = res.bookings ?? [];
|
|
195
|
+
bookings.push(...batch);
|
|
196
|
+
if (batch.length < 100) break;
|
|
197
|
+
}
|
|
198
|
+
// The same member still? A sign-out or a switch during the read must not paint one member's
|
|
199
|
+
// bookings for another (the shared member-bookings read checks the same way).
|
|
200
|
+
await assertMemberAccess(who);
|
|
201
|
+
const found = bySession(placesOf(bookings, who.email), inWindow);
|
|
202
|
+
const byKey = new Map(inWindow.map((x) => [x.key, x] as const));
|
|
203
|
+
const waitlisted = [...found.keys()].map((k) => byKey.get(k)).filter((x): x is Session => !!x && x.waitlistTotal > 0 && !!x.sessionId);
|
|
204
|
+
if (waitlisted.length) {
|
|
205
|
+
// Only the session's waitlist can tell a waitlist place from a pending booking. If it cannot
|
|
206
|
+
// be read, that place is neither Booked nor free: the load fails (error state, Retry) rather
|
|
207
|
+
// than offer Book or Waitlist on a session the member already holds.
|
|
208
|
+
markWaitlisted(found, byKey, (await listWaitlists(waitlisted.map((x) => x.sessionId))).list ?? []);
|
|
209
|
+
// The lookup awaited too: still the same member?
|
|
210
|
+
await assertMemberAccess(who);
|
|
211
|
+
}
|
|
212
|
+
return { who, found };
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// A failed read is not "no bookings": showing Book on a session the member holds invites a
|
|
215
|
+
// duplicate. It fails the load (the error state, with Retry), as My Bookings does.
|
|
216
|
+
if ((err as { name?: string } | null)?.name !== "MemberAccessChanged") throw err;
|
|
217
|
+
// A sign-out or a switch during the read: read again, for whoever is signed in now — never
|
|
218
|
+
// commit them with places nobody read for them. A second switch fails the load.
|
|
219
|
+
if (attempt < 1) return placesIn(start, end, inWindow, attempt + 1);
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
|
|
|
97
97
|
himi_gift_card: ["giftCards"],
|
|
98
98
|
himi_gift_cards: ["giftCards"],
|
|
99
99
|
himi_groups: ["groups"],
|
|
100
|
+
himi_group_about: ["groups"],
|
|
100
101
|
himi_invoice: ["invoices"],
|
|
101
102
|
himi_invoices: ["invoices"],
|
|
102
103
|
himi_loyalty: ["loyalty"],
|
|
@@ -107,7 +108,6 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
|
|
|
107
108
|
himi_my_events: ["events"],
|
|
108
109
|
himi_my_subscriptions: ["plans"],
|
|
109
110
|
himi_new_message: ["inbox", "members"],
|
|
110
|
-
himi_notif_settings: ["notifsettings"],
|
|
111
111
|
himi_notifications: ["notifications"],
|
|
112
112
|
himi_orders: ["store"],
|
|
113
113
|
himi_photo_album: ["galleries"],
|
|
@@ -19,6 +19,8 @@ export interface CollectionConfig {
|
|
|
19
19
|
/** Numeric 0–5 row field rendered as a Rating (accessible "N out of 5") instead of
|
|
20
20
|
* folding a "★★★★☆" glyph string into the metadata line, so screen readers get a value. */
|
|
21
21
|
rating?: string;
|
|
22
|
+
/** Domain-provided calendar text and its unavailable flag, kept separate from media. */
|
|
23
|
+
dateBadge?: { text: string; hidden: string };
|
|
22
24
|
sectioned?: boolean;
|
|
23
25
|
view: 'list' | 'grid';
|
|
24
26
|
backendQuery?: boolean;
|
|
@@ -38,7 +40,13 @@ export interface CollectionConfig {
|
|
|
38
40
|
/** The card's primary verb, shown as a visible button beside the overflow.
|
|
39
41
|
* `screenId` opens that screen with the row id; without it the verb reuses the
|
|
40
42
|
* row's own tap action (the detail screen that owns the verb). */
|
|
41
|
-
primaryVerb?: { label: string; screenId?: string
|
|
43
|
+
primaryVerb?: { label: string; screenId?: string;
|
|
44
|
+
/** A row field holding THIS row's verb label (Register, RSVP, Buy Tickets…); `label` stays the fallback copy. */
|
|
45
|
+
labelPath?: string;
|
|
46
|
+
/** A row boolean that hides the verb on rows that offer no action (an event with no registration). */
|
|
47
|
+
hiddenPath?: string };
|
|
48
|
+
/** The worker action that shares a row (`tier5.<service>.<name>` with `{ id }`): adds Share to the card's overflow menu. */
|
|
49
|
+
shareAction?: string;
|
|
42
50
|
/** A TIME-BOUND entity (an event, a booking, a reservation): the boolean row field the
|
|
43
51
|
* DOMAIN sets once the record has finished — computed by the same rule as the row's own
|
|
44
52
|
* "Past" label, in the same place, so the two cannot disagree. A finished
|
|
@@ -73,7 +81,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
|
|
|
73
81
|
himiBookingBookings: { rows: 'rows', title: 'title', subtitle: 'subtitle', metadata: ['status'], view: 'list', backendQuery: true,
|
|
74
82
|
defaultFilter: 'value:Upcoming', searchPlaceholder: 'Search by booking ID', countNoun: ['booking', 'bookings'], filterValues: ['Upcoming', 'Past', 'Cancelled'],
|
|
75
83
|
sortLabels: { default: 'Soonest', az: 'Oldest', za: 'Latest' } },
|
|
76
|
-
himiEvents: { rows: 'results', title: 'title', subtitle: 'dateLine', image: 'image', view: 'grid', backendQuery: true, defaultFilter: 'value:Upcoming', searchPlaceholder: 'Search events', countNoun: ['event', 'events'], filterValues: ['Upcoming', 'Past'], sortLabels: { default: 'Date', az: 'Title A–Z', za: 'Title Z–A' }, primaryVerb: { label: 'Register' }, endedWhen: 'isPast' },
|
|
84
|
+
himiEvents: { rows: 'results', title: 'title', subtitle: 'dateLine', image: 'image', dateBadge: { text: 'dateBadge', hidden: 'noDateBadge' }, gridColumns: 1, view: 'grid', backendQuery: true, defaultFilter: 'value:Upcoming', searchPlaceholder: 'Search events', countNoun: ['event', 'events'], filterValues: ['Upcoming', 'Past'], sortLabels: { default: 'Date', az: 'Title A–Z', za: 'Title Z–A' }, primaryVerb: { label: 'Register', labelPath: 'actionLabel', hiddenPath: 'noAction' }, shareAction: 'share', endedWhen: 'isPast' },
|
|
77
85
|
himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid', narrowedBy: 'selectedCategoryId' },
|
|
78
86
|
himiOwnerLiteProducts: { rows: 'products', title: 'name', subtitle: 'price', image: 'image', view: 'grid', backendQuery: true, countNoun: ['product', 'products'] },
|
|
79
87
|
himiOwnerModules: { instanceInput: 'module', rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', filter: 'statusLabel', view: 'list', paged: true, reviewAction: 'prepareRecord', reviewSelectedAction: 'prepareSelected' },
|
|
@@ -208,7 +216,15 @@ export function projectCollection(source: CollectionRow[], config: CollectionCon
|
|
|
208
216
|
...(config.endedWhen ? (() => {
|
|
209
217
|
const ended = row[config.endedWhen] === true || (config.endedWhen === 'isPast' && row.statusLabel === 'Past');
|
|
210
218
|
return { collectionEnded: ended, collectionNotEnded: !ended };
|
|
211
|
-
})() : {})
|
|
219
|
+
})() : {}),
|
|
220
|
+
// A row restored from state saved by an older build has no badge flag. An unset hidden flag
|
|
221
|
+
// fails open and would paint an empty chip, so it reads as "no badge" until a refresh.
|
|
222
|
+
...(config.dateBadge && typeof row[config.dateBadge.hidden] !== 'boolean'
|
|
223
|
+
? { [config.dateBadge.text]: '', [config.dateBadge.hidden]: true } : {}),
|
|
224
|
+
// Same for a per-row verb: an unset hidden flag fails open and would show a verb with an unresolved
|
|
225
|
+
// label, so a row without the flag offers no action until a refresh establishes one.
|
|
226
|
+
...(config.primaryVerb?.hiddenPath && typeof row[config.primaryVerb.hiddenPath] !== 'boolean'
|
|
227
|
+
? { ...(config.primaryVerb.labelPath ? { [config.primaryVerb.labelPath]: '' } : {}), [config.primaryVerb.hiddenPath]: true } : {}) })),
|
|
212
228
|
filters, loaded: rows.length,
|
|
213
229
|
};
|
|
214
230
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Pure Events registration helpers shared by the event detail and the event list: which action an
|
|
2
|
+
// event offers, its label, and the URLs behind it. One copy, so a card and the detail screen never
|
|
3
|
+
// disagree about whether an event can be registered for.
|
|
4
|
+
import type { WixEvent } from "@himalaya/wix-events";
|
|
5
|
+
import { formatWeekdayDateTime } from "./engine-intl.js";
|
|
6
|
+
import { copyParam } from "./himi-copy.js";
|
|
7
|
+
|
|
8
|
+
function detailCopy(key: string, fallback: string): string { return copyParam(key, fallback); }
|
|
9
|
+
|
|
10
|
+
/** Registration CTA kind derived from the event's registration/eventType. */
|
|
11
|
+
export type RegistrationKind = "rsvp" | "tickets" | "external" | "none";
|
|
12
|
+
|
|
13
|
+
/** Map registration status and decoded eventType onto the CTA variant. The decoded
|
|
14
|
+
* `eventType` already folds registration.type / registration.initialType /
|
|
15
|
+
* the legacy flat `type` (decodeEvent, #478). Without a recognized type or
|
|
16
|
+
* status there is no registration action to offer. */
|
|
17
|
+
export function registrationKindOf(e: WixEvent): RegistrationKind {
|
|
18
|
+
const raw = e as unknown as { registration?: { type?: string; initialType?: string; status?: string } };
|
|
19
|
+
const value = (raw.registration?.type ?? e.eventType ?? raw.registration?.initialType ?? "").toUpperCase();
|
|
20
|
+
if (value === "NONE" || value.includes("NO_REGISTRATION")) return "none";
|
|
21
|
+
// Open statuses select the footer action, so they must also select the
|
|
22
|
+
// inventory read and action guards when type metadata is absent or stale.
|
|
23
|
+
switch (raw.registration?.status?.toUpperCase()) {
|
|
24
|
+
case "OPEN_TICKETS": return "tickets";
|
|
25
|
+
case "OPEN_EXTERNAL": return "external";
|
|
26
|
+
case "OPEN_RSVP":
|
|
27
|
+
case "OPEN_RSVP_WAITLIST_ONLY":
|
|
28
|
+
case "SCHEDULED_RSVP": return "rsvp";
|
|
29
|
+
}
|
|
30
|
+
if (value.includes("EXTERNAL")) return "external";
|
|
31
|
+
if (value.includes("TICKET")) return "tickets";
|
|
32
|
+
if (value.includes("RSVP")) return "rsvp";
|
|
33
|
+
return "none";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function eventStatus(e: WixEvent): string {
|
|
37
|
+
const raw = e as unknown as { status?: string };
|
|
38
|
+
return (raw.status ?? e.eventStatus ?? "").toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registrationUi(e: WixEvent, now: number): { state: string; label: string; action: string; caption: string; notice: string } {
|
|
42
|
+
const raw = e as unknown as { registration?: { status?: string; registrationPaused?: boolean; registrationDisabled?: boolean; allowedGuestTypes?: string; rsvp?: { startDate?: string }; tickets?: { soldOut?: boolean }; external?: { url?: string } }; form?: { messages?: { registrationClosed?: { message?: string; exploreEventsActionLabel?: string }; ticketsUnavailable?: { message?: string; exploreEventsActionLabel?: string } } } };
|
|
43
|
+
const r = raw.registration ?? {};
|
|
44
|
+
const status = eventStatus(e);
|
|
45
|
+
const type = registrationKindOf(e);
|
|
46
|
+
if (status === "CANCELED") return { state: "notice", label: "", action: "", caption: "", notice: detailCopy("event.canceledNotice", "This event was canceled.") };
|
|
47
|
+
if (status === "ENDED") return { state: "notice", label: "", action: "", caption: "", notice: detailCopy("event.endedNotice", "This event has ended.") };
|
|
48
|
+
if (type === "none" || r.registrationDisabled) return { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
49
|
+
const ownerClosed = raw.form?.messages?.registrationClosed?.message;
|
|
50
|
+
const ownerSold = raw.form?.messages?.ticketsUnavailable?.message;
|
|
51
|
+
if (["CLOSED_AUTOMATICALLY", "CLOSED_MANUALLY"].includes(r.status || "") || r.registrationPaused) return { state: "notice", label: "", action: "", caption: "", notice: ownerClosed || detailCopy("event.closedNotice", "Registration is closed.") };
|
|
52
|
+
if (type === "tickets" && r.tickets?.soldOut) return { state: "notice", label: "", action: "", caption: "", notice: ownerSold || detailCopy("event.soldOutNotice", "Tickets are sold out.") };
|
|
53
|
+
if (r.status === "SCHEDULED_RSVP") return { state: "scheduled", label: detailCopy("event.rsvpCta", "RSVP"), action: "", caption: detailCopy("event.opensCaption", "RSVP opens {date}.").replace("{date}", r.rsvp?.startDate ? formatWeekdayDateTime(new Date(r.rsvp.startDate)) : ""), notice: "" };
|
|
54
|
+
if (r.status === "OPEN_RSVP_WAITLIST_ONLY") return { state: "waitlist", label: detailCopy("event.waitlistCta", "Join Waitlist"), action: "himi.open", caption: detailCopy("event.fullCaption", "This event is full."), notice: "" };
|
|
55
|
+
if (r.status === "OPEN_TICKETS" || type === "tickets") return { state: "tickets", label: detailCopy("event.ticketsCta", "Buy Tickets"), action: "openTickets", caption: "", notice: "" };
|
|
56
|
+
if (r.status === "OPEN_EXTERNAL" || type === "external") return registrationUrlOf(e)
|
|
57
|
+
? { state: "external", label: detailCopy("event.externalCta", "Register"), action: "openRegistration", caption: "", notice: "" }
|
|
58
|
+
: { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
59
|
+
if (type === "rsvp" && r.status === "OPEN_RSVP") return { state: "rsvp", label: detailCopy("event.rsvpCta", "RSVP"), action: "himi.open", caption: "", notice: "" };
|
|
60
|
+
if (r.status === "OPEN_RSVP" || (type === "rsvp" && !r.status)) return { state: "rsvp", label: detailCopy("event.rsvpCta", "RSVP"), action: "himi.open", caption: "", notice: "" };
|
|
61
|
+
void now;
|
|
62
|
+
return { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** External registration URL / event page URL from the raw payload ("" when absent). */
|
|
66
|
+
export function registrationUrlOf(e: WixEvent): string {
|
|
67
|
+
const raw = e as unknown as {
|
|
68
|
+
registration?: { external?: { registration?: string; url?: string } };
|
|
69
|
+
eventPageUrl?: { base?: string; path?: string } | string;
|
|
70
|
+
};
|
|
71
|
+
const external = raw.registration?.external?.registration ?? raw.registration?.external?.url;
|
|
72
|
+
if (typeof external === "string" && /^https?:\/\//i.test(external)) return external;
|
|
73
|
+
return eventPageUrlOf(e);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The event's public page URL when the payload carries one ("" otherwise). */
|
|
77
|
+
export function eventPageUrlOf(e: WixEvent): string {
|
|
78
|
+
const raw = e as unknown as { eventPageUrl?: { base?: string; path?: string } | string };
|
|
79
|
+
const page = raw.eventPageUrl;
|
|
80
|
+
if (typeof page === "string" && /^https?:\/\//i.test(page)) return page;
|
|
81
|
+
if (page && typeof page === "object" && typeof page.base === "string" && page.base) {
|
|
82
|
+
const path = typeof page.path === "string" ? page.path : "";
|
|
83
|
+
return `${page.base.replace(/\/$/, "")}${path && !path.startsWith("/") ? "/" : ""}${path}`;
|
|
84
|
+
}
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { descriptionBody } from "./rich-body.js";
|
|
1
2
|
// Shared restaurants-menu shaping for himi workers (branded-lite `restaurants`
|
|
2
3
|
// vertical): dish cards + menu categories derived from the Wix Restaurants
|
|
3
4
|
// Menus hierarchy (menu → sections → items). Used by BOTH the menus worker
|
|
@@ -166,7 +167,7 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId:
|
|
|
166
167
|
|
|
167
168
|
/** HTML-stripped, whitespace-collapsed description snippet (≤ 90 chars). */
|
|
168
169
|
export function descriptionSnippet(value: unknown, maxLength = 90): string {
|
|
169
|
-
const text = (
|
|
170
|
+
const text = descriptionBody(value).bodyText.replace(/\s+/g, " ").trim();
|
|
170
171
|
return text.length > maxLength ? `${text.slice(0, maxLength - 1).trimEnd()}…` : text;
|
|
171
172
|
}
|
|
172
173
|
|