@ossy/booking 1.16.2 → 1.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +44 -21
  2. package/package.json +11 -8
  3. package/src/AvailabilityEditor.jsx +2 -13
  4. package/src/BookingList.jsx +1 -1
  5. package/src/BookingProductHome.jsx +38 -0
  6. package/src/BookingSalesPage.jsx +55 -0
  7. package/src/Definition.js +1 -1
  8. package/src/HeroCover.jsx +5 -5
  9. package/src/SalesSection.jsx +7 -15
  10. package/src/ServiceCard.jsx +2 -2
  11. package/src/availability-action-maps.js +39 -0
  12. package/src/availability-engine.js +18 -14
  13. package/src/availability-engine.spec.js +36 -4
  14. package/src/availability-setup.page.jsx +45 -46
  15. package/src/{availability.resource.js → availability.schema.js} +1 -5
  16. package/src/booking-cancellation.email.jsx +1 -1
  17. package/src/booking-card.component.jsx +3 -3
  18. package/src/booking-confirmation.email.jsx +5 -3
  19. package/src/booking-detail.page.jsx +15 -10
  20. package/src/booking-email-token.js +45 -0
  21. package/src/booking-home-content.js +50 -0
  22. package/src/booking-list.aggregate.js +65 -0
  23. package/src/booking-reminder.email.jsx +4 -3
  24. package/src/booking-request.email.jsx +5 -2
  25. package/src/booking-resources.js +3 -3
  26. package/src/{booking.resource.js → booking.schema.js} +12 -3
  27. package/src/bookings.component.jsx +7 -11
  28. package/src/bookings.page.jsx +29 -16
  29. package/src/cancel-booking.action.js +1 -1
  30. package/src/cancel-booking.task.js +14 -17
  31. package/src/confirm-booking.action.js +1 -1
  32. package/src/confirm-booking.api.js +35 -0
  33. package/src/confirm-booking.task.js +26 -24
  34. package/src/create-booking.action.js +1 -1
  35. package/src/create-booking.task.js +73 -37
  36. package/src/create-service.action.js +5 -1
  37. package/src/create-service.task.js +7 -7
  38. package/src/decline-booking.action.js +1 -1
  39. package/src/decline-booking.api.js +35 -0
  40. package/src/decline-booking.task.js +15 -18
  41. package/src/delete-service.action.js +1 -1
  42. package/src/delete-service.task.js +12 -15
  43. package/src/en.translations.json +83 -37
  44. package/src/get-availability.action.js +1 -1
  45. package/src/get-availability.task.js +5 -4
  46. package/src/get-available-slots.action.js +1 -1
  47. package/src/get-available-slots.task.js +16 -13
  48. package/src/get-services.action.js +1 -1
  49. package/src/get-services.task.js +13 -6
  50. package/src/home.page.jsx +14 -46
  51. package/src/index.js +17 -0
  52. package/src/list-bookings.action.js +1 -1
  53. package/src/list-bookings.task.js +33 -16
  54. package/src/locations.js +6 -4
  55. package/src/moduleStatus.js +30 -2
  56. package/src/open-availability-setup.action.js +5 -0
  57. package/src/open-bookings.action.js +5 -0
  58. package/src/open-create-service.action.js +5 -0
  59. package/src/open-services.action.js +5 -0
  60. package/src/provider-availability.flow.js +39 -0
  61. package/src/provider-fields.js +34 -0
  62. package/src/provider-onboard.flow.js +25 -0
  63. package/src/public-booking-form.form.js +7 -0
  64. package/src/public-booking-form.schema.js +12 -0
  65. package/src/public-booking.page.jsx +58 -56
  66. package/src/resolve-workspace-contact.js +55 -0
  67. package/src/save-availability.action.js +5 -1
  68. package/src/save-availability.task.js +26 -28
  69. package/src/schema-ids.js +7 -0
  70. package/src/select-duration-30.action.js +4 -0
  71. package/src/select-duration-60.action.js +4 -0
  72. package/src/select-duration-90.action.js +4 -0
  73. package/src/send-booking-reminder.task.js +39 -14
  74. package/src/service-card-slot.component.jsx +4 -4
  75. package/src/service-detail.component.jsx +4 -4
  76. package/src/service.form.js +7 -0
  77. package/src/service.schema.js +14 -0
  78. package/src/services.page.jsx +62 -58
  79. package/src/stories/AvailabilityEditor.stories.jsx +54 -0
  80. package/src/stories/AvailabilitySetupPage.stories.jsx +44 -0
  81. package/src/stories/BookingCard.stories.jsx +57 -0
  82. package/src/stories/BookingList.stories.jsx +30 -0
  83. package/src/stories/BookingsComponent.stories.jsx +31 -0
  84. package/src/stories/BookingsPage.stories.jsx +66 -0
  85. package/src/stories/HeroCover.stories.jsx +42 -0
  86. package/src/stories/PublicBookingForm.stories.jsx +66 -0
  87. package/src/stories/PublicBookingPage.stories.jsx +78 -0
  88. package/src/stories/ServiceCard.stories.jsx +52 -0
  89. package/src/stories/ServiceDetail.stories.jsx +52 -0
  90. package/src/stories/ServiceForm.stories.jsx +64 -0
  91. package/src/stories/ServicesPage.stories.jsx +53 -0
  92. package/src/stories/decorators.jsx +68 -0
  93. package/src/stories/fixtures.js +149 -0
  94. package/src/stories/story-helpers.jsx +85 -0
  95. package/src/sv.translations.json +83 -37
  96. package/src/time.js +27 -0
  97. package/src/toggle-availability-day-0.action.js +4 -0
  98. package/src/toggle-availability-day-1.action.js +4 -0
  99. package/src/toggle-availability-day-2.action.js +4 -0
  100. package/src/toggle-availability-day-3.action.js +4 -0
  101. package/src/toggle-availability-day-4.action.js +4 -0
  102. package/src/toggle-availability-day-5.action.js +4 -0
  103. package/src/toggle-availability-day-6.action.js +4 -0
  104. package/src/update-service.action.js +1 -1
  105. package/src/update-service.task.js +13 -16
  106. package/src/workspace-services.js +5 -0
  107. package/src/service.resource.js +0 -32
