@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
|
@@ -40,7 +40,8 @@ import {
|
|
|
40
40
|
} from "@himalaya/wix-online-programs";
|
|
41
41
|
import { createRedirectSession } from "@himalaya/wix-redirects";
|
|
42
42
|
import { getMyMember } from "@himalaya/wix-members";
|
|
43
|
-
import { session as wixPlatformSession } from "@himalaya/wix-platform";
|
|
43
|
+
import { session as wixPlatformSession, i18n } from "@himalaya/wix-platform";
|
|
44
|
+
import { programFacts, type ProgramFact } from "./facts.js";
|
|
44
45
|
import { cleanTitle, initials } from "../_shared/himi-text.js";
|
|
45
46
|
import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
|
|
46
47
|
import { isPaymentSetupFailure, payCashierOrder } from "@himalaya/checkout";
|
|
@@ -62,8 +63,10 @@ interface State {
|
|
|
62
63
|
description: string;
|
|
63
64
|
descriptionJson: string;
|
|
64
65
|
hasDescriptionJson: boolean;
|
|
65
|
-
/** "N sections · M steps"
|
|
66
|
+
/** "N sections · M steps" — structure meta line; pace has its own fact row. */
|
|
66
67
|
meta: string;
|
|
68
|
+
facts: ProgramFact[];
|
|
69
|
+
hideFacts: boolean;
|
|
67
70
|
/** Formatted price when the payload carries one ("" for free/absent). */
|
|
68
71
|
priceLine: string;
|
|
69
72
|
image: string;
|
|
@@ -105,8 +108,11 @@ function inputId(): string {
|
|
|
105
108
|
|
|
106
109
|
function derive(state: State): State {
|
|
107
110
|
const empty = state.status === "ready" && !state.name;
|
|
111
|
+
const facts = Array.isArray(state.facts) ? state.facts : [];
|
|
108
112
|
return {
|
|
109
113
|
...state,
|
|
114
|
+
facts,
|
|
115
|
+
hideFacts: facts.length === 0,
|
|
110
116
|
hasDescriptionJson: state.descriptionJson !== "",
|
|
111
117
|
isLoading: state.status === "loading",
|
|
112
118
|
notLoading: state.status !== "loading",
|
|
@@ -132,9 +138,7 @@ function metaOf(p: WixProgram): string {
|
|
|
132
138
|
const steps = Number(summary?.stepCount ?? 0);
|
|
133
139
|
if (sections) bits.push(`${sections} ${sections === 1 ? "section" : "sections"}`);
|
|
134
140
|
if (steps) bits.push(`${steps} ${steps === 1 ? "step" : "steps"}`);
|
|
135
|
-
|
|
136
|
-
const selfPaced = (p as { timeline?: { selfPaced?: boolean } }).timeline?.selfPaced;
|
|
137
|
-
return selfPaced ? copyParam("program.selfPacedLabel", "Self-paced") : "";
|
|
141
|
+
return bits.join(" · ");
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function priceOf(p: WixProgram): string {
|
|
@@ -149,10 +153,10 @@ function priceOf(p: WixProgram): string {
|
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
function ready(state: State, p: WixProgram | undefined): State {
|
|
156
|
+
function ready(state: State, p: WixProgram | undefined, locale: string): State {
|
|
153
157
|
const stateOf = ((p?.state ?? p?.status ?? "") as string).toUpperCase();
|
|
154
158
|
if (!p || stateOf === "DRAFT") {
|
|
155
|
-
return derive({ ...state, status: "ready", name: "", description: "", descriptionJson: "", meta: "", priceLine: "", image: "", initial: "", emptyText: copyParam("program.emptyText", "This program is no longer available.") });
|
|
159
|
+
return derive({ ...state, status: "ready", name: "", description: "", descriptionJson: "", meta: "", facts: [], priceLine: "", image: "", initial: "", emptyText: copyParam("program.emptyText", "This program is no longer available.") });
|
|
156
160
|
}
|
|
157
161
|
const name = titleOf(p);
|
|
158
162
|
const image = programImage(p, WIX_DETAIL_IMAGE);
|
|
@@ -164,6 +168,7 @@ function ready(state: State, p: WixProgram | undefined): State {
|
|
|
164
168
|
description: body.bodyText,
|
|
165
169
|
descriptionJson: body.bodyJson,
|
|
166
170
|
meta: metaOf(p),
|
|
171
|
+
facts: programFacts(p, locale),
|
|
167
172
|
priceLine: priceOf(p),
|
|
168
173
|
image,
|
|
169
174
|
initial: initials(name),
|
|
@@ -274,7 +279,7 @@ const pricing = commerce.pricing;
|
|
|
274
279
|
const PRICING_NOT_HYDRATED = [
|
|
275
280
|
"priceLine", "priceNote", "hidePriceRetry", "enrollDisabled", "enrollNote", "enrollHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote",
|
|
276
281
|
] as const;
|
|
277
|
-
const CONTENT_KEYS = ["status", "name", "description", "descriptionJson", "meta", "image", "initial", "emptyText"] as const;
|
|
282
|
+
const CONTENT_KEYS = ["status", "name", "description", "descriptionJson", "meta", "facts", "image", "initial", "emptyText"] as const;
|
|
278
283
|
type Content = Pick<State, (typeof CONTENT_KEYS)[number]>;
|
|
279
284
|
const NO_PRICE: IapPriceFields = { priceText: "", priceNote: "", priceLoading: false, hidePriceRetry: true, actionDisabled: true, actionNote: "" };
|
|
280
285
|
const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
|
|
@@ -307,9 +312,12 @@ async function load(state: State, arg?: unknown): Promise<undefined> {
|
|
|
307
312
|
if (presentation === "sheet" || presentation === "full_screen") signInPresentation = presentation;
|
|
308
313
|
const id = inputId();
|
|
309
314
|
const seq = ++loadSeq;
|
|
310
|
-
const found = await
|
|
311
|
-
|
|
312
|
-
|
|
315
|
+
const [found, locale] = await Promise.all([
|
|
316
|
+
fetchProgram(id).then((program) => ({ program }), () => undefined),
|
|
317
|
+
i18n.locale().catch(() => "en"),
|
|
318
|
+
]);
|
|
319
|
+
if (seq !== loadSeq || inputId() !== id) return undefined;
|
|
320
|
+
content = found ? contentOf(ready(state, found.program, locale)) : { ...content, status: "error" };
|
|
313
321
|
pricedItem = found?.program ? onlineProgramItem({ id: inputId() }, priceOf(found.program)) : pricedItem;
|
|
314
322
|
void svc.pricingPublish();
|
|
315
323
|
const prices = found?.program ? priceProgram() : Promise.resolve();
|
|
@@ -467,6 +475,8 @@ const INITIAL_STATE: State = derive({
|
|
|
467
475
|
descriptionJson: "",
|
|
468
476
|
hasDescriptionJson: false,
|
|
469
477
|
meta: "",
|
|
478
|
+
facts: [],
|
|
479
|
+
hideFacts: true,
|
|
470
480
|
priceLine: "",
|
|
471
481
|
image: "",
|
|
472
482
|
initial: "",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { descriptionBody } from "../_shared/rich-body.js";
|
|
1
2
|
import { defineService } from "@himalaya/state";
|
|
2
3
|
import { LOADING, loaded, loadFailed, needsSignIn, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
3
4
|
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
@@ -54,7 +55,7 @@ const svc = defineService<S, { reset: (s: S) => S; load: (s: S, arg?: unknown) =
|
|
|
54
55
|
if (!channel || !video || video.status !== "VISIBLE") next = unavailable(next, "unavailable", copyParam("video.unavailableText", "This video is unavailable."));
|
|
55
56
|
else {
|
|
56
57
|
next.title = video.title ?? "";
|
|
57
|
-
next.description = video.description
|
|
58
|
+
next.description = descriptionBody(video.description).bodyText;
|
|
58
59
|
if (video.content?.status !== "READY") next = isVideoProcessing(video) ?
|
|
59
60
|
unavailable(next, "processing", copyParam("video.processingText", "Video is processing. Try again later.")) :
|
|
60
61
|
unavailable(next, "unavailable", copyParam("video.unavailableText", "This video is unavailable."));
|
|
@@ -35,8 +35,10 @@ import {
|
|
|
35
35
|
type EventRow,
|
|
36
36
|
} from "../_shared/himi-events";
|
|
37
37
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
38
|
+
import { eventPageUrlOf, eventStatus, registrationKindOf, registrationUi, registrationUrlOf, type RegistrationKind } from "../_shared/event-registration.js";
|
|
39
|
+
export { eventPageUrlOf, registrationKindOf, registrationUrlOf, type RegistrationKind };
|
|
38
40
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
39
|
-
import {
|
|
41
|
+
import { descriptionBody } from "../_shared/rich-body.js";
|
|
40
42
|
import { mayUseRegularTicketCheckout } from "../_shared/iap-attendance.js";
|
|
41
43
|
import { ATTENDANCE_INITIAL, ATTENDANCE_NOT_HYDRATED, createAttendancePurchase, type AttendancePurchaseFields } from "../_shared/iap-attendance-purchase.js";
|
|
42
44
|
import { resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
@@ -88,9 +90,6 @@ interface TicketTierRow {
|
|
|
88
90
|
notAvailable?: boolean;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
/** Registration CTA kind derived from the event's registration/eventType. */
|
|
92
|
-
export type RegistrationKind = "rsvp" | "tickets" | "external" | "none";
|
|
93
|
-
|
|
94
93
|
// Offline-first (data-offline): the event detail is a read-through single-item read —
|
|
95
94
|
// it shares the `events` entity written by the list, and also write-throughs its own
|
|
96
95
|
// fetched row so a deep-linked detail is cached for the next offline open. On a failed
|
|
@@ -302,7 +301,7 @@ function loadedTiers(defs: WixAvailableTicket[]): { rows: TicketTierRow[]; ticke
|
|
|
302
301
|
const ticket = tickets.get(row.ticketDefinitionId ?? row.id);
|
|
303
302
|
if (!ticket) return { ...row, quantity: 0, description: "", feeNote: "", policy: "", noPolicy: true, notAvailable: !row.available, notGuestPrice: !row.isGuestPrice, maxQuantity: 0 };
|
|
304
303
|
const policy = (ticket as unknown as { policy?: string; policyText?: string }).policyText || (ticket as unknown as { policy?: string }).policy || "";
|
|
305
|
-
return { ...row, quantity: 0, description: ticket.description
|
|
304
|
+
return { ...row, quantity: 0, description: descriptionBody(ticket.description).bodyText, feeNote: ((ticket as unknown as { wixFeeConfig?: { type?: string }; feeType?: string }).wixFeeConfig?.type ?? (ticket as unknown as { feeType?: string }).feeType) === "FEE_ADDED_AT_CHECKOUT" ? detailCopy("event.serviceFee", "+ ticket service fee") : "", policy, noPolicy: !policy, notAvailable: !row.available, notGuestPrice: !row.isGuestPrice, priceAmount: row.priceAmount ?? Number(ticket.price?.amount ?? 0), rowLimit: row.available ? ticket.limitPerCheckout ?? 99 : 0, maxQuantity: row.available ? ticket.limitPerCheckout ?? 99 : 0 };
|
|
306
305
|
});
|
|
307
306
|
return { rows, tickets };
|
|
308
307
|
}
|
|
@@ -451,29 +450,6 @@ function calendarFallbackUrl(s: S, end: number): string {
|
|
|
451
450
|
* shortDescription: REGISTRATION fills eventType, which skips the query enrichment that used to supply them. */
|
|
452
451
|
export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS", "FORM", "AGENDA"];
|
|
453
452
|
|
|
454
|
-
/** Map registration status and decoded eventType onto the CTA variant. The decoded
|
|
455
|
-
* `eventType` already folds registration.type / registration.initialType /
|
|
456
|
-
* the legacy flat `type` (decodeEvent, #478). Without a recognized type or
|
|
457
|
-
* status there is no registration action to offer. */
|
|
458
|
-
export function registrationKindOf(e: WixEvent): RegistrationKind {
|
|
459
|
-
const raw = e as unknown as { registration?: { type?: string; initialType?: string; status?: string } };
|
|
460
|
-
const value = (raw.registration?.type ?? e.eventType ?? raw.registration?.initialType ?? "").toUpperCase();
|
|
461
|
-
if (value === "NONE" || value.includes("NO_REGISTRATION")) return "none";
|
|
462
|
-
// Open statuses select the footer action, so they must also select the
|
|
463
|
-
// inventory read and action guards when type metadata is absent or stale.
|
|
464
|
-
switch (raw.registration?.status?.toUpperCase()) {
|
|
465
|
-
case "OPEN_TICKETS": return "tickets";
|
|
466
|
-
case "OPEN_EXTERNAL": return "external";
|
|
467
|
-
case "OPEN_RSVP":
|
|
468
|
-
case "OPEN_RSVP_WAITLIST_ONLY":
|
|
469
|
-
case "SCHEDULED_RSVP": return "rsvp";
|
|
470
|
-
}
|
|
471
|
-
if (value.includes("EXTERNAL")) return "external";
|
|
472
|
-
if (value.includes("TICKET")) return "tickets";
|
|
473
|
-
if (value.includes("RSVP")) return "rsvp";
|
|
474
|
-
return "none";
|
|
475
|
-
}
|
|
476
|
-
|
|
477
453
|
function detailCopy(key: string, fallback: string): string { return copyParam(key, fallback); }
|
|
478
454
|
|
|
479
455
|
function detailDate(e: WixEvent): { line: string; zone: string; start: number; end: number } {
|
|
@@ -536,69 +512,17 @@ function detailPlace(e: WixEvent): { value: string; mapUrl: string; hideMapTap:
|
|
|
536
512
|
return { value, mapUrl: mapped, hideMapTap: !mapped, icon: "location" };
|
|
537
513
|
}
|
|
538
514
|
|
|
539
|
-
function eventStatus(e: WixEvent): string {
|
|
540
|
-
const raw = e as unknown as { status?: string };
|
|
541
|
-
return (raw.status ?? e.eventStatus ?? "").toUpperCase();
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function registrationUi(e: WixEvent, now: number): { state: string; label: string; action: string; caption: string; notice: string } {
|
|
545
|
-
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 } } } };
|
|
546
|
-
const r = raw.registration ?? {};
|
|
547
|
-
const status = eventStatus(e);
|
|
548
|
-
const type = registrationKindOf(e);
|
|
549
|
-
if (status === "CANCELED") return { state: "notice", label: "", action: "", caption: "", notice: detailCopy("event.canceledNotice", "This event was canceled.") };
|
|
550
|
-
if (status === "ENDED") return { state: "notice", label: "", action: "", caption: "", notice: detailCopy("event.endedNotice", "This event has ended.") };
|
|
551
|
-
if (type === "none" || r.registrationDisabled) return { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
552
|
-
const ownerClosed = raw.form?.messages?.registrationClosed?.message;
|
|
553
|
-
const ownerSold = raw.form?.messages?.ticketsUnavailable?.message;
|
|
554
|
-
if (["CLOSED_AUTOMATICALLY", "CLOSED_MANUALLY"].includes(r.status || "") || r.registrationPaused) return { state: "notice", label: "", action: "", caption: "", notice: ownerClosed || detailCopy("event.closedNotice", "Registration is closed.") };
|
|
555
|
-
if (type === "tickets" && r.tickets?.soldOut) return { state: "notice", label: "", action: "", caption: "", notice: ownerSold || detailCopy("event.soldOutNotice", "Tickets are sold out.") };
|
|
556
|
-
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: "" };
|
|
557
|
-
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: "" };
|
|
558
|
-
if (r.status === "OPEN_TICKETS" || type === "tickets") return { state: "tickets", label: detailCopy("event.ticketsCta", "Buy Tickets"), action: "openTickets", caption: "", notice: "" };
|
|
559
|
-
if (r.status === "OPEN_EXTERNAL" || type === "external") return registrationUrlOf(e)
|
|
560
|
-
? { state: "external", label: detailCopy("event.externalCta", "Register"), action: "openRegistration", caption: "", notice: "" }
|
|
561
|
-
: { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
562
|
-
if (type === "rsvp" && r.status === "OPEN_RSVP") return { state: "rsvp", label: detailCopy("event.rsvpCta", "RSVP"), action: "himi.open", caption: "", notice: "" };
|
|
563
|
-
if (r.status === "OPEN_RSVP" || (type === "rsvp" && !r.status)) return { state: "rsvp", label: detailCopy("event.rsvpCta", "RSVP"), action: "himi.open", caption: "", notice: "" };
|
|
564
|
-
void now;
|
|
565
|
-
return { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/** External registration URL / event page URL from the raw payload ("" when absent). */
|
|
569
|
-
export function registrationUrlOf(e: WixEvent): string {
|
|
570
|
-
const raw = e as unknown as {
|
|
571
|
-
registration?: { external?: { registration?: string; url?: string } };
|
|
572
|
-
eventPageUrl?: { base?: string; path?: string } | string;
|
|
573
|
-
};
|
|
574
|
-
const external = raw.registration?.external?.registration ?? raw.registration?.external?.url;
|
|
575
|
-
if (typeof external === "string" && /^https?:\/\//i.test(external)) return external;
|
|
576
|
-
return eventPageUrlOf(e);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
515
|
/** Event slug for createRedirectSession({ eventsCheckout }) — "" when absent. */
|
|
580
516
|
export function eventSlugOf(e: WixEvent): string {
|
|
581
517
|
const slug = e.slug ?? (e as unknown as { slug?: string }).slug;
|
|
582
518
|
return typeof slug === "string" ? slug.trim() : "";
|
|
583
519
|
}
|
|
584
520
|
|
|
585
|
-
/** The event's public page URL when the payload carries one ("" otherwise). */
|
|
586
|
-
export function eventPageUrlOf(e: WixEvent): string {
|
|
587
|
-
const raw = e as unknown as { eventPageUrl?: { base?: string; path?: string } | string };
|
|
588
|
-
const page = raw.eventPageUrl;
|
|
589
|
-
if (typeof page === "string" && /^https?:\/\//i.test(page)) return page;
|
|
590
|
-
if (page && typeof page === "object" && typeof page.base === "string" && page.base) {
|
|
591
|
-
const path = typeof page.path === "string" ? page.path : "";
|
|
592
|
-
return `${page.base.replace(/\/$/, "")}${path && !path.startsWith("/") ? "/" : ""}${path}`;
|
|
593
|
-
}
|
|
594
|
-
return "";
|
|
595
|
-
}
|
|
596
|
-
|
|
597
521
|
/** The description pair: the v3 Ricos `description` for RichContent, and its plain text. A string description
|
|
598
522
|
* (or none) keeps `eventDescription`'s legacy read, with no document. */
|
|
599
523
|
function eventBody(e: WixEvent): { description: string; descriptionJson: string } {
|
|
600
|
-
const legacy = eventDescription(e);
|
|
601
|
-
const { bodyJson, bodyText } =
|
|
524
|
+
const legacy = eventDescription({ ...e, description: undefined });
|
|
525
|
+
const { bodyJson, bodyText } = descriptionBody((e as unknown as { description?: unknown }).description, descriptionBody(legacy).bodyText);
|
|
602
526
|
return { description: bodyText, descriptionJson: bodyJson };
|
|
603
527
|
}
|
|
604
528
|
|
|
@@ -1132,6 +1056,8 @@ const svc: ReturnType<typeof defineService<S, DetailActions>> = defineService<S,
|
|
|
1132
1056
|
}).sort((a, b) => (eventStartMillis(a) ?? 0) - (eventStartMillis(b) ?? 0)).map((item) => ({ id: item.id ?? "", date: eventWhen(item) })).filter((item) => !!item.id);
|
|
1133
1057
|
} catch { moreDates = []; optionalReadFailed = true; }
|
|
1134
1058
|
}
|
|
1059
|
+
const body = eventBody(e);
|
|
1060
|
+
const shortDescription = descriptionBody(rawEvent.shortDescription).bodyText;
|
|
1135
1061
|
const readyState = derive({
|
|
1136
1062
|
...s,
|
|
1137
1063
|
status: "ready",
|
|
@@ -1153,9 +1079,9 @@ const svc: ReturnType<typeof defineService<S, DetailActions>> = defineService<S,
|
|
|
1153
1079
|
price,
|
|
1154
1080
|
zone: dates.zone,
|
|
1155
1081
|
mapUrl: place.mapUrl,
|
|
1156
|
-
shortDescription
|
|
1157
|
-
noShortDescription: !
|
|
1158
|
-
noDescription: !
|
|
1082
|
+
shortDescription,
|
|
1083
|
+
noShortDescription: !shortDescription || shortDescription === body.description,
|
|
1084
|
+
noDescription: !body.description && !body.descriptionJson,
|
|
1159
1085
|
noDate: !dates.line,
|
|
1160
1086
|
registrationState: registration.state,
|
|
1161
1087
|
footerLabel: registration.label,
|
|
@@ -1177,7 +1103,7 @@ const svc: ReturnType<typeof defineService<S, DetailActions>> = defineService<S,
|
|
|
1177
1103
|
noMoreDates: moreDates.length === 0,
|
|
1178
1104
|
// The Ricos description when the event has one (its own text is the plain copy); the legacy
|
|
1179
1105
|
// string fields otherwise.
|
|
1180
|
-
...
|
|
1106
|
+
...body,
|
|
1181
1107
|
image,
|
|
1182
1108
|
hasImage: !!image,
|
|
1183
1109
|
noImage: !image,
|
|
@@ -5,6 +5,8 @@ import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
|
5
5
|
import { normalizeEvent, type EventRow } from "../_shared/himi-events";
|
|
6
6
|
import { offsetPage } from "../_shared/offset-page.js";
|
|
7
7
|
import { copyParam } from "../_shared/himi-copy.js";
|
|
8
|
+
import { eventPageUrlOf, registrationUi } from "../_shared/event-registration.js";
|
|
9
|
+
import { share as shareSheet } from "@himalaya/share";
|
|
8
10
|
|
|
9
11
|
// Offline-first (data-offline): the public events list is a read-through catalog —
|
|
10
12
|
// paint the last-synced rows from the on-device store first (instant, works offline),
|
|
@@ -13,8 +15,37 @@ import { copyParam } from "../_shared/himi-copy.js";
|
|
|
13
15
|
// (+ declared the `events` entity in its dataSchema) just falls back to today's
|
|
14
16
|
// online-only path — the try/catch is the seam that keeps this shared bundle safe.
|
|
15
17
|
const ENTITY = "events";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
interface ListEventRow extends EventRow {
|
|
19
|
+
dateBadge: string; noDateBadge: boolean;
|
|
20
|
+
/** This card's verb (Register / RSVP / Buy Tickets / Join Waitlist) from the SAME rule the detail uses; hidden when none applies. */
|
|
21
|
+
actionLabel: string; noAction: boolean;
|
|
22
|
+
/** The event page URL (when the payload carries one) for Share. */
|
|
23
|
+
shareUrl: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The day monogram is a title initial for undated events. Only an actual calendar
|
|
27
|
+
// date becomes a badge; derive it for old cached rows too, without a cache migration.
|
|
28
|
+
// The month must be the three-letter abbreviation: rows cached by an older worker on an
|
|
29
|
+
// Intl-less engine can hold a whole "11/10/2030, 12:00:00 PM" there.
|
|
30
|
+
function withDateBadge(row: EventRow): ListEventRow {
|
|
31
|
+
const dated = typeof row.month === "string" && /^[A-Z]{3}$/.test(row.month)
|
|
32
|
+
&& /^(?:[1-9]|[12]\d|3[01])$/.test(row.day);
|
|
33
|
+
const known = row as Partial<ListEventRow>;
|
|
34
|
+
// Rows cached by an older build carry no registration data: they show no verb until a refresh, never a wrong one.
|
|
35
|
+
return { ...row, dateBadge: dated ? `${row.day}\n${row.month}` : "", noDateBadge: !dated,
|
|
36
|
+
actionLabel: known.actionLabel ?? "", noAction: known.noAction ?? true, shareUrl: known.shareUrl ?? "" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The card's action, from the event's own registration (REGISTRATION + URLS fieldsets). Cancelled, ended,
|
|
40
|
+
* closed, sold-out and registration-less events offer none, so a card never promises "Register" the detail refuses. */
|
|
41
|
+
function withRegistration(raw: WixEvent, row: ListEventRow): ListEventRow {
|
|
42
|
+
const ui = registrationUi(raw, Date.now());
|
|
43
|
+
const offers = ui.state !== "notice" && ui.state !== "none" && ui.state !== "scheduled" && !!ui.label;
|
|
44
|
+
return { ...row, actionLabel: offers ? ui.label : "", noAction: !offers, shareUrl: eventPageUrlOf(raw) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readCached(): Promise<ListEventRow[]> {
|
|
48
|
+
try { return ((await getAll(ENTITY)) as unknown as EventRow[]).map(withDateBadge); } catch { return []; }
|
|
18
49
|
}
|
|
19
50
|
async function writeCached(rows: EventRow[]): Promise<void> {
|
|
20
51
|
try { for (const r of rows) await upsert(ENTITY, r as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
|
|
@@ -22,12 +53,12 @@ async function writeCached(rows: EventRow[]): Promise<void> {
|
|
|
22
53
|
|
|
23
54
|
interface S {
|
|
24
55
|
status: "loading" | "ready";
|
|
25
|
-
query: string; all:
|
|
56
|
+
query: string; all: ListEventRow[]; results: ListEventRow[]; emptyText: string; countLabel: string;
|
|
26
57
|
errored: boolean; notErrored: boolean; isLoading: boolean; notLoading: boolean; noEmpty: boolean;
|
|
27
58
|
nextCursor?: string; complete?: boolean; total?: number; loadingMore?: boolean; pageError?: boolean; fromCache?: boolean;
|
|
28
59
|
}
|
|
29
60
|
let queryNow = new Date().toISOString();
|
|
30
|
-
const svc = defineBackendCollection<S,
|
|
61
|
+
const svc = defineBackendCollection<S, ListEventRow>("himiEvents", {
|
|
31
62
|
restorePagesOnLoad: true,
|
|
32
63
|
state: { status: "loading", query: "", all: [], results: [], emptyText: "", countLabel: "", errored: false, notErrored: true,
|
|
33
64
|
isLoading: true, notLoading: false, noEmpty: true },
|
|
@@ -62,10 +93,10 @@ const svc = defineBackendCollection<S, EventRow>("himiEvents", {
|
|
|
62
93
|
];
|
|
63
94
|
}
|
|
64
95
|
if (criteria.filter === "value:Past") filter["dateAndTimeSettings.startDate"] = { $lt: queryNow };
|
|
65
|
-
const res = await queryEvents({ limit: 50, offset: Number(cursor || 0), query: { filter,
|
|
96
|
+
const res = await queryEvents({ limit: 50, offset: Number(cursor || 0), fields: ["DETAILS", "REGISTRATION", "URLS"], query: { filter,
|
|
66
97
|
sort: [{ fieldName: criteria.sort === "default" ? "dateAndTimeSettings.startDate" : "title", order: criteria.sort === "za" ? "DESC" : "ASC" }, { fieldName: "id", order: "ASC" }] } });
|
|
67
98
|
const raw = res.events ?? [];
|
|
68
|
-
const items = raw.filter(row => !!row.id).map(normalizeEvent);
|
|
99
|
+
const items = raw.filter(row => !!row.id).map(row => withRegistration(row, withDateBadge(normalizeEvent(row))));
|
|
69
100
|
await writeCached(items);
|
|
70
101
|
const metadata = (res as { pagingMetadata?: { total?: number } }).pagingMetadata;
|
|
71
102
|
return { items, ...offsetPage(cursor, raw.length, 50, metadata), metadata: { fromCache: false } };
|
|
@@ -78,6 +109,16 @@ const svc = defineBackendCollection<S, EventRow>("himiEvents", {
|
|
|
78
109
|
fromCache: page.metadata?.fromCache === true, errored, notErrored: !errored, isLoading: loading, notLoading: !loading,
|
|
79
110
|
noEmpty: true, countLabel: "", emptyText: errored ? copyParam("events.errorText", "Events could not be loaded.") : "" };
|
|
80
111
|
},
|
|
112
|
+
actions: {
|
|
113
|
+
// Share a card's event: its title and date line, plus the event page when the payload carried one.
|
|
114
|
+
share: async (s, arg) => {
|
|
115
|
+
const id = typeof arg === "string" ? arg : String((arg as { id?: unknown } | undefined)?.id ?? "");
|
|
116
|
+
const row = s.results.find(item => item.id === id);
|
|
117
|
+
if (!row) return undefined;
|
|
118
|
+
try { await shareSheet({ subject: row.title, text: [row.title, row.dateLine].filter(Boolean).join(" — "), ...(row.shareUrl ? { url: row.shareUrl } : {}) }); } catch { /* the sheet is best-effort: nothing to undo */ }
|
|
119
|
+
return undefined;
|
|
120
|
+
},
|
|
121
|
+
},
|
|
81
122
|
});
|
|
82
123
|
svc.load();
|
|
83
124
|
export { svc };
|
|
@@ -20,6 +20,7 @@ import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
|
20
20
|
import { isProductSaved, toggleSavedProduct } from "../_shared/himi-store-saved.js";
|
|
21
21
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
22
22
|
import { descriptionBody } from "../_shared/rich-body.js";
|
|
23
|
+
import { PRODUCT_DETAIL_FIELDS, productInfoSections, type ProductInfoSection } from "../_shared/store-product-info.js";
|
|
23
24
|
|
|
24
25
|
/** Live cart size (sum of line-item quantities) for the top-bar cart badge.
|
|
25
26
|
* Best-effort: a cart error just yields 0 (badge hidden), never fails the screen. */
|
|
@@ -201,6 +202,7 @@ interface S {
|
|
|
201
202
|
wasPriceEmpty: boolean;
|
|
202
203
|
noSku: boolean;
|
|
203
204
|
skuLabel: string;
|
|
205
|
+
infoSections: ProductInfoSection[];
|
|
204
206
|
returnPolicyBody: string;
|
|
205
207
|
shippingInfoBody: string;
|
|
206
208
|
noReturnPolicyBody: boolean;
|
|
@@ -535,6 +537,7 @@ const svc = onlineService<S, {
|
|
|
535
537
|
wasPriceEmpty: false,
|
|
536
538
|
skuLabel: "",
|
|
537
539
|
noSku: true,
|
|
540
|
+
infoSections: [],
|
|
538
541
|
returnPolicyBody: "",
|
|
539
542
|
shippingInfoBody: "",
|
|
540
543
|
noReturnPolicyBody: true,
|
|
@@ -625,7 +628,7 @@ const svc = onlineService<S, {
|
|
|
625
628
|
try { saved = isProductSaved(id); } catch { /* save action reports a retryable error */ }
|
|
626
629
|
s = { ...s, saved, saveHint: "",
|
|
627
630
|
optionValidationTarget: "",
|
|
628
|
-
...(id !== lastLoadedId ? { adding: false, added: false, shareHint: "", addedSheetPresented: false, quantity: 1, showOptionErrors: false, optionValidationRequest: 0, galleryIndex: 0, returnPolicyExpanded: false, shippingInfoExpanded: false } : {}) };
|
|
631
|
+
...(id !== lastLoadedId ? { adding: false, added: false, shareHint: "", addedSheetPresented: false, quantity: 1, showOptionErrors: false, optionValidationRequest: 0, galleryIndex: 0, returnPolicyExpanded: false, shippingInfoExpanded: false, infoSections: [] } : {}) };
|
|
629
632
|
// A fresh navigation into the product resets any quantity left over
|
|
630
633
|
// from a previous visit to the same worker instance.
|
|
631
634
|
if (!id) {
|
|
@@ -642,8 +645,8 @@ const svc = onlineService<S, {
|
|
|
642
645
|
void cartCount.then((n) => { cartKnown = n; }, () => {});
|
|
643
646
|
// The live product read goes out FIRST too — the local-db read below must never delay it.
|
|
644
647
|
// Settled into a value so a failure is handled where it is awaited, never unhandled.
|
|
645
|
-
// Read
|
|
646
|
-
const live = getStoreProduct(id, [
|
|
648
|
+
// Read the Ricos description, the Product Info sections (V3) and choice-linked images; V3 omits them by default.
|
|
649
|
+
const live = getStoreProduct(id, [...PRODUCT_DETAIL_FIELDS, "PRODUCT_CHOICES_DISPLAY_IMAGE"], readOptions(arg)).then(
|
|
647
650
|
(r) => ({ ok: true as const, r }),
|
|
648
651
|
(error) => ({ ok: false as const, error }),
|
|
649
652
|
);
|
|
@@ -702,6 +705,7 @@ const svc = onlineService<S, {
|
|
|
702
705
|
...sale,
|
|
703
706
|
commercePrice: sale.price,
|
|
704
707
|
skuLabel: skuOf(p) ? `SKU: ${skuOf(p)}` : "",
|
|
708
|
+
infoSections: productInfoSections(p, base.infoSections),
|
|
705
709
|
returnPolicyBody: productPolicyBody(p, "returns") || copyParam("product.returnPolicyBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
706
710
|
shippingInfoBody: productPolicyBody(p, "shipping") || copyParam("product.shippingInfoBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
707
711
|
...productBody(p),
|
|
@@ -721,6 +725,12 @@ const svc = onlineService<S, {
|
|
|
721
725
|
const quantity = parseQuantity(argValue(arg, "value"));
|
|
722
726
|
return quantity ? derive({ ...s, quantity }) : s;
|
|
723
727
|
},
|
|
728
|
+
toggleInfoSection: (s, arg) => {
|
|
729
|
+
const id = argValue(arg, "id");
|
|
730
|
+
if (!s.infoSections.some(section => section.id === id && !section.noTitle)) return s;
|
|
731
|
+
return derive({ ...s, infoSections: s.infoSections.map(section => section.id === id
|
|
732
|
+
? { ...section, expanded: !section.expanded, collapsed: section.expanded } : section) });
|
|
733
|
+
},
|
|
724
734
|
toggleReturnPolicy: s => derive({ ...s, returnPolicyExpanded: !s.returnPolicyExpanded }),
|
|
725
735
|
toggleShippingInfo: s => derive({ ...s, shippingInfoExpanded: !s.shippingInfoExpanded }),
|
|
726
736
|
// One chip row per option group. `group` is the descriptor slot ("1".."3");
|
|
@@ -5,7 +5,7 @@ import { defineService } from "@himalaya/state";
|
|
|
5
5
|
import { getPost, reactToPost, removePostReaction, createForumComment, forumPostContent, type WixPost } from "@himalaya/wix-forum";
|
|
6
6
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
7
7
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
8
|
-
import { richBody } from "../_shared/rich-body.js";
|
|
8
|
+
import { richBody, descriptionBody } from "../_shared/rich-body.js";
|
|
9
9
|
|
|
10
10
|
import { readAccess, assertReadAccess, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
|
|
11
11
|
|
|
@@ -62,7 +62,8 @@ const svc = defineService<S, {
|
|
|
62
62
|
const doc = forumPostContent(p);
|
|
63
63
|
// A string `content` is legacy HTML only when it is not a serialized Ricos document, which must never
|
|
64
64
|
// surface as raw JSON text.
|
|
65
|
-
const legacy =
|
|
65
|
+
const legacy = pp.contentText ? plain(pp.contentText)
|
|
66
|
+
: descriptionBody(typeof pp.content === "string" && !doc ? pp.content : "").bodyText;
|
|
66
67
|
// The Ricos `content`, drawn by RichContent; bodyJson "" (none, or rejected) shows the plain body instead,
|
|
67
68
|
// which is the document's own text when there is no contentText.
|
|
68
69
|
const { bodyJson, bodyText } = richBody(doc, legacy);
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@himalaya/wix-members";
|
|
15
15
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
16
16
|
|
|
17
|
-
import { readAccess, assertReadAccess, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
|
|
17
|
+
import { readAccess, assertReadAccess, readSignInFlags, NO_READ_SIGN_IN, type ReadAccess } from "../_shared/member-read.js";
|
|
18
18
|
|
|
19
19
|
interface S extends PresenceFlags {
|
|
20
20
|
signInRequired: boolean; noSignIn: boolean;
|
|
@@ -27,8 +27,8 @@ interface S extends PresenceFlags {
|
|
|
27
27
|
photo: string;
|
|
28
28
|
followHint: string;
|
|
29
29
|
isFollowing: boolean;
|
|
30
|
-
followers: number;
|
|
31
|
-
following: number;
|
|
30
|
+
followers: number | null;
|
|
31
|
+
following: number | null;
|
|
32
32
|
followLabel: string;
|
|
33
33
|
followCount: string;
|
|
34
34
|
}
|
|
@@ -42,8 +42,8 @@ function name(m: WixMember): string {
|
|
|
42
42
|
function followLabelFor(isFollowing: boolean): string {
|
|
43
43
|
return isFollowing ? "Following ✓" : "Follow";
|
|
44
44
|
}
|
|
45
|
-
function followCountFor(followers: number, following: number): string {
|
|
46
|
-
return followers + " Followers · " + following + " Following";
|
|
45
|
+
function followCountFor(followers: number | null, following: number | null): string {
|
|
46
|
+
return followers === null || following === null ? "" : followers + " Followers · " + following + " Following";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const INITIAL: S = {
|
|
@@ -56,22 +56,24 @@ const INITIAL: S = {
|
|
|
56
56
|
photo: "",
|
|
57
57
|
followHint: "",
|
|
58
58
|
isFollowing: false,
|
|
59
|
-
followers:
|
|
60
|
-
following:
|
|
59
|
+
followers: null,
|
|
60
|
+
following: null,
|
|
61
61
|
followLabel: "Follow",
|
|
62
|
-
followCount: "
|
|
62
|
+
followCount: "",
|
|
63
63
|
...presence("loading"), ...NO_READ_SIGN_IN,
|
|
64
64
|
};
|
|
65
65
|
let loadVersion = 0;
|
|
66
|
+
let loadedAccess: ReadAccess | undefined;
|
|
67
|
+
const pendingFollows = new Set<string>();
|
|
66
68
|
|
|
67
69
|
const svc = defineService<S, {
|
|
68
70
|
prepare: (s: S) => S;
|
|
69
71
|
load: (s: S) => Promise<S | undefined>;
|
|
70
|
-
toggleFollow: (s: S) => Promise<S>;
|
|
72
|
+
toggleFollow: (s: S) => Promise<S | undefined>;
|
|
71
73
|
}>("spacesMemberDetail", {
|
|
72
74
|
state: INITIAL,
|
|
73
75
|
actions: {
|
|
74
|
-
prepare: (s) =>
|
|
76
|
+
prepare: (s) => { loadedAccess = undefined; return { ...s, ...INITIAL }; },
|
|
75
77
|
load: async (s): Promise<S | undefined> => {
|
|
76
78
|
const version = ++loadVersion;
|
|
77
79
|
await svc.prepare();
|
|
@@ -83,15 +85,15 @@ const svc = defineService<S, {
|
|
|
83
85
|
const m = ((res as unknown as { member?: WixMember }).member) ?? (res as unknown as WixMember);
|
|
84
86
|
const n = name(m);
|
|
85
87
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
let followers =
|
|
89
|
-
let following =
|
|
88
|
+
// A forbidden/missing aggregate is unknown, while the public profile
|
|
89
|
+
// remains readable. The descriptor collapses an empty count label.
|
|
90
|
+
let followers: number | null = null;
|
|
91
|
+
let following: number | null = null;
|
|
90
92
|
try {
|
|
91
93
|
const counts = await getFollowCounts(id);
|
|
92
94
|
followers = counts.followers;
|
|
93
95
|
following = counts.following;
|
|
94
|
-
} catch { /*
|
|
96
|
+
} catch { /* unavailable counts stay hidden */ }
|
|
95
97
|
|
|
96
98
|
let isFollowing = false;
|
|
97
99
|
try {
|
|
@@ -100,6 +102,7 @@ const svc = defineService<S, {
|
|
|
100
102
|
|
|
101
103
|
await assertReadAccess(access);
|
|
102
104
|
if (version !== loadVersion) return undefined;
|
|
105
|
+
loadedAccess = access;
|
|
103
106
|
return {
|
|
104
107
|
...INITIAL, status: "ready",
|
|
105
108
|
id,
|
|
@@ -125,35 +128,48 @@ const svc = defineService<S, {
|
|
|
125
128
|
// adjusts the follower total, then recomputes the label + count strings.
|
|
126
129
|
toggleFollow: async (s) => {
|
|
127
130
|
if (!s.id) return { ...s, followHint: "No member selected." };
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
followHint: "Unfollowed.",
|
|
139
|
-
};
|
|
140
|
-
} catch {
|
|
141
|
-
return { ...s, followHint: "Couldn't unfollow — try again." };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
131
|
+
const access = loadedAccess;
|
|
132
|
+
if (!access || detailInputId() !== s.id) return undefined;
|
|
133
|
+
const pendingKey = JSON.stringify([access.tenant, access.member.scope, access.member.email, s.id]);
|
|
134
|
+
if (pendingFollows.has(pendingKey)) return undefined;
|
|
135
|
+
const version = loadVersion;
|
|
136
|
+
const current = () => version === loadVersion && access === loadedAccess && detailInputId() === s.id;
|
|
137
|
+
// Serialize this reader's writes to one member within this worker, including after
|
|
138
|
+
// navigation back to it. Another member or reader/site can act while this request is
|
|
139
|
+
// pending. Separate worker instances are isolated contexts and cannot share this guard.
|
|
140
|
+
pendingFollows.add(pendingKey);
|
|
144
141
|
try {
|
|
145
|
-
await
|
|
146
|
-
|
|
142
|
+
await assertReadAccess(access);
|
|
143
|
+
if (!current()) return undefined;
|
|
144
|
+
if (s.isFollowing) await unfollowMember(s.id);
|
|
145
|
+
else await followMember(s.id);
|
|
146
|
+
await assertReadAccess(access);
|
|
147
|
+
if (!current()) {
|
|
148
|
+
// The write is confirmed, but a reload overlapped it and may have read the old
|
|
149
|
+
// relationship. Same reader and member: re-read instead of leaving a stale label.
|
|
150
|
+
if (detailInputId() === s.id) await svc.load();
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const isFollowing = !s.isFollowing;
|
|
154
|
+
const followers = s.followers === null ? null : Math.max(0, s.followers + (isFollowing ? 1 : -1));
|
|
147
155
|
return {
|
|
148
156
|
...s,
|
|
149
|
-
isFollowing
|
|
157
|
+
isFollowing,
|
|
150
158
|
followers,
|
|
151
|
-
followLabel: followLabelFor(
|
|
159
|
+
followLabel: followLabelFor(isFollowing),
|
|
152
160
|
followCount: followCountFor(followers, s.following),
|
|
153
|
-
followHint: "Following ✓",
|
|
161
|
+
followHint: isFollowing ? "Following ✓" : "Unfollowed.",
|
|
154
162
|
};
|
|
155
163
|
} catch {
|
|
156
|
-
|
|
164
|
+
// A failed write belongs only to the reader who loaded this profile.
|
|
165
|
+
// Compare explicitly: a context change discards this completion, while a
|
|
166
|
+
// failure for the same reader produces the retry hint below.
|
|
167
|
+
const now = await readAccess();
|
|
168
|
+
if (now.member.email !== access.member.email || now.member.scope !== access.member.scope || now.tenant !== access.tenant) return undefined;
|
|
169
|
+
if (!current()) return undefined;
|
|
170
|
+
return { ...s, followHint: s.isFollowing ? "Couldn't unfollow — try again." : "Couldn't follow — try again." };
|
|
171
|
+
} finally {
|
|
172
|
+
pendingFollows.delete(pendingKey);
|
|
157
173
|
}
|
|
158
174
|
},
|
|
159
175
|
},
|