@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
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { View, Title, Text } from '@ossy/design-system'
|
|
2
|
+
import { View, Title, Text, useLocale } from '@ossy/design-system'
|
|
3
3
|
import { useRouter } from '@ossy/router-react'
|
|
4
4
|
import { BookingForm } from './BookingForm.jsx'
|
|
5
5
|
|
|
6
6
|
export const metadata = {
|
|
7
7
|
id: 'booking/book',
|
|
8
|
-
// Public-facing — no auth required
|
|
9
8
|
public: true,
|
|
10
9
|
path: {
|
|
11
10
|
sv: '/boka/:workspaceId',
|
|
@@ -14,6 +13,7 @@ export const metadata = {
|
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
const BookingPage = () => {
|
|
16
|
+
const { t } = useLocale()
|
|
17
17
|
const router = useRouter()
|
|
18
18
|
const { workspaceId } = router.params
|
|
19
19
|
|
|
@@ -24,15 +24,12 @@ const BookingPage = () => {
|
|
|
24
24
|
inset="s"
|
|
25
25
|
style={{ minHeight: '100dvh' }}
|
|
26
26
|
>
|
|
27
|
-
<title>Book a session</title>
|
|
28
|
-
|
|
29
27
|
<View inset="m" roundness="l" surface="primary" slot="content" gap="m">
|
|
30
28
|
<View gap="xs">
|
|
31
|
-
<Title>
|
|
32
|
-
<Text>
|
|
29
|
+
<Title>{t('booking.book.title')}</Title>
|
|
30
|
+
<Text>{t('booking.book.description')}</Text>
|
|
33
31
|
</View>
|
|
34
32
|
|
|
35
|
-
{/* TODO: load services for workspaceId, let client pick one */}
|
|
36
33
|
<BookingForm workspaceId={workspaceId} />
|
|
37
34
|
</View>
|
|
38
35
|
</View>
|
package/src/bookings.page.jsx
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from 'react'
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import { View, Title, Text, Tabs, Alert, useLocale } from '@ossy/design-system'
|
|
3
|
+
import { Definition } from './Definition.js'
|
|
2
4
|
import { BookingCard } from './booking-card.component.jsx'
|
|
3
5
|
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Tokens
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
const C = {
|
|
8
|
-
bg: '#fafafa',
|
|
9
|
-
surface: '#ffffff',
|
|
10
|
-
border: '#e4e4e7',
|
|
11
|
-
primary: '#111111',
|
|
12
|
-
secondary: '#52525b',
|
|
13
|
-
muted: '#a1a1aa',
|
|
14
|
-
tabActive: '#111111',
|
|
15
|
-
tabActiveBg: '#f4f4f5',
|
|
16
|
-
tabText: '#52525b',
|
|
17
|
-
errorText: '#dc2626',
|
|
18
|
-
}
|
|
19
|
-
const FONT = 'system-ui, -apple-system, sans-serif'
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Page metadata
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
6
|
export const metadata = {
|
|
25
7
|
id: 'booking/bookings',
|
|
26
8
|
path: {
|
|
@@ -29,131 +11,34 @@ export const metadata = {
|
|
|
29
11
|
},
|
|
30
12
|
}
|
|
31
13
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
const TABS = [
|
|
36
|
-
{ id: 'pending', label: 'Förfrågningar' },
|
|
37
|
-
{ id: 'confirmed', label: 'Bekräftade' },
|
|
38
|
-
{ id: 'all', label: 'Alla' },
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
function TabBar({ activeId, onChange, counts }) {
|
|
42
|
-
return (
|
|
43
|
-
<div style={{ display: 'flex', gap: 4, borderBottom: `1px solid ${C.border}`, marginBottom: 20 }}>
|
|
44
|
-
{TABS.map(tab => {
|
|
45
|
-
const isActive = tab.id === activeId
|
|
46
|
-
const count = counts?.[tab.id]
|
|
47
|
-
return (
|
|
48
|
-
<button
|
|
49
|
-
key={tab.id}
|
|
50
|
-
onClick={() => onChange(tab.id)}
|
|
51
|
-
style={{
|
|
52
|
-
background: 'none',
|
|
53
|
-
border: 'none',
|
|
54
|
-
borderBottom: isActive ? `2px solid ${C.primary}` : '2px solid transparent',
|
|
55
|
-
padding: '10px 14px',
|
|
56
|
-
marginBottom: -1,
|
|
57
|
-
fontFamily: FONT,
|
|
58
|
-
fontSize: 13,
|
|
59
|
-
fontWeight: isActive ? 600 : 400,
|
|
60
|
-
color: isActive ? C.primary : C.secondary,
|
|
61
|
-
cursor: 'pointer',
|
|
62
|
-
display: 'flex',
|
|
63
|
-
alignItems: 'center',
|
|
64
|
-
gap: 6,
|
|
65
|
-
whiteSpace: 'nowrap',
|
|
66
|
-
transition: 'color 0.1s',
|
|
67
|
-
}}
|
|
68
|
-
>
|
|
69
|
-
{tab.label}
|
|
70
|
-
{count != null && count > 0 && (
|
|
71
|
-
<span
|
|
72
|
-
style={{
|
|
73
|
-
background: isActive ? C.primary : C.border,
|
|
74
|
-
color: isActive ? '#fff' : C.secondary,
|
|
75
|
-
borderRadius: 999,
|
|
76
|
-
fontSize: 10,
|
|
77
|
-
fontWeight: 700,
|
|
78
|
-
padding: '1px 6px',
|
|
79
|
-
lineHeight: 1.5,
|
|
80
|
-
}}
|
|
81
|
-
>
|
|
82
|
-
{count}
|
|
83
|
-
</span>
|
|
84
|
-
)}
|
|
85
|
-
</button>
|
|
86
|
-
)
|
|
87
|
-
})}
|
|
88
|
-
</div>
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// Empty state
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
function EmptyState({ tab }) {
|
|
96
|
-
const messages = {
|
|
97
|
-
pending: { icon: '📭', heading: 'Inga förfrågningar', body: 'Du har inga väntande bokningsförfrågningar just nu.' },
|
|
98
|
-
confirmed: { icon: '📅', heading: 'Inga bekräftade bokningar', body: 'Bekräftade bokningar visas här.' },
|
|
99
|
-
all: { icon: '🗂', heading: 'Inga bokningar', body: 'Bokningar från dina klienter visas här.' },
|
|
100
|
-
}
|
|
101
|
-
const m = messages[tab] ?? messages.all
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div
|
|
105
|
-
style={{
|
|
106
|
-
display: 'flex',
|
|
107
|
-
flexDirection: 'column',
|
|
108
|
-
alignItems: 'center',
|
|
109
|
-
justifyContent: 'center',
|
|
110
|
-
padding: '48px 24px',
|
|
111
|
-
color: C.muted,
|
|
112
|
-
fontFamily: FONT,
|
|
113
|
-
textAlign: 'center',
|
|
114
|
-
gap: 8,
|
|
115
|
-
}}
|
|
116
|
-
>
|
|
117
|
-
<span style={{ fontSize: 32 }}>{m.icon}</span>
|
|
118
|
-
<span style={{ fontSize: 15, fontWeight: 600, color: C.secondary }}>{m.heading}</span>
|
|
119
|
-
<span style={{ fontSize: 13 }}>{m.body}</span>
|
|
120
|
-
</div>
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Spinner
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
function Spinner() {
|
|
128
|
-
return (
|
|
129
|
-
<>
|
|
130
|
-
<style href="booking/spinner" precedence="low">
|
|
131
|
-
{`@keyframes booking-spin { to { transform: rotate(360deg) } }`}
|
|
132
|
-
</style>
|
|
133
|
-
<div
|
|
134
|
-
style={{
|
|
135
|
-
width: 22,
|
|
136
|
-
height: 22,
|
|
137
|
-
border: '3px solid #e4e4e7',
|
|
138
|
-
borderTopColor: C.primary,
|
|
139
|
-
borderRadius: '50%',
|
|
140
|
-
animation: 'booking-spin 0.7s linear infinite',
|
|
141
|
-
margin: '40px auto',
|
|
142
|
-
}}
|
|
143
|
-
/>
|
|
144
|
-
</>
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
// Page
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
export default function BookingsPage({ workspaceId }) {
|
|
14
|
+
export default function BookingsPage () {
|
|
15
|
+
const { t } = useLocale()
|
|
152
16
|
const [activeTab, setActiveTab] = useState('pending')
|
|
153
17
|
const [bookings, setBookings] = useState([])
|
|
154
18
|
const [loading, setLoading] = useState(true)
|
|
155
19
|
const [error, setError] = useState(null)
|
|
156
20
|
|
|
21
|
+
const TABS = useMemo(() => [
|
|
22
|
+
{ id: 'pending', label: t('booking.bookings.tab.pending') },
|
|
23
|
+
{ id: 'confirmed', label: t('booking.bookings.tab.confirmed') },
|
|
24
|
+
{ id: 'all', label: t('booking.bookings.tab.all') },
|
|
25
|
+
], [t])
|
|
26
|
+
|
|
27
|
+
const EMPTY_MESSAGES = useMemo(() => ({
|
|
28
|
+
pending: {
|
|
29
|
+
heading: t('booking.bookings.empty.pending.heading'),
|
|
30
|
+
body: t('booking.bookings.empty.pending.body'),
|
|
31
|
+
},
|
|
32
|
+
confirmed: {
|
|
33
|
+
heading: t('booking.bookings.empty.confirmed.heading'),
|
|
34
|
+
body: t('booking.bookings.empty.confirmed.body'),
|
|
35
|
+
},
|
|
36
|
+
all: {
|
|
37
|
+
heading: t('booking.bookings.empty.all.heading'),
|
|
38
|
+
body: t('booking.bookings.empty.all.body'),
|
|
39
|
+
},
|
|
40
|
+
}), [t])
|
|
41
|
+
|
|
157
42
|
const loadBookings = useCallback(async () => {
|
|
158
43
|
setLoading(true)
|
|
159
44
|
setError(null)
|
|
@@ -165,7 +50,7 @@ export default function BookingsPage({ workspaceId }) {
|
|
|
165
50
|
})
|
|
166
51
|
if (!res.ok) {
|
|
167
52
|
const data = await res.json().catch(() => ({}))
|
|
168
|
-
throw new Error(data?.message ??
|
|
53
|
+
throw new Error(data?.message ?? t('booking.bookings.errorStatus', { status: res.status }))
|
|
169
54
|
}
|
|
170
55
|
const data = await res.json()
|
|
171
56
|
setBookings(data)
|
|
@@ -174,20 +59,16 @@ export default function BookingsPage({ workspaceId }) {
|
|
|
174
59
|
} finally {
|
|
175
60
|
setLoading(false)
|
|
176
61
|
}
|
|
177
|
-
}, [])
|
|
62
|
+
}, [t])
|
|
178
63
|
|
|
179
64
|
useEffect(() => {
|
|
180
65
|
loadBookings()
|
|
181
66
|
}, [loadBookings])
|
|
182
67
|
|
|
183
|
-
// Filtered views
|
|
184
68
|
const pending = bookings.filter(b => b.status === 'pending')
|
|
185
69
|
const confirmed = bookings.filter(b => b.status === 'confirmed')
|
|
186
70
|
const visible = activeTab === 'pending' ? pending : activeTab === 'confirmed' ? confirmed : bookings
|
|
187
71
|
|
|
188
|
-
const counts = { pending: pending.length, confirmed: confirmed.length, all: bookings.length }
|
|
189
|
-
|
|
190
|
-
// Actions
|
|
191
72
|
const handleConfirm = async (booking) => {
|
|
192
73
|
const res = await fetch('/actions/booking/confirm', {
|
|
193
74
|
method: 'POST',
|
|
@@ -196,7 +77,7 @@ export default function BookingsPage({ workspaceId }) {
|
|
|
196
77
|
})
|
|
197
78
|
if (!res.ok) {
|
|
198
79
|
const data = await res.json().catch(() => ({}))
|
|
199
|
-
throw new Error(data?.message ?? '
|
|
80
|
+
throw new Error(data?.message ?? t('booking.bookings.errorConfirm'))
|
|
200
81
|
}
|
|
201
82
|
await loadBookings()
|
|
202
83
|
}
|
|
@@ -209,7 +90,7 @@ export default function BookingsPage({ workspaceId }) {
|
|
|
209
90
|
})
|
|
210
91
|
if (!res.ok) {
|
|
211
92
|
const data = await res.json().catch(() => ({}))
|
|
212
|
-
throw new Error(data?.message ?? '
|
|
93
|
+
throw new Error(data?.message ?? t('booking.bookings.errorDecline'))
|
|
213
94
|
}
|
|
214
95
|
await loadBookings()
|
|
215
96
|
}
|
|
@@ -222,7 +103,7 @@ export default function BookingsPage({ workspaceId }) {
|
|
|
222
103
|
})
|
|
223
104
|
if (!res.ok) {
|
|
224
105
|
const data = await res.json().catch(() => ({}))
|
|
225
|
-
throw new Error(data?.message ?? '
|
|
106
|
+
throw new Error(data?.message ?? t('booking.bookings.errorCancel'))
|
|
226
107
|
}
|
|
227
108
|
await loadBookings()
|
|
228
109
|
}
|
|
@@ -231,56 +112,41 @@ export default function BookingsPage({ workspaceId }) {
|
|
|
231
112
|
window.location.href = `/bokningar/${booking.id}`
|
|
232
113
|
}
|
|
233
114
|
|
|
115
|
+
const empty = EMPTY_MESSAGES[activeTab] ?? EMPTY_MESSAGES.all
|
|
116
|
+
|
|
234
117
|
return (
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
overflowY: 'auto',
|
|
240
|
-
background: C.bg,
|
|
241
|
-
padding: 'var(--space-m, 16px) var(--space-l, 24px)',
|
|
242
|
-
boxSizing: 'border-box',
|
|
243
|
-
}}
|
|
118
|
+
<View
|
|
119
|
+
gap="m"
|
|
120
|
+
surface="primary"
|
|
121
|
+
style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
|
|
244
122
|
>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
</
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
background: C.surface,
|
|
259
|
-
border: `1px solid ${C.border}`,
|
|
260
|
-
borderRadius: 12,
|
|
261
|
-
padding: '20px 24px',
|
|
262
|
-
}}
|
|
263
|
-
>
|
|
264
|
-
<TabBar activeId={activeTab} onChange={setActiveTab} counts={counts} />
|
|
123
|
+
<View inset="s" gap="s">
|
|
124
|
+
<Title>{Definition.title} — {t('booking.bookings.titleSuffix')}</Title>
|
|
125
|
+
<Text style={{ maxWidth: '400px' }}>
|
|
126
|
+
{t('booking.bookings.description')}
|
|
127
|
+
</Text>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
<View gap="m" inset="s" surface="secondary" roundness="m">
|
|
131
|
+
<Tabs
|
|
132
|
+
tabs={TABS}
|
|
133
|
+
activeTabId={activeTab}
|
|
134
|
+
onTabClick={(_, tab) => setActiveTab(tab.id)}
|
|
135
|
+
/>
|
|
265
136
|
|
|
266
137
|
{loading ? (
|
|
267
|
-
<
|
|
138
|
+
<Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>
|
|
139
|
+
{t('booking.bookings.loading')}
|
|
140
|
+
</Text>
|
|
268
141
|
) : error ? (
|
|
269
|
-
<
|
|
270
|
-
style={{
|
|
271
|
-
padding: '24px',
|
|
272
|
-
fontSize: 14,
|
|
273
|
-
color: C.errorText,
|
|
274
|
-
textAlign: 'center',
|
|
275
|
-
fontFamily: FONT,
|
|
276
|
-
}}
|
|
277
|
-
>
|
|
278
|
-
{error}
|
|
279
|
-
</div>
|
|
142
|
+
<Alert variant="danger">{error}</Alert>
|
|
280
143
|
) : visible.length === 0 ? (
|
|
281
|
-
<
|
|
144
|
+
<View gap="xs" alignItems="center" inset="l">
|
|
145
|
+
<Title variant="tertiary">{empty.heading}</Title>
|
|
146
|
+
<Text color="secondary">{empty.body}</Text>
|
|
147
|
+
</View>
|
|
282
148
|
) : (
|
|
283
|
-
<
|
|
149
|
+
<View gap="s">
|
|
284
150
|
{visible.map(booking => (
|
|
285
151
|
<BookingCard
|
|
286
152
|
key={booking.id}
|
|
@@ -291,9 +157,9 @@ export default function BookingsPage({ workspaceId }) {
|
|
|
291
157
|
onView={handleView}
|
|
292
158
|
/>
|
|
293
159
|
))}
|
|
294
|
-
</
|
|
160
|
+
</View>
|
|
295
161
|
)}
|
|
296
|
-
</
|
|
297
|
-
</
|
|
162
|
+
</View>
|
|
163
|
+
</View>
|
|
298
164
|
)
|
|
299
165
|
}
|
package/src/cover.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const cover = {
|
|
2
|
+
title: 'Låt kunder boka tid direkt',
|
|
3
|
+
text: 'Dela en länk. Kunder väljer tid. Du fokuserar på jobbet.',
|
|
4
|
+
actions: [
|
|
5
|
+
{ label: 'Kom igång gratis', variant: 'cta', href: '/sign-up' },
|
|
6
|
+
{ label: 'Se ett exempel', variant: 'secondary', href: '/boka/demo' },
|
|
7
|
+
],
|
|
8
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
{
|
|
2
|
+
"booking.home.documentTitle": "Booking calendar – Ossy",
|
|
3
|
+
"booking.home.cover.title": "Let clients book time directly",
|
|
4
|
+
"booking.home.cover.text": "Share a link. Clients pick a slot. You focus on the work.",
|
|
5
|
+
"booking.home.cover.ctaPrimary": "Get started free",
|
|
6
|
+
"booking.home.cover.ctaSecondary": "See an example",
|
|
7
|
+
"booking.home.features.page.title": "Your booking page",
|
|
8
|
+
"booking.home.features.page.text": "A personal page at ossy.se/booking/your-name. Share the link with clients — that's all it takes.",
|
|
9
|
+
"booking.home.features.confirmations.title": "Automatic confirmations",
|
|
10
|
+
"booking.home.features.confirmations.text": "Clients get a confirmation with a calendar invite instantly. No manual steps on your side.",
|
|
11
|
+
"booking.home.features.availability.title": "Manage your time",
|
|
12
|
+
"booking.home.features.availability.text": "Set when you're available. We only show open slots — never double-booked.",
|
|
13
|
+
"booking.home.features.email.title": "Email ping-pong",
|
|
14
|
+
"booking.home.features.email.text": "Skip days of back-and-forth just to find a shared slot.",
|
|
15
|
+
"booking.home.features.doubleBook.title": "No double bookings",
|
|
16
|
+
"booking.home.features.doubleBook.text": "One calendar here, one link there — we keep track so the same slot isn't booked twice.",
|
|
17
|
+
"booking.sales.overview": "Overview",
|
|
18
|
+
"booking.sales.features": "Features",
|
|
19
|
+
"booking.bookings.documentTitle": "Bookings",
|
|
20
|
+
"booking.bookings.titleSuffix": "Bookings",
|
|
21
|
+
"booking.bookings.description": "Manage booking requests and confirmed meetings.",
|
|
22
|
+
"booking.bookings.tab.pending": "Requests",
|
|
23
|
+
"booking.bookings.tab.confirmed": "Confirmed",
|
|
24
|
+
"booking.bookings.tab.all": "All",
|
|
25
|
+
"booking.bookings.empty.pending.heading": "No requests",
|
|
26
|
+
"booking.bookings.empty.pending.body": "You have no pending booking requests right now.",
|
|
27
|
+
"booking.bookings.empty.confirmed.heading": "No confirmed bookings",
|
|
28
|
+
"booking.bookings.empty.confirmed.body": "Confirmed bookings appear here.",
|
|
29
|
+
"booking.bookings.empty.all.heading": "No bookings",
|
|
30
|
+
"booking.bookings.empty.all.body": "Bookings from your clients appear here.",
|
|
31
|
+
"booking.bookings.loading": "Loading…",
|
|
32
|
+
"booking.bookings.errorStatus": "Error {status}",
|
|
33
|
+
"booking.bookings.errorConfirm": "Confirmation failed",
|
|
34
|
+
"booking.bookings.errorDecline": "Decline failed",
|
|
35
|
+
"booking.bookings.errorCancel": "Cancellation failed",
|
|
36
|
+
"booking.bookingDetail.documentTitle": "Booking details",
|
|
37
|
+
"booking.bookingDetail.back": "← All bookings",
|
|
38
|
+
"booking.bookingDetail.loading": "Loading…",
|
|
39
|
+
"booking.bookingDetail.notFound": "Booking not found",
|
|
40
|
+
"booking.bookingDetail.errorStatus": "Error {status}",
|
|
41
|
+
"booking.bookingDetail.status.pending": "Awaiting confirmation",
|
|
42
|
+
"booking.bookingDetail.status.confirmed": "Confirmed",
|
|
43
|
+
"booking.bookingDetail.status.cancelled": "Cancelled",
|
|
44
|
+
"booking.bookingDetail.label.dateTime": "Date and time",
|
|
45
|
+
"booking.bookingDetail.label.duration": "Duration",
|
|
46
|
+
"booking.bookingDetail.label.message": "Message",
|
|
47
|
+
"booking.bookingDetail.durationMinutes": "{minutes} minutes",
|
|
48
|
+
"booking.bookingDetail.durationHours": "{hours} hour",
|
|
49
|
+
"booking.bookingDetail.durationHoursMinutes": "{hours} hour {minutes} min",
|
|
50
|
+
"booking.bookingDetail.manageRequest": "Manage request",
|
|
51
|
+
"booking.bookingDetail.manageBooking": "Manage booking",
|
|
52
|
+
"booking.bookingDetail.confirm": "Confirm booking",
|
|
53
|
+
"booking.bookingDetail.confirming": "Confirming…",
|
|
54
|
+
"booking.bookingDetail.errorConfirm": "Confirmation failed",
|
|
55
|
+
"booking.bookingDetail.decline": "Decline request",
|
|
56
|
+
"booking.bookingDetail.declineTitle": "Decline booking request",
|
|
57
|
+
"booking.bookingDetail.declinePlaceholder": "E.g. Unfortunately the requested time is no longer available…",
|
|
58
|
+
"booking.bookingDetail.declineReasonHint": "Reason (optional — sent to the client)",
|
|
59
|
+
"booking.bookingDetail.declineConfirm": "Confirm decline",
|
|
60
|
+
"booking.bookingDetail.declining": "Declining…",
|
|
61
|
+
"booking.bookingDetail.errorDecline": "Decline failed",
|
|
62
|
+
"booking.bookingDetail.cancelMeeting": "Cancel meeting",
|
|
63
|
+
"booking.bookingDetail.cancelTitle": "Cancel confirmed booking",
|
|
64
|
+
"booking.bookingDetail.cancelBody": "The client will be notified by email. This action cannot be undone.",
|
|
65
|
+
"booking.bookingDetail.cancelConfirm": "Confirm cancellation",
|
|
66
|
+
"booking.bookingDetail.cancelling": "Cancelling…",
|
|
67
|
+
"booking.bookingDetail.errorCancel": "Cancellation failed",
|
|
68
|
+
"booking.bookingDetail.cancelButton": "Cancel",
|
|
69
|
+
"booking.availability.documentTitle": "Availability",
|
|
70
|
+
"booking.availability.titleSuffix": "Availability",
|
|
71
|
+
"booking.availability.description": "Set your weekly availability. Clients can only book slots within these windows.",
|
|
72
|
+
"booking.availabilitySetup.documentTitle": "Availability",
|
|
73
|
+
"booking.availabilitySetup.title": "Availability",
|
|
74
|
+
"booking.availabilitySetup.description": "Set when you are available for bookings. Clients can only book times within these windows.",
|
|
75
|
+
"booking.availabilitySetup.loading": "Loading…",
|
|
76
|
+
"booking.availabilitySetup.weeklySchedule": "Weekly schedule",
|
|
77
|
+
"booking.availabilitySetup.day.monday": "Monday",
|
|
78
|
+
"booking.availabilitySetup.day.tuesday": "Tuesday",
|
|
79
|
+
"booking.availabilitySetup.day.wednesday": "Wednesday",
|
|
80
|
+
"booking.availabilitySetup.day.thursday": "Thursday",
|
|
81
|
+
"booking.availabilitySetup.day.friday": "Friday",
|
|
82
|
+
"booking.availabilitySetup.day.saturday": "Saturday",
|
|
83
|
+
"booking.availabilitySetup.day.sunday": "Sunday",
|
|
84
|
+
"booking.availabilitySetup.unavailable": "Unavailable",
|
|
85
|
+
"booking.availabilitySetup.sessionDuration": "Session length",
|
|
86
|
+
"booking.availabilitySetup.sessionDurationHint": "Choose session lengths clients can select.",
|
|
87
|
+
"booking.availabilitySetup.durationMin": "{minutes} min",
|
|
88
|
+
"booking.availabilitySetup.bufferLabel": "Buffer time between bookings",
|
|
89
|
+
"booking.availabilitySetup.bufferNone": "No buffer",
|
|
90
|
+
"booking.availabilitySetup.bufferMinutes": "{minutes} minutes",
|
|
91
|
+
"booking.availabilitySetup.bufferHint": "Automatic pause added after each booking.",
|
|
92
|
+
"booking.availabilitySetup.timezone": "Time zone",
|
|
93
|
+
"booking.availabilitySetup.save": "Save availability",
|
|
94
|
+
"booking.availabilitySetup.saving": "Saving…",
|
|
95
|
+
"booking.availabilitySetup.errorSave": "Could not save ({status})",
|
|
96
|
+
"booking.availabilitySetup.savedTitle": "Availability saved",
|
|
97
|
+
"booking.availabilitySetup.savedBody": "Your booking link is ready to share with clients.",
|
|
98
|
+
"booking.availabilitySetup.copyLink": "Copy link",
|
|
99
|
+
"booking.availabilitySetup.copied": "Copied!",
|
|
100
|
+
"booking.services.documentTitle": "Services",
|
|
101
|
+
"booking.services.titleSuffix": "Services",
|
|
102
|
+
"booking.services.description": "Define what you offer — name, duration, and price. Clients will see these when booking.",
|
|
103
|
+
"booking.services.addService": "Add service",
|
|
104
|
+
"booking.services.yourServices": "Your services",
|
|
105
|
+
"booking.services.example.introCall.name": "Intro call",
|
|
106
|
+
"booking.services.example.introCall.description": "A free 30-minute intro to see if we're a good fit.",
|
|
107
|
+
"booking.services.example.strategy.name": "Strategy session",
|
|
108
|
+
"booking.services.example.strategy.description": "Deep-dive strategy session.",
|
|
109
|
+
"booking.book.documentTitle": "Book a session",
|
|
110
|
+
"booking.book.title": "Book a session",
|
|
111
|
+
"booking.book.description": "Pick a time that works for you and confirm your appointment.",
|
|
112
|
+
"booking.book.form.chooseService": "Choose a service",
|
|
113
|
+
"booking.book.form.loadingServices": "Loading services for workspace {workspaceId}…",
|
|
114
|
+
"booking.book.form.continuePlaceholder": "Continue (placeholder)",
|
|
115
|
+
"booking.book.form.pickTime": "Pick a time",
|
|
116
|
+
"booking.book.form.slotsPlaceholder": "Available slots will appear here.",
|
|
117
|
+
"booking.book.form.back": "Back",
|
|
118
|
+
"booking.book.form.yourDetails": "Your details",
|
|
119
|
+
"booking.book.form.nameLabel": "Name",
|
|
120
|
+
"booking.book.form.namePlaceholder": "Jane Smith",
|
|
121
|
+
"booking.book.form.emailLabel": "Email",
|
|
122
|
+
"booking.book.form.emailPlaceholder": "jane@example.com",
|
|
123
|
+
"booking.book.form.confirmPay": "Confirm & Pay",
|
|
124
|
+
"booking.book.form.confirmed": "Booking confirmed!",
|
|
125
|
+
"booking.book.form.checkEmail": "Check your email for a confirmation.",
|
|
126
|
+
"booking.availabilityEditor.save": "Save availability",
|
|
127
|
+
"booking.availabilityEditor.unavailable": "Unavailable",
|
|
128
|
+
"booking.availabilityEditor.to": "to",
|
|
129
|
+
"booking.availabilityEditor.buffer": "Buffer",
|
|
130
|
+
"booking.availabilityEditor.min": "min",
|
|
131
|
+
"booking.availabilityEditor.day.sun": "Sun",
|
|
132
|
+
"booking.availabilityEditor.day.mon": "Mon",
|
|
133
|
+
"booking.availabilityEditor.day.tue": "Tue",
|
|
134
|
+
"booking.availabilityEditor.day.wed": "Wed",
|
|
135
|
+
"booking.availabilityEditor.day.thu": "Thu",
|
|
136
|
+
"booking.availabilityEditor.day.fri": "Fri",
|
|
137
|
+
"booking.availabilityEditor.day.sat": "Sat",
|
|
138
|
+
"publicBooking.documentTitle": "Book an appointment",
|
|
139
|
+
"publicBooking.title": "Book an appointment",
|
|
140
|
+
"publicBooking.description": "Choose a date and time that works for you.",
|
|
141
|
+
"publicBooking.loadingSlots": "Loading times…",
|
|
142
|
+
"publicBooking.slotsError": "Could not load available times.",
|
|
143
|
+
"publicBooking.noSlots": "No available times for the selected date.",
|
|
144
|
+
"publicBooking.back": "← Back",
|
|
145
|
+
"publicBooking.selectedTime": "Selected time: {datetime}",
|
|
146
|
+
"publicBooking.durationMin": "({minutes} min)",
|
|
147
|
+
"publicBooking.continue": "Continue →",
|
|
148
|
+
"publicBooking.pickDate": "Select a highlighted date in the calendar",
|
|
149
|
+
"publicBooking.form.name": "Your name",
|
|
150
|
+
"publicBooking.form.namePlaceholder": "Anna Svensson",
|
|
151
|
+
"publicBooking.form.email": "Email address",
|
|
152
|
+
"publicBooking.form.emailPlaceholder": "anna@example.com",
|
|
153
|
+
"publicBooking.form.message": "Message (optional)",
|
|
154
|
+
"publicBooking.form.messagePlaceholder": "Tell us what you'd like to discuss…",
|
|
155
|
+
"publicBooking.form.submit": "Send request",
|
|
156
|
+
"publicBooking.form.submitting": "Sending…",
|
|
157
|
+
"publicBooking.form.error": "Booking failed ({status})",
|
|
158
|
+
"publicBooking.pending.title": "Your request has been sent!",
|
|
159
|
+
"publicBooking.pending.body": "The consultant will confirm shortly. You will receive an email with a calendar invite once the booking is confirmed.",
|
|
160
|
+
"publicBooking.pending.requestedTime": "Requested time",
|
|
161
|
+
"publicBooking.pending.duration": "{minutes} minutes",
|
|
162
|
+
"publicBooking.month.january": "January",
|
|
163
|
+
"publicBooking.month.february": "February",
|
|
164
|
+
"publicBooking.month.march": "March",
|
|
165
|
+
"publicBooking.month.april": "April",
|
|
166
|
+
"publicBooking.month.may": "May",
|
|
167
|
+
"publicBooking.month.june": "June",
|
|
168
|
+
"publicBooking.month.july": "July",
|
|
169
|
+
"publicBooking.month.august": "August",
|
|
170
|
+
"publicBooking.month.september": "September",
|
|
171
|
+
"publicBooking.month.october": "October",
|
|
172
|
+
"publicBooking.month.november": "November",
|
|
173
|
+
"publicBooking.month.december": "December",
|
|
174
|
+
"publicBooking.weekday.sun": "Sun",
|
|
175
|
+
"publicBooking.weekday.mon": "Mon",
|
|
176
|
+
"publicBooking.weekday.tue": "Tue",
|
|
177
|
+
"publicBooking.weekday.wed": "Wed",
|
|
178
|
+
"publicBooking.weekday.thu": "Thu",
|
|
179
|
+
"publicBooking.weekday.fri": "Fri",
|
|
180
|
+
"publicBooking.weekday.sat": "Sat",
|
|
181
|
+
"publicBooking.weekdayLong.sun": "Sunday",
|
|
182
|
+
"publicBooking.weekdayLong.mon": "Monday",
|
|
183
|
+
"publicBooking.weekdayLong.tue": "Tuesday",
|
|
184
|
+
"publicBooking.weekdayLong.wed": "Wednesday",
|
|
185
|
+
"publicBooking.weekdayLong.thu": "Thursday",
|
|
186
|
+
"publicBooking.weekdayLong.fri": "Friday",
|
|
187
|
+
"publicBooking.weekdayLong.sat": "Saturday",
|
|
188
|
+
"booking.card.status.pending": "Pending",
|
|
189
|
+
"booking.card.status.confirmed": "Confirmed",
|
|
190
|
+
"booking.card.status.cancelled": "Cancelled",
|
|
191
|
+
"booking.card.view": "View →",
|
|
192
|
+
"booking.card.confirm": "Confirm",
|
|
193
|
+
"booking.card.decline": "Decline",
|
|
194
|
+
"booking.card.cancel": "Cancel",
|
|
195
|
+
"booking.card.waiting": "Waiting…",
|
|
196
|
+
"booking.card.errorGeneric": "Something went wrong",
|
|
197
|
+
"booking/create.label": "Create booking",
|
|
198
|
+
"booking/create.description": "Create a new booking request from a client",
|
|
199
|
+
"booking/get-available-slots.label": "Get available slots",
|
|
200
|
+
"booking/get-available-slots.description": "List bookable time slots for a consultant",
|
|
201
|
+
"booking/get-availability.label": "Get availability",
|
|
202
|
+
"booking/get-availability.description": "Load weekly availability settings for the workspace",
|
|
203
|
+
"booking/save-availability.label": "Save availability",
|
|
204
|
+
"booking/save-availability.description": "Save weekly availability and booking preferences",
|
|
205
|
+
"booking/list.label": "List bookings",
|
|
206
|
+
"booking/list.description": "List all bookings for the current workspace",
|
|
207
|
+
"booking/decline.label": "Decline booking",
|
|
208
|
+
"booking/decline.description": "Decline a pending booking request",
|
|
209
|
+
"booking/confirm.label": "Confirm booking",
|
|
210
|
+
"booking/confirm.description": "Confirm a pending booking request",
|
|
211
|
+
"booking/cancel.label": "Cancel booking",
|
|
212
|
+
"booking/cancel.description": "Cancel a confirmed booking"
|
|
213
|
+
}
|
package/src/features.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const features = [
|
|
2
|
+
{
|
|
3
|
+
title: 'Din bokningssida',
|
|
4
|
+
icon: 'share',
|
|
5
|
+
text: 'En personlig sida på ossy.se/boka/ditt-namn. Dela länken med kunder – det är allt som krävs.',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
title: 'Automatiska bekräftelser',
|
|
9
|
+
icon: 'check-circle',
|
|
10
|
+
text: 'Kunden får en bekräftelse med kalenderinbjudan direkt. Inga manuella steg från din sida.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
title: 'Hantera din tid',
|
|
14
|
+
icon: 'calendar',
|
|
15
|
+
text: 'Ange när du är tillgänglig. Vi visar bara lediga tider – aldrig dubbelbokade.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: 'E-postpingpong',
|
|
19
|
+
icon: 'mail',
|
|
20
|
+
text: 'Slipp fram och tillbaka i flera dagar bara för att hitta ett gemensamt tidslucka.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: 'Inga dubbelbokningar',
|
|
24
|
+
icon: 'warning',
|
|
25
|
+
text: 'En kalender här, en länk där – vi håller koll så samma tid inte bokas två gånger.',
|
|
26
|
+
},
|
|
27
|
+
]
|