@@ -0,0 +1,42 @@
1
+ import React from 'react'
2
+ import { HeroCover } from '../HeroCover.jsx'
3
+ import { sampleHeroCover } from './fixtures.js'
4
+
5
+ export default {
6
+ title: 'Booking/Components/HeroCover',
7
+ component: HeroCover,
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ story: {
12
+ inline: true,
13
+ height: 'auto',
14
+ },
15
+ },
16
+ },
17
+ }
18
+
19
+ export const Default = () => <HeroCover {...sampleHeroCover} />
20
+
21
+ export const SingleAction = () => (
22
+ <HeroCover
23
+ {...sampleHeroCover}
24
+ actions={[{ label: 'Start booking setup', variant: 'cta' }]}
25
+ />
26
+ )
27
+
28
+ export const CompactTitle = () => (
29
+ <HeroCover
30
+ title="Simple scheduling for small teams"
31
+ titleMaxWidth="640px"
32
+ text="Publish services, collect requests, and confirm appointments from one workspace."
33
+ actions={[{ label: 'Explore booking', variant: 'primary' }]}
34
+ />
35
+ )
36
+
37
+ export const Minimal = () => (
38
+ <HeroCover
39
+ title="Booking"
40
+ text="Let clients request time with you online."
41
+ />
42
+ )
@@ -0,0 +1,66 @@
1
+ import React, { useState } from 'react'
2
+ import { Button, View, Form, FieldFactory } from '@ossy/design-system'
3
+ import publicBookingResource from '../public-booking-form.schema.js'
4
+ import { metadata as publicBookingFormMeta } from '../public-booking-form.form.js'
5
+ import { samplePublicBookingFormData } from './fixtures.js'
6
+
7
+ const dataPreviewStyle = {
8
+ margin: 0,
9
+ padding: 'var(--space-s)',
10
+ background: 'var(--color-secondary)',
11
+ color: 'var(--text-default-color, CanvasText)',
12
+ borderRadius: 'var(--space-xs)',
13
+ border: '1px solid var(--separator-primary)',
14
+ fontSize: 12,
15
+ overflow: 'auto',
16
+ }
17
+
18
+ export default {
19
+ title: 'Booking/Forms/PublicBookingForm',
20
+ component: Form,
21
+ parameters: {
22
+ layout: 'padded',
23
+ docs: {
24
+ story: {
25
+ inline: true,
26
+ height: 'auto',
27
+ },
28
+ },
29
+ },
30
+ }
31
+
32
+ function PublicBookingFormShell ({ initial = {} }) {
33
+ const [preview, setPreview] = useState(initial)
34
+
35
+ return (
36
+ <View
37
+ surface="primary"
38
+ inset="m"
39
+ roundness="m"
40
+ gap="m"
41
+ style={{ maxWidth: 480, width: '100%', fontFamily: 'system-ui, sans-serif' }}
42
+ >
43
+ <Form
44
+ id="storybook/booking/public-booking"
45
+ schemaId={publicBookingFormMeta.schemaId}
46
+ fields={publicBookingResource.fields}
47
+ defaultData={initial}
48
+ onChange={({ data }) => setPreview(data)}
49
+ onSubmit={(data) => setPreview(data)}
50
+ gap="m"
51
+ >
52
+ <FieldFactory />
53
+ <Button type="submit" variant="cta">
54
+ Request booking
55
+ </Button>
56
+ </Form>
57
+ <pre style={dataPreviewStyle}>{JSON.stringify(preview, null, 2)}</pre>
58
+ </View>
59
+ )
60
+ }
61
+
62
+ export const Empty = () => <PublicBookingFormShell />
63
+
64
+ export const Prefilled = () => (
65
+ <PublicBookingFormShell initial={samplePublicBookingFormData} />
66
+ )
@@ -0,0 +1,78 @@
1
+ import React from 'react'
2
+ import PublicBookingPage from '../public-booking.page.jsx'
3
+ import { withMockSdk } from './decorators.jsx'
4
+ import { useAdvancePublicBookingStep } from './story-helpers.jsx'
5
+ import { sampleProviderContact } from './fixtures.js'
6
+
7
+ export default {
8
+ title: 'Booking/Pages/PublicBookingPage',
9
+ component: PublicBookingPage,
10
+ decorators: [withMockSdk],
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ docs: {
14
+ story: {
15
+ inline: false,
16
+ height: '720px',
17
+ },
18
+ },
19
+ },
20
+ }
21
+
22
+ const pageArgs = {
23
+ providerSlug: sampleProviderContact.workspaceId,
24
+ }
25
+
26
+ export const ServiceSelection = {
27
+ args: pageArgs,
28
+ parameters: {
29
+ mockSdk: {},
30
+ },
31
+ }
32
+
33
+ function CalendarStepStory () {
34
+ useAdvancePublicBookingStep('pick-slot')
35
+ return <PublicBookingPage providerSlug={pageArgs.providerSlug} />
36
+ }
37
+
38
+ export const CalendarAndSlots = {
39
+ render: () => <CalendarStepStory />,
40
+ args: pageArgs,
41
+ parameters: {
42
+ mockSdk: {},
43
+ },
44
+ }
45
+
46
+ function ClientDetailsStepStory () {
47
+ useAdvancePublicBookingStep('fill-form')
48
+ return <PublicBookingPage providerSlug={pageArgs.providerSlug} />
49
+ }
50
+
51
+ export const ClientDetailsForm = {
52
+ render: () => <ClientDetailsStepStory />,
53
+ args: pageArgs,
54
+ parameters: {
55
+ mockSdk: {},
56
+ },
57
+ }
58
+
59
+ export const NoServices = {
60
+ args: pageArgs,
61
+ parameters: {
62
+ mockSdk: {
63
+ publicServices: {
64
+ services: [],
65
+ contact: sampleProviderContact,
66
+ },
67
+ },
68
+ },
69
+ }
70
+
71
+ export const ServicesLoadError = {
72
+ args: pageArgs,
73
+ parameters: {
74
+ mockSdk: {
75
+ '@ossy/booking/actions/get-services': () => Promise.reject(new Error('Provider unavailable')),
76
+ },
77
+ },
78
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import { View } from '@ossy/design-system'
3
+ import { ServiceCard } from '../ServiceCard.jsx'
4
+ import { sampleServices } from './fixtures.js'
5
+
6
+ export default {
7
+ title: 'Booking/Components/ServiceCard',
8
+ component: ServiceCard,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ story: {
13
+ inline: true,
14
+ height: 'auto',
15
+ },
16
+ },
17
+ },
18
+ }
19
+
20
+ const cardShell = (props) => (
21
+ <View style={{ maxWidth: 520, width: '100%' }}>
22
+ <ServiceCard {...props} />
23
+ </View>
24
+ )
25
+
26
+ export const Default = () => cardShell(sampleServices[0])
27
+
28
+ export const FreeService = () =>
29
+ cardShell({
30
+ ...sampleServices[0],
31
+ name: 'Intro call',
32
+ price: 0,
33
+ duration: 30,
34
+ description: 'A short complimentary introduction.',
35
+ })
36
+
37
+ export const Inactive = () => cardShell(sampleServices[2])
38
+
39
+ export const LongDescription = () =>
40
+ cardShell({
41
+ ...sampleServices[0],
42
+ description:
43
+ 'A comprehensive first meeting covering goals, constraints, timeline, and deliverables. We leave with a shared understanding of scope and a proposed next step.',
44
+ })
45
+
46
+ export const AllVariants = () => (
47
+ <View gap="m" style={{ maxWidth: 520, width: '100%' }}>
48
+ {sampleServices.map((service) => (
49
+ <ServiceCard key={service.name} {...service} />
50
+ ))}
51
+ </View>
52
+ )
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import { View } from '@ossy/design-system'
3
+ import ServiceDetailComponent from '../service-detail.component.jsx'
4
+ import { sampleServiceResource } from './fixtures.js'
5
+
6
+ export default {
7
+ title: 'Booking/Components/ServiceDetail',
8
+ component: ServiceDetailComponent,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ story: {
13
+ inline: true,
14
+ height: 'auto',
15
+ },
16
+ },
17
+ },
18
+ }
19
+
20
+ const detailShell = (resource) => (
21
+ <View surface="primary" roundness="m" style={{ maxWidth: 560, width: '100%' }}>
22
+ <ServiceDetailComponent resource={resource} />
23
+ </View>
24
+ )
25
+
26
+ export const ActiveService = () => detailShell(sampleServiceResource)
27
+
28
+ export const FreeService = () =>
29
+ detailShell({
30
+ ...sampleServiceResource,
31
+ name: 'Intro call',
32
+ content: {
33
+ ...sampleServiceResource.content,
34
+ name: 'Intro call',
35
+ price: 0,
36
+ duration: 30,
37
+ description: 'A short complimentary introduction call.',
38
+ },
39
+ })
40
+
41
+ export const InactiveService = () =>
42
+ detailShell({
43
+ ...sampleServiceResource,
44
+ content: {
45
+ ...sampleServiceResource.content,
46
+ name: 'Legacy workshop',
47
+ active: false,
48
+ price: 0,
49
+ duration: 120,
50
+ description: 'No longer offered to new clients.',
51
+ },
52
+ })
@@ -0,0 +1,64 @@
1
+ import React, { useState } from 'react'
2
+ import { Button, View, Form, FieldFactory } from '@ossy/design-system'
3
+ import serviceResource from '../service.schema.js'
4
+ import { metadata as serviceFormMeta } from '../service.form.js'
5
+ import { sampleServiceFormData } from './fixtures.js'
6
+
7
+ const dataPreviewStyle = {
8
+ margin: 0,
9
+ padding: 'var(--space-s)',
10
+ background: 'var(--color-secondary)',
11
+ color: 'var(--text-default-color, CanvasText)',
12
+ borderRadius: 'var(--space-xs)',
13
+ border: '1px solid var(--separator-primary)',
14
+ fontSize: 12,
15
+ overflow: 'auto',
16
+ }
17
+
18
+ export default {
19
+ title: 'Booking/Forms/ServiceForm',
20
+ component: Form,
21
+ parameters: {
22
+ layout: 'padded',
23
+ docs: {
24
+ story: {
25
+ inline: true,
26
+ height: 'auto',
27
+ },
28
+ },
29
+ },
30
+ }
31
+
32
+ function ServiceFormShell ({ initial = sampleServiceFormData }) {
33
+ const [preview, setPreview] = useState(initial)
34
+
35
+ return (
36
+ <View
37
+ surface="primary"
38
+ inset="m"
39
+ roundness="m"
40
+ gap="m"
41
+ style={{ maxWidth: 480, width: '100%', fontFamily: 'system-ui, sans-serif' }}
42
+ >
43
+ <Form
44
+ id="storybook/booking/service"
45
+ schemaId={serviceFormMeta.schemaId}
46
+ fields={serviceResource.fields}
47
+ defaultData={initial}
48
+ onChange={({ data }) => setPreview(data)}
49
+ onSubmit={(data) => setPreview(data)}
50
+ gap="m"
51
+ >
52
+ <FieldFactory />
53
+ <Button type="submit" variant="cta">
54
+ Save service
55
+ </Button>
56
+ </Form>
57
+ <pre style={dataPreviewStyle}>{JSON.stringify(preview, null, 2)}</pre>
58
+ </View>
59
+ )
60
+ }
61
+
62
+ export const Default = () => <ServiceFormShell />
63
+
64
+ export const Empty = () => <ServiceFormShell initial={{ active: true, currency: 'SEK' }} />
@@ -0,0 +1,53 @@
1
+ import React from 'react'
2
+ import ServicesPage from '../services.page.jsx'
3
+ import { withMockSdk } from './decorators.jsx'
4
+ import { useOpenServicesCreateForm } from './story-helpers.jsx'
5
+
6
+ export default {
7
+ title: 'Booking/Pages/ServicesPage',
8
+ component: ServicesPage,
9
+ decorators: [withMockSdk],
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ docs: {
13
+ story: {
14
+ inline: false,
15
+ height: '640px',
16
+ },
17
+ },
18
+ },
19
+ }
20
+
21
+ export const ListWithServices = {
22
+ parameters: {
23
+ mockSdk: {},
24
+ },
25
+ }
26
+
27
+ export const EmptyList = {
28
+ parameters: {
29
+ mockSdk: {
30
+ services: [],
31
+ },
32
+ },
33
+ }
34
+
35
+ function CreateFormOpenStory () {
36
+ useOpenServicesCreateForm()
37
+ return <ServicesPage />
38
+ }
39
+
40
+ export const CreateFormOpen = {
41
+ render: () => <CreateFormOpenStory />,
42
+ parameters: {
43
+ mockSdk: {},
44
+ },
45
+ }
46
+
47
+ export const LoadError = {
48
+ parameters: {
49
+ mockSdk: {
50
+ '@ossy/booking/actions/get-services': () => Promise.reject(new Error('Failed to load services')),
51
+ },
52
+ },
53
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ createMockRouterDecorator,
3
+ createMockSdkDecorator,
4
+ resolveMockActionId,
5
+ } from '../../../../.storybook/decorators/index.jsx'
6
+ import {
7
+ buildSampleSlots,
8
+ sampleAvailabilitySettings,
9
+ sampleBookings,
10
+ samplePublicServicesResponse,
11
+ sampleServicesWithIds,
12
+ } from './fixtures.js'
13
+
14
+ export { resolveMockActionId }
15
+
16
+ export function createMockSdkHandlers (overrides = {}) {
17
+ return {
18
+ '@ossy/booking/actions/get-services': async (payload) => {
19
+ if (payload?.providerSlug) {
20
+ if ('publicServices' in overrides) return overrides.publicServices
21
+ return samplePublicServicesResponse
22
+ }
23
+ if ('services' in overrides) return overrides.services
24
+ return sampleServicesWithIds
25
+ },
26
+ '@ossy/booking/actions/list': async () => (
27
+ 'bookings' in overrides ? overrides.bookings : sampleBookings
28
+ ),
29
+ '@ossy/booking/actions/get-available-slots': async () => (
30
+ 'slots' in overrides ? overrides.slots : buildSampleSlots()
31
+ ),
32
+ '@ossy/booking/actions/get-availability': async () => (
33
+ 'availability' in overrides ? overrides.availability : sampleAvailabilitySettings
34
+ ),
35
+ '@ossy/booking/actions/create-service': async () => ({}),
36
+ '@ossy/booking/actions/update-service': async () => ({}),
37
+ '@ossy/booking/actions/delete-service': async () => ({}),
38
+ '@ossy/booking/actions/save-availability': async () => ({}),
39
+ '@ossy/booking/actions/create': async () => ({}),
40
+ '@ossy/booking/actions/confirm': async () => ({}),
41
+ '@ossy/booking/actions/decline': async () => ({}),
42
+ '@ossy/booking/actions/cancel': async () => ({}),
43
+ }
44
+ }
45
+
46
+ const defaultMockRouter = {
47
+ href: 'http://localhost/en/bookings',
48
+ pages: [],
49
+ language: 'en',
50
+ defaultLanguage: 'en',
51
+ supportedLanguages: ['en', 'sv'],
52
+ params: {},
53
+ searchParams: {},
54
+ navigate: () => {},
55
+ getHref: ({ id, params = {} } = {}) => {
56
+ if (id === 'booking/booking-detail' && params.bookingId) {
57
+ return `/en/bookings/${params.bookingId}`
58
+ }
59
+ if (id === 'public-booking' && params.providerSlug) {
60
+ return `/en/book/${params.providerSlug}`
61
+ }
62
+ return `/en/${String(id).replace(/\//g, '-')}`
63
+ },
64
+ back: () => {},
65
+ }
66
+
67
+ export const withMockSdk = createMockSdkDecorator(createMockSdkHandlers)
68
+ export const withMockRouter = createMockRouterDecorator(defaultMockRouter)
@@ -0,0 +1,149 @@
1
+ export const sampleServices = [
2
+ {
3
+ name: 'Initial consultation',
4
+ duration: 60,
5
+ price: 120000,
6
+ currency: 'SEK',
7
+ description: 'A focused first meeting to understand your needs and outline next steps.',
8
+ active: true,
9
+ },
10
+ {
11
+ name: 'Follow-up session',
12
+ duration: 45,
13
+ price: 90000,
14
+ currency: 'SEK',
15
+ description: 'Check in on progress and adjust the plan.',
16
+ active: true,
17
+ },
18
+ {
19
+ name: 'Legacy workshop',
20
+ duration: 120,
21
+ price: 0,
22
+ currency: 'SEK',
23
+ description: 'No longer offered to new clients.',
24
+ active: false,
25
+ },
26
+ ]
27
+
28
+ export const sampleBookings = [
29
+ {
30
+ id: 'booking-1',
31
+ clientName: 'Anna Svensson',
32
+ clientEmail: 'anna@example.com',
33
+ startsAt: Date.parse('2026-06-20T09:00:00.000Z'),
34
+ duration: 60,
35
+ status: 'pending',
36
+ paymentStatus: 'unpaid',
37
+ clientMessage: 'Looking forward to discussing the project scope.',
38
+ },
39
+ {
40
+ id: 'booking-2',
41
+ clientName: 'Erik Johansson',
42
+ clientEmail: 'erik@example.com',
43
+ startsAt: Date.parse('2026-06-22T13:30:00.000Z'),
44
+ duration: 45,
45
+ status: 'confirmed',
46
+ paymentStatus: 'paid',
47
+ },
48
+ {
49
+ id: 'booking-3',
50
+ clientName: 'Maria Lind',
51
+ clientEmail: 'maria@example.com',
52
+ startsAt: Date.parse('2026-06-10T10:00:00.000Z'),
53
+ duration: 60,
54
+ status: 'cancelled',
55
+ paymentStatus: 'refunded',
56
+ },
57
+ ]
58
+
59
+ export const sampleAvailability = [
60
+ { dayOfWeek: 1, startTime: '09:00', endTime: '12:00' },
61
+ { dayOfWeek: 1, startTime: '13:00', endTime: '17:00' },
62
+ { dayOfWeek: 2, startTime: '10:00', endTime: '16:00' },
63
+ { dayOfWeek: 4, startTime: '08:30', endTime: '11:30' },
64
+ ]
65
+
66
+ export const sampleServiceFormData = {
67
+ name: 'Strategy session',
68
+ duration: 90,
69
+ price: 150000,
70
+ currency: 'SEK',
71
+ description: 'Deep-dive strategy workshop for growing teams.',
72
+ active: true,
73
+ }
74
+
75
+ export const samplePublicBookingFormData = {
76
+ clientName: 'Anna Svensson',
77
+ clientEmail: 'anna@example.com',
78
+ clientMessage: 'I would like to book the initial consultation.',
79
+ }
80
+
81
+ export const sampleServicesWithIds = sampleServices.map((service, index) => ({
82
+ ...service,
83
+ id: `service-${index + 1}`,
84
+ }))
85
+
86
+ export const sampleProviderContact = {
87
+ workspaceId: 'demo-studio',
88
+ providerEmail: 'hello@demo.studio',
89
+ providerName: 'Demo Studio',
90
+ }
91
+
92
+ export const samplePublicServicesResponse = {
93
+ services: sampleServicesWithIds.filter((service) => service.active),
94
+ contact: sampleProviderContact,
95
+ }
96
+
97
+ /** Slots on upcoming weekdays so the public booking calendar has selectable dates. */
98
+ export function buildSampleSlots ({ fromDate = new Date() } = {}) {
99
+ const slots = []
100
+ const cursor = new Date(fromDate)
101
+ cursor.setHours(0, 0, 0, 0)
102
+
103
+ for (let day = 0; day < 45 && slots.length < 18; day += 1) {
104
+ const date = new Date(cursor)
105
+ date.setDate(cursor.getDate() + day)
106
+ const weekday = date.getDay()
107
+ if (weekday === 0 || weekday === 6) continue
108
+
109
+ for (const hour of [9, 11, 14, 16]) {
110
+ const start = new Date(date)
111
+ start.setHours(hour, 0, 0, 0)
112
+ if (start <= fromDate) continue
113
+ slots.push({
114
+ startAt: start.getTime(),
115
+ duration: 60,
116
+ })
117
+ }
118
+ }
119
+
120
+ return slots
121
+ }
122
+
123
+ export const sampleAvailabilitySettings = {
124
+ weeklyWindows: sampleAvailability,
125
+ sessionDurations: [30, 60],
126
+ bufferMinutes: 15,
127
+ timezone: 'Europe/Stockholm',
128
+ }
129
+
130
+ export const sampleServiceResource = {
131
+ name: 'Initial consultation',
132
+ content: {
133
+ name: 'Initial consultation',
134
+ duration: 60,
135
+ price: 120000,
136
+ currency: 'SEK',
137
+ description: 'A focused first meeting to understand your needs and outline next steps.',
138
+ active: true,
139
+ },
140
+ }
141
+
142
+ export const sampleHeroCover = {
143
+ title: 'Book time with us',
144
+ text: 'Share your availability, publish services, and let clients request appointments online.',
145
+ actions: [
146
+ { label: 'Get started', variant: 'cta' },
147
+ { label: 'See how it works', variant: 'neutral' },
148
+ ],
149
+ }