@ossy/booking 1.15.6 → 1.16.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/README.md +12 -0
- package/package.json +7 -7
- package/src/AvailabilityEditor.jsx +30 -19
- package/src/BookingForm.jsx +21 -31
- package/src/Definition.js +1 -23
- package/src/HeroCover.jsx +99 -0
- package/src/SalesSection.jsx +77 -0
- package/src/availability-setup.page.jsx +147 -397
- package/src/availability.page.jsx +25 -23
- package/src/booking-card.component.jsx +76 -215
- package/src/booking-detail.page.jsx +103 -573
- package/src/booking-page.page.jsx +4 -7
- package/src/bookings.page.jsx +62 -196
- package/src/cover.js +8 -0
- package/src/en.translations.json +213 -0
- package/src/features.js +27 -0
- package/src/home.page.jsx +47 -364
- package/src/public-booking.page.jsx +138 -636
- package/src/service-card-slot.component.jsx +48 -0
- package/src/service-detail.component.jsx +38 -0
- package/src/services.page.jsx +44 -43
- package/src/sv.translations.json +213 -0
package/README.md
CHANGED
|
@@ -19,6 +19,18 @@ Three resource schemas are registered under the `@ossy/booking` category:
|
|
|
19
19
|
- `@ossy/booking/booking` — a confirmed appointment
|
|
20
20
|
- `@ossy/booking/availability` — recurring weekly availability window
|
|
21
21
|
|
|
22
|
+
## Resource view slots
|
|
23
|
+
|
|
24
|
+
Register custom UI for resource types via `*.component.jsx` with `metadata.id` matching the slot key (see `@ossy/design-system` [SLOTS.md](../design-system/docs/SLOTS.md)):
|
|
25
|
+
|
|
26
|
+
| Component | Slot key |
|
|
27
|
+
|-----------|----------|
|
|
28
|
+
| `service-detail.component.jsx` | `resource:booking/service/detail` |
|
|
29
|
+
| `service-card-slot.component.jsx` | `resource:booking/service/card` |
|
|
30
|
+
| `booking-card.component.jsx` | `resource:booking/booking/card` |
|
|
31
|
+
|
|
32
|
+
Dedicated pages (`services.page.jsx`, `booking-detail.page.jsx`, etc.) continue to work alongside generic `@ossy/resources` slot hosts.
|
|
33
|
+
|
|
22
34
|
## What still needs implementation
|
|
23
35
|
|
|
24
36
|
- **Slot generation** — derive available time slots from `availability` records minus existing `booking` records
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/booking",
|
|
3
3
|
"description": "Booking feature package — services, availability, and appointment management for Ossy consultants",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.16.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"src": "./src"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@ossy/email": "^1.
|
|
18
|
-
"@ossy/event-store": "^1.
|
|
19
|
-
"@ossy/observability": "^1.
|
|
20
|
-
"@ossy/platform": "^1.
|
|
21
|
-
"@ossy/resources": "^1.
|
|
17
|
+
"@ossy/email": "^1.6.0",
|
|
18
|
+
"@ossy/event-store": "^1.8.0",
|
|
19
|
+
"@ossy/observability": "^1.8.0",
|
|
20
|
+
"@ossy/platform": "^1.39.0",
|
|
21
|
+
"@ossy/resources": "^1.12.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@ossy/design-system": ">=1.0.0",
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"/src",
|
|
35
35
|
"README.md"
|
|
36
36
|
],
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "d9d31182be64a448d575da432af2830d909ad4b9"
|
|
38
38
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { View,
|
|
3
|
-
|
|
4
|
-
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
1
|
+
import React, { useState, useMemo } from 'react'
|
|
2
|
+
import { View, Text, Button, Input, useLocale } from '@ossy/design-system'
|
|
5
3
|
|
|
6
4
|
const DEFAULT_WINDOW = { enabled: false, startTime: '09:00', endTime: '17:00', bufferMinutes: 0 }
|
|
7
5
|
|
|
8
6
|
const buildInitialState = (availability = []) => {
|
|
9
|
-
const state =
|
|
7
|
+
const state = Array.from({ length: 7 }, () => ({ ...DEFAULT_WINDOW }))
|
|
10
8
|
for (const window of availability) {
|
|
11
9
|
if (window.dayOfWeek >= 0 && window.dayOfWeek <= 6) {
|
|
12
10
|
state[window.dayOfWeek] = {
|
|
@@ -21,8 +19,19 @@ const buildInitialState = (availability = []) => {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
22
|
+
const { t } = useLocale()
|
|
24
23
|
const [days, setDays] = useState(() => buildInitialState(availability))
|
|
25
24
|
|
|
25
|
+
const dayKeys = useMemo(() => [
|
|
26
|
+
'booking.availabilityEditor.day.sun',
|
|
27
|
+
'booking.availabilityEditor.day.mon',
|
|
28
|
+
'booking.availabilityEditor.day.tue',
|
|
29
|
+
'booking.availabilityEditor.day.wed',
|
|
30
|
+
'booking.availabilityEditor.day.thu',
|
|
31
|
+
'booking.availabilityEditor.day.fri',
|
|
32
|
+
'booking.availabilityEditor.day.sat',
|
|
33
|
+
], [])
|
|
34
|
+
|
|
26
35
|
const update = (idx, patch) =>
|
|
27
36
|
setDays((prev) => prev.map((d, i) => (i === idx ? { ...d, ...patch } : d)))
|
|
28
37
|
|
|
@@ -37,16 +46,15 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
37
46
|
bufferMinutes: Number(bufferMinutes),
|
|
38
47
|
}))
|
|
39
48
|
|
|
40
|
-
// TODO: dispatch booking.set-availability action with `windows`
|
|
41
49
|
onSave?.(windows)
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
return (
|
|
45
53
|
<View gap="m">
|
|
46
54
|
<View gap="s">
|
|
47
|
-
{
|
|
55
|
+
{dayKeys.map((key, idx) => (
|
|
48
56
|
<View
|
|
49
|
-
key={
|
|
57
|
+
key={key}
|
|
50
58
|
layout="row"
|
|
51
59
|
gap="m"
|
|
52
60
|
alignItems="center"
|
|
@@ -54,11 +62,15 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
54
62
|
roundness="m"
|
|
55
63
|
inset="s"
|
|
56
64
|
>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
|
66
|
+
<input
|
|
67
|
+
type="checkbox"
|
|
68
|
+
checked={days[idx].enabled}
|
|
69
|
+
onChange={e => update(idx, { enabled: e.target.checked })}
|
|
70
|
+
style={{ width: 16, height: 16, accentColor: 'var(--color-accent)' }}
|
|
71
|
+
/>
|
|
72
|
+
</label>
|
|
73
|
+
<Text weight="medium" style={{ width: '2.5rem', flexShrink: 0 }}>{t(key)}</Text>
|
|
62
74
|
|
|
63
75
|
{days[idx].enabled ? (
|
|
64
76
|
<View layout="row" gap="s" alignItems="center" style={{ flex: 1 }}>
|
|
@@ -68,14 +80,14 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
68
80
|
onChange={(v) => update(idx, { startTime: v })}
|
|
69
81
|
size="s"
|
|
70
82
|
/>
|
|
71
|
-
<Text color="secondary">to</Text>
|
|
83
|
+
<Text color="secondary">{t('booking.availabilityEditor.to')}</Text>
|
|
72
84
|
<Input
|
|
73
85
|
type="time"
|
|
74
86
|
value={days[idx].endTime}
|
|
75
87
|
onChange={(v) => update(idx, { endTime: v })}
|
|
76
88
|
size="s"
|
|
77
89
|
/>
|
|
78
|
-
<Text color="secondary" size="s">
|
|
90
|
+
<Text color="secondary" size="s">{t('booking.availabilityEditor.buffer')}</Text>
|
|
79
91
|
<Input
|
|
80
92
|
type="number"
|
|
81
93
|
value={String(days[idx].bufferMinutes)}
|
|
@@ -83,17 +95,16 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
83
95
|
size="s"
|
|
84
96
|
style={{ width: '4rem' }}
|
|
85
97
|
/>
|
|
86
|
-
<Text color="secondary" size="s">min</Text>
|
|
98
|
+
<Text color="secondary" size="s">{t('booking.availabilityEditor.min')}</Text>
|
|
87
99
|
</View>
|
|
88
100
|
) : (
|
|
89
|
-
<Text color="secondary" size="s">
|
|
101
|
+
<Text color="secondary" size="s">{t('booking.availabilityEditor.unavailable')}</Text>
|
|
90
102
|
)}
|
|
91
103
|
</View>
|
|
92
104
|
))}
|
|
93
105
|
</View>
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
<Button variant="primary" onPress={handleSave}>Save availability</Button>
|
|
107
|
+
<Button variant="primary" onClick={handleSave}>{t('booking.availabilityEditor.save')}</Button>
|
|
97
108
|
</View>
|
|
98
109
|
)
|
|
99
110
|
}
|
package/src/BookingForm.jsx
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { View, Title, Text, Button, Input,
|
|
2
|
+
import { View, Title, Text, Button, Input, useLocale } from '@ossy/design-system'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Public client-facing form.
|
|
6
|
-
*
|
|
7
|
-
* TODO:
|
|
8
|
-
* - Load services for workspaceId via booking.get-services
|
|
9
|
-
* - Load available slots for selected service via booking.get-available-slots
|
|
10
|
-
* - On submit: call booking.create-booking action, then redirect to Stripe Checkout
|
|
11
|
-
*/
|
|
12
4
|
export const BookingForm = ({ workspaceId }) => {
|
|
5
|
+
const { t } = useLocale()
|
|
13
6
|
const [step, setStep] = useState('pick-service')
|
|
14
7
|
const [selectedService, setSelectedService] = useState(null)
|
|
15
8
|
const [selectedSlot, setSelectedSlot] = useState(null)
|
|
@@ -19,11 +12,10 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
19
12
|
if (step === 'pick-service') {
|
|
20
13
|
return (
|
|
21
14
|
<View gap="m">
|
|
22
|
-
<Title variant="secondary">
|
|
23
|
-
{
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
Continue (placeholder)
|
|
15
|
+
<Title variant="secondary">{t('booking.book.form.chooseService')}</Title>
|
|
16
|
+
<Text color="secondary">{t('booking.book.form.loadingServices', { workspaceId })}</Text>
|
|
17
|
+
<Button variant="secondary" onClick={() => setStep('pick-slot')}>
|
|
18
|
+
{t('booking.book.form.continuePlaceholder')}
|
|
27
19
|
</Button>
|
|
28
20
|
</View>
|
|
29
21
|
)
|
|
@@ -32,13 +24,12 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
32
24
|
if (step === 'pick-slot') {
|
|
33
25
|
return (
|
|
34
26
|
<View gap="m">
|
|
35
|
-
<Title variant="secondary">
|
|
36
|
-
{
|
|
37
|
-
<Text color="secondary">Available slots will appear here.</Text>
|
|
27
|
+
<Title variant="secondary">{t('booking.book.form.pickTime')}</Title>
|
|
28
|
+
<Text color="secondary">{t('booking.book.form.slotsPlaceholder')}</Text>
|
|
38
29
|
<View layout="row" gap="s">
|
|
39
|
-
<Button variant="ghost"
|
|
40
|
-
<Button variant="secondary"
|
|
41
|
-
|
|
30
|
+
<Button variant="ghost" onClick={() => setStep('pick-service')}>{t('booking.book.form.back')}</Button>
|
|
31
|
+
<Button variant="secondary" onClick={() => setStep('enter-details')}>
|
|
32
|
+
{t('booking.book.form.continuePlaceholder')}
|
|
42
33
|
</Button>
|
|
43
34
|
</View>
|
|
44
35
|
</View>
|
|
@@ -48,25 +39,24 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
48
39
|
if (step === 'enter-details') {
|
|
49
40
|
return (
|
|
50
41
|
<View gap="m">
|
|
51
|
-
<Title variant="secondary">
|
|
42
|
+
<Title variant="secondary">{t('booking.book.form.yourDetails')}</Title>
|
|
52
43
|
<Input
|
|
53
|
-
label=
|
|
44
|
+
label={t('booking.book.form.nameLabel')}
|
|
54
45
|
value={clientName}
|
|
55
46
|
onChange={setClientName}
|
|
56
|
-
placeholder=
|
|
47
|
+
placeholder={t('booking.book.form.namePlaceholder')}
|
|
57
48
|
/>
|
|
58
49
|
<Input
|
|
59
|
-
label=
|
|
50
|
+
label={t('booking.book.form.emailLabel')}
|
|
60
51
|
type="email"
|
|
61
52
|
value={clientEmail}
|
|
62
53
|
onChange={setClientEmail}
|
|
63
|
-
placeholder=
|
|
54
|
+
placeholder={t('booking.book.form.emailPlaceholder')}
|
|
64
55
|
/>
|
|
65
56
|
<View layout="row" gap="s">
|
|
66
|
-
<Button variant="ghost"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Confirm & Pay
|
|
57
|
+
<Button variant="ghost" onClick={() => setStep('pick-slot')}>{t('booking.book.form.back')}</Button>
|
|
58
|
+
<Button variant="primary" onClick={() => setStep('confirm')}>
|
|
59
|
+
{t('booking.book.form.confirmPay')}
|
|
70
60
|
</Button>
|
|
71
61
|
</View>
|
|
72
62
|
</View>
|
|
@@ -75,8 +65,8 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
75
65
|
|
|
76
66
|
return (
|
|
77
67
|
<View gap="m">
|
|
78
|
-
<Title variant="secondary">
|
|
79
|
-
<Text>
|
|
68
|
+
<Title variant="secondary">{t('booking.book.form.confirmed')}</Title>
|
|
69
|
+
<Text>{t('booking.book.form.checkEmail')}</Text>
|
|
80
70
|
</View>
|
|
81
71
|
)
|
|
82
72
|
}
|
package/src/Definition.js
CHANGED
|
@@ -2,28 +2,6 @@ export const Definition = {
|
|
|
2
2
|
id: 'booking',
|
|
3
3
|
title: 'Booking',
|
|
4
4
|
description: 'Services, availability windows, and client appointment management for consultants.',
|
|
5
|
-
|
|
6
|
-
id: 'booking',
|
|
7
|
-
enabled: true,
|
|
8
|
-
},
|
|
5
|
+
icon: 'calendar',
|
|
9
6
|
statuses: ['beta'],
|
|
10
|
-
actions: [
|
|
11
|
-
'booking.create-service',
|
|
12
|
-
'booking.update-service',
|
|
13
|
-
'booking.delete-service',
|
|
14
|
-
'booking.create-booking',
|
|
15
|
-
'booking.confirm-booking',
|
|
16
|
-
'booking.cancel-booking',
|
|
17
|
-
'booking.set-availability',
|
|
18
|
-
],
|
|
19
|
-
views: [
|
|
20
|
-
'booking.get-services',
|
|
21
|
-
'booking.get-bookings',
|
|
22
|
-
'booking.get-availability',
|
|
23
|
-
'booking.get-available-slots',
|
|
24
|
-
],
|
|
25
|
-
tasks: [
|
|
26
|
-
'booking.send-confirmation-email',
|
|
27
|
-
'booking.sync-stripe-payment-status',
|
|
28
|
-
],
|
|
29
7
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Title, Button, View, PageSection } from '@ossy/design-system'
|
|
3
|
+
|
|
4
|
+
const Cover = ({
|
|
5
|
+
title,
|
|
6
|
+
titleMaxWidth = '1100px',
|
|
7
|
+
text,
|
|
8
|
+
actions = [],
|
|
9
|
+
}) => (
|
|
10
|
+
<View
|
|
11
|
+
gap="l"
|
|
12
|
+
placeContent="center"
|
|
13
|
+
layout="column"
|
|
14
|
+
justifyContent="center"
|
|
15
|
+
style={{
|
|
16
|
+
height: '100%',
|
|
17
|
+
borderRadius: 'var(--space-m)',
|
|
18
|
+
padding: 'var(--space-xl) var(--space-s)',
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<View gap="m" alignItems="center">
|
|
22
|
+
<Title as="h1" variant="display" style={{ maxWidth: titleMaxWidth, textWrap: 'balance' }}>
|
|
23
|
+
{title}
|
|
24
|
+
</Title>
|
|
25
|
+
|
|
26
|
+
<Title as="h2" variant="title-tertiary" style={{ maxWidth: '800px' }}>
|
|
27
|
+
{text}
|
|
28
|
+
</Title>
|
|
29
|
+
</View>
|
|
30
|
+
|
|
31
|
+
<View gap="m" layout="row" justifyContent="center" style={{ flexWrap: 'wrap' }}>
|
|
32
|
+
{actions.map(({ label, ...props }) => (
|
|
33
|
+
<Button {...props} key={label}>
|
|
34
|
+
{label}
|
|
35
|
+
</Button>
|
|
36
|
+
))}
|
|
37
|
+
</View>
|
|
38
|
+
</View>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export const HeroCover = ({
|
|
42
|
+
title,
|
|
43
|
+
titleMaxWidth = '1100px',
|
|
44
|
+
text,
|
|
45
|
+
actions = [],
|
|
46
|
+
|
|
47
|
+
id = 'hero-cover',
|
|
48
|
+
surfaceAs = 'div',
|
|
49
|
+
maxWidth = 'l',
|
|
50
|
+
surface = 'hero',
|
|
51
|
+
...props
|
|
52
|
+
}) => {
|
|
53
|
+
const { style: passthroughStyle, ...pageSectionProps } = props
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<PageSection
|
|
57
|
+
id={id}
|
|
58
|
+
surfaceAs={surfaceAs}
|
|
59
|
+
maxWidth={maxWidth}
|
|
60
|
+
surface={surface}
|
|
61
|
+
{...pageSectionProps}
|
|
62
|
+
data-rounded
|
|
63
|
+
data-component="@ossy/website-ossy/cover"
|
|
64
|
+
style={{
|
|
65
|
+
backgroundAttachment: 'scroll',
|
|
66
|
+
boxShadow: 'inset 0 1px 0 color-mix(in srgb, var(--foreground) 6%, transparent)',
|
|
67
|
+
...passthroughStyle,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<style href="@ossy/website-ossy/cover" precedence="high">
|
|
71
|
+
{`
|
|
72
|
+
[data-component="@ossy/website-ossy/cover"] {
|
|
73
|
+
gap: var(--space-m);
|
|
74
|
+
min-height: min(42vh, 420px);
|
|
75
|
+
height: auto;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
[data-rounded] {
|
|
79
|
+
border-radius: var(--space-m);
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media (min-width: 900px) {
|
|
84
|
+
[data-component="@ossy/website-ossy/cover"] {
|
|
85
|
+
min-height: min(46vh, 520px);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`}
|
|
89
|
+
</style>
|
|
90
|
+
|
|
91
|
+
<Cover
|
|
92
|
+
title={title}
|
|
93
|
+
titleMaxWidth={titleMaxWidth}
|
|
94
|
+
text={text}
|
|
95
|
+
actions={actions}
|
|
96
|
+
/>
|
|
97
|
+
</PageSection>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Title,
|
|
4
|
+
Text,
|
|
5
|
+
View,
|
|
6
|
+
Icon,
|
|
7
|
+
PageSection,
|
|
8
|
+
MarkdownViewer,
|
|
9
|
+
useLocale,
|
|
10
|
+
} from '@ossy/design-system'
|
|
11
|
+
import { HeroCover } from './HeroCover.jsx'
|
|
12
|
+
|
|
13
|
+
export default ({
|
|
14
|
+
cover,
|
|
15
|
+
features,
|
|
16
|
+
}) => {
|
|
17
|
+
const { t } = useLocale()
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<PageSection maxWidth="xl" gap="l">
|
|
21
|
+
<View gap="l" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
|
|
22
|
+
<HeroCover {...cover} />
|
|
23
|
+
|
|
24
|
+
<View gap="m" inset="s">
|
|
25
|
+
<Title variant="secondary">
|
|
26
|
+
{t('booking.sales.overview')}
|
|
27
|
+
</Title>
|
|
28
|
+
|
|
29
|
+
<View gap="m" layout="row-wrap">
|
|
30
|
+
{features.map(x => (
|
|
31
|
+
<View
|
|
32
|
+
key={x.title}
|
|
33
|
+
roundness="m"
|
|
34
|
+
surface="primary"
|
|
35
|
+
inset="m"
|
|
36
|
+
gap="m"
|
|
37
|
+
style={{
|
|
38
|
+
width: 200,
|
|
39
|
+
border: '1px solid var(--separator)',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<View justifyContent="center" alignItems="center">
|
|
43
|
+
<Icon name={x.icon} size="m" />
|
|
44
|
+
</View>
|
|
45
|
+
<Text variant="small" style={{ fontWeight: 'bold', textAlign: 'center' }}>{x.title}</Text>
|
|
46
|
+
<Text variant="small" style={{ textAlign: 'center' }}>{x.text}</Text>
|
|
47
|
+
</View>
|
|
48
|
+
))}
|
|
49
|
+
</View>
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
<View gap="m" inset="s">
|
|
53
|
+
<Title variant="secondary">
|
|
54
|
+
{t('booking.sales.features')}
|
|
55
|
+
</Title>
|
|
56
|
+
|
|
57
|
+
{features.map(x => (
|
|
58
|
+
<View key={x.title} roundness="m" gap="m" inset="l" style={{ border: '1px solid var(--separator)' }}>
|
|
59
|
+
<View layout="row" gap="s" alignItems="center">
|
|
60
|
+
<View justifyContent="center" alignItems="center">
|
|
61
|
+
<Icon name={x.icon} size="m" />
|
|
62
|
+
</View>
|
|
63
|
+
<Title variant="tertiary">
|
|
64
|
+
{x.title}
|
|
65
|
+
</Title>
|
|
66
|
+
</View>
|
|
67
|
+
|
|
68
|
+
<Text>{x.text}</Text>
|
|
69
|
+
|
|
70
|
+
{x.content && <MarkdownViewer children={x.content} />}
|
|
71
|
+
</View>
|
|
72
|
+
))}
|
|
73
|
+
</View>
|
|
74
|
+
</View>
|
|
75
|
+
</PageSection>
|
|
76
|
+
)
|
|
77
|
+
}
|