@ossy/booking 1.15.5 → 1.15.7
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 +10 -6
- package/src/BookingForm.jsx +5 -5
- package/src/HeroCover.jsx +99 -0
- package/src/SalesSection.jsx +72 -0
- package/src/availability-setup.page.jsx +123 -340
- package/src/booking-card.component.jsx +67 -197
- package/src/booking-detail.page.jsx +102 -428
- package/src/bookings.page.jsx +36 -180
- package/src/cover.js +8 -0
- package/src/features.js +27 -0
- package/src/home.page.jsx +13 -0
- package/src/public-booking.page.jsx +163 -520
- package/src/service-card-slot.component.jsx +48 -0
- package/src/service-detail.component.jsx +38 -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.15.
|
|
4
|
+
"version": "1.15.7",
|
|
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.5.
|
|
18
|
-
"@ossy/event-store": "^1.7.
|
|
19
|
-
"@ossy/observability": "^1.7.
|
|
20
|
-
"@ossy/platform": "^1.38.
|
|
21
|
-
"@ossy/resources": "^1.11.
|
|
17
|
+
"@ossy/email": "^1.5.7",
|
|
18
|
+
"@ossy/event-store": "^1.7.7",
|
|
19
|
+
"@ossy/observability": "^1.7.7",
|
|
20
|
+
"@ossy/platform": "^1.38.7",
|
|
21
|
+
"@ossy/resources": "^1.11.7"
|
|
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": "f65df0cd6da864c314c4f424112e05e8e2dec0ff"
|
|
38
38
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { View, Title, Text, Button, Input
|
|
2
|
+
import { View, Title, Text, Button, Input } from '@ossy/design-system'
|
|
3
3
|
|
|
4
4
|
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
5
5
|
|
|
@@ -54,10 +54,14 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
54
54
|
roundness="m"
|
|
55
55
|
inset="s"
|
|
56
56
|
>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
|
58
|
+
<input
|
|
59
|
+
type="checkbox"
|
|
60
|
+
checked={days[idx].enabled}
|
|
61
|
+
onChange={e => update(idx, { enabled: e.target.checked })}
|
|
62
|
+
style={{ width: 16, height: 16, accentColor: 'var(--color-accent)' }}
|
|
63
|
+
/>
|
|
64
|
+
</label>
|
|
61
65
|
<Text weight="medium" style={{ width: '2.5rem', flexShrink: 0 }}>{label}</Text>
|
|
62
66
|
|
|
63
67
|
{days[idx].enabled ? (
|
|
@@ -93,7 +97,7 @@ export const AvailabilityEditor = ({ availability = [], onSave }) => {
|
|
|
93
97
|
</View>
|
|
94
98
|
|
|
95
99
|
{/* TODO: wire to booking.set-availability action */}
|
|
96
|
-
<Button variant="primary"
|
|
100
|
+
<Button variant="primary" onClick={handleSave}>Save availability</Button>
|
|
97
101
|
</View>
|
|
98
102
|
)
|
|
99
103
|
}
|
package/src/BookingForm.jsx
CHANGED
|
@@ -22,7 +22,7 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
22
22
|
<Title variant="secondary">Choose a service</Title>
|
|
23
23
|
{/* TODO: replace with real services loaded from API */}
|
|
24
24
|
<Text color="secondary">Loading services for workspace {workspaceId}…</Text>
|
|
25
|
-
<Button variant="secondary"
|
|
25
|
+
<Button variant="secondary" onClick={() => setStep('pick-slot')}>
|
|
26
26
|
Continue (placeholder)
|
|
27
27
|
</Button>
|
|
28
28
|
</View>
|
|
@@ -36,8 +36,8 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
36
36
|
{/* TODO: render a slot grid derived from availability minus existing bookings */}
|
|
37
37
|
<Text color="secondary">Available slots will appear here.</Text>
|
|
38
38
|
<View layout="row" gap="s">
|
|
39
|
-
<Button variant="ghost"
|
|
40
|
-
<Button variant="secondary"
|
|
39
|
+
<Button variant="ghost" onClick={() => setStep('pick-service')}>Back</Button>
|
|
40
|
+
<Button variant="secondary" onClick={() => setStep('enter-details')}>
|
|
41
41
|
Continue (placeholder)
|
|
42
42
|
</Button>
|
|
43
43
|
</View>
|
|
@@ -63,9 +63,9 @@ export const BookingForm = ({ workspaceId }) => {
|
|
|
63
63
|
placeholder="jane@example.com"
|
|
64
64
|
/>
|
|
65
65
|
<View layout="row" gap="s">
|
|
66
|
-
<Button variant="ghost"
|
|
66
|
+
<Button variant="ghost" onClick={() => setStep('pick-slot')}>Back</Button>
|
|
67
67
|
{/* TODO: submit booking action then redirect to Stripe */}
|
|
68
|
-
<Button variant="primary"
|
|
68
|
+
<Button variant="primary" onClick={() => setStep('confirm')}>
|
|
69
69
|
Confirm & Pay
|
|
70
70
|
</Button>
|
|
71
71
|
</View>
|
|
@@ -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,72 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Title,
|
|
4
|
+
Text,
|
|
5
|
+
View,
|
|
6
|
+
Icon,
|
|
7
|
+
PageSection,
|
|
8
|
+
MarkdownViewer,
|
|
9
|
+
} from '@ossy/design-system'
|
|
10
|
+
import { HeroCover } from './HeroCover.jsx'
|
|
11
|
+
|
|
12
|
+
export default ({
|
|
13
|
+
cover,
|
|
14
|
+
features,
|
|
15
|
+
}) => (
|
|
16
|
+
<PageSection maxWidth="xl" gap="l">
|
|
17
|
+
<View gap="l" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
|
|
18
|
+
<HeroCover {...cover} />
|
|
19
|
+
|
|
20
|
+
<View gap="m" inset="s">
|
|
21
|
+
<Title variant="secondary">
|
|
22
|
+
Översikt
|
|
23
|
+
</Title>
|
|
24
|
+
|
|
25
|
+
<View gap="m" layout="row-wrap">
|
|
26
|
+
{features.map(x => (
|
|
27
|
+
<View
|
|
28
|
+
key={x.title}
|
|
29
|
+
roundness="m"
|
|
30
|
+
surface="primary"
|
|
31
|
+
inset="m"
|
|
32
|
+
gap="m"
|
|
33
|
+
style={{
|
|
34
|
+
width: 200,
|
|
35
|
+
border: '1px solid var(--separator)',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<View justifyContent="center" alignItems="center">
|
|
39
|
+
<Icon name={x.icon} size="m" />
|
|
40
|
+
</View>
|
|
41
|
+
<Text variant="small" style={{ fontWeight: 'bold', textAlign: 'center' }}>{x.title}</Text>
|
|
42
|
+
<Text variant="small" style={{ textAlign: 'center' }}>{x.text}</Text>
|
|
43
|
+
</View>
|
|
44
|
+
))}
|
|
45
|
+
</View>
|
|
46
|
+
</View>
|
|
47
|
+
|
|
48
|
+
<View gap="m" inset="s">
|
|
49
|
+
<Title variant="secondary">
|
|
50
|
+
Funktioner
|
|
51
|
+
</Title>
|
|
52
|
+
|
|
53
|
+
{features.map(x => (
|
|
54
|
+
<View key={x.title} roundness="m" gap="m" inset="l" style={{ border: '1px solid var(--separator)' }}>
|
|
55
|
+
<View layout="row" gap="s" alignItems="center">
|
|
56
|
+
<View justifyContent="center" alignItems="center">
|
|
57
|
+
<Icon name={x.icon} size="m" />
|
|
58
|
+
</View>
|
|
59
|
+
<Title variant="tertiary">
|
|
60
|
+
{x.title}
|
|
61
|
+
</Title>
|
|
62
|
+
</View>
|
|
63
|
+
|
|
64
|
+
<Text>{x.text}</Text>
|
|
65
|
+
|
|
66
|
+
{x.content && <MarkdownViewer children={x.content} />}
|
|
67
|
+
</View>
|
|
68
|
+
))}
|
|
69
|
+
</View>
|
|
70
|
+
</View>
|
|
71
|
+
</PageSection>
|
|
72
|
+
)
|