@ossy/booking 1.15.6 → 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 +4 -364
- 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
|
@@ -1,44 +1,27 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
surfaceAlt: '#f9f9f9',
|
|
16
|
-
surfaceEnabled: '#f8f9ff',
|
|
17
|
-
border: '#e2e2e2',
|
|
18
|
-
borderEnabled: '#c7d2fe',
|
|
19
|
-
primary: '#111111',
|
|
20
|
-
primaryText: '#ffffff',
|
|
21
|
-
secondary: '#555555',
|
|
22
|
-
muted: '#999999',
|
|
23
|
-
success: '#16a34a',
|
|
24
|
-
successBg: '#f0fdf4',
|
|
25
|
-
successBorder: '#bbf7d0',
|
|
26
|
-
error: '#dc2626',
|
|
27
|
-
errorBg: '#fef2f2',
|
|
28
|
-
errorBorder: '#fecaca',
|
|
29
|
-
accent: '#4f46e5',
|
|
30
|
-
accentBg: '#eef2ff',
|
|
31
|
-
accentBorder: '#c7d2fe',
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Title,
|
|
5
|
+
Text,
|
|
6
|
+
Button,
|
|
7
|
+
Input,
|
|
8
|
+
Select,
|
|
9
|
+
Alert,
|
|
10
|
+
} from '@ossy/design-system'
|
|
11
|
+
|
|
12
|
+
export const metadata = {
|
|
13
|
+
id: 'booking/availability-setup',
|
|
14
|
+
path: { sv: '/tillganglighet', en: '/availability' },
|
|
32
15
|
}
|
|
33
16
|
|
|
34
17
|
const DAYS = [
|
|
35
|
-
{ label: 'Måndag',
|
|
36
|
-
{ label: 'Tisdag',
|
|
37
|
-
{ label: 'Onsdag',
|
|
18
|
+
{ label: 'Måndag', dayOfWeek: 1 },
|
|
19
|
+
{ label: 'Tisdag', dayOfWeek: 2 },
|
|
20
|
+
{ label: 'Onsdag', dayOfWeek: 3 },
|
|
38
21
|
{ label: 'Torsdag', dayOfWeek: 4 },
|
|
39
|
-
{ label: 'Fredag',
|
|
40
|
-
{ label: 'Lördag',
|
|
41
|
-
{ label: 'Söndag',
|
|
22
|
+
{ label: 'Fredag', dayOfWeek: 5 },
|
|
23
|
+
{ label: 'Lördag', dayOfWeek: 6 },
|
|
24
|
+
{ label: 'Söndag', dayOfWeek: 0 },
|
|
42
25
|
]
|
|
43
26
|
|
|
44
27
|
const SESSION_DURATIONS = [30, 60, 90]
|
|
@@ -53,11 +36,7 @@ const TIMEZONE_OPTIONS = [
|
|
|
53
36
|
'Asia/Tokyo',
|
|
54
37
|
]
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
// Helpers
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
function buildDayState(weeklyWindows = []) {
|
|
39
|
+
function buildDayState (weeklyWindows = []) {
|
|
61
40
|
const byDay = {}
|
|
62
41
|
for (const w of weeklyWindows) {
|
|
63
42
|
byDay[w.dayOfWeek] = {
|
|
@@ -74,113 +53,49 @@ function buildDayState(weeklyWindows = []) {
|
|
|
74
53
|
)
|
|
75
54
|
}
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
// Sub-components
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
function Toggle({ checked, onChange }) {
|
|
56
|
+
function DayRow ({ label, day, onToggle, onTimeChange }) {
|
|
82
57
|
return (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
border: 'none',
|
|
92
|
-
background: checked ? COLOR.accent : '#d1d5db',
|
|
93
|
-
cursor: 'pointer',
|
|
94
|
-
position: 'relative',
|
|
95
|
-
flexShrink: 0,
|
|
96
|
-
transition: 'background 0.15s',
|
|
97
|
-
padding: 0,
|
|
98
|
-
}}
|
|
58
|
+
<View
|
|
59
|
+
layout="row"
|
|
60
|
+
gap="m"
|
|
61
|
+
alignItems="center"
|
|
62
|
+
surface={day.enabled ? 'secondary' : 'primary'}
|
|
63
|
+
roundness="m"
|
|
64
|
+
inset="s"
|
|
65
|
+
style={{ border: '1px solid var(--separator)' }}
|
|
99
66
|
>
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
width: 16,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
transition: 'left 0.15s',
|
|
110
|
-
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
</button>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function TimeInput({ value, onChange }) {
|
|
118
|
-
return (
|
|
119
|
-
<input
|
|
120
|
-
type="time"
|
|
121
|
-
value={value}
|
|
122
|
-
onChange={e => onChange(e.target.value)}
|
|
123
|
-
style={{
|
|
124
|
-
padding: '5px 8px',
|
|
125
|
-
border: `1px solid ${COLOR.border}`,
|
|
126
|
-
borderRadius: 6,
|
|
127
|
-
fontFamily: FONT,
|
|
128
|
-
fontSize: 13,
|
|
129
|
-
color: COLOR.primary,
|
|
130
|
-
background: COLOR.surface,
|
|
131
|
-
outline: 'none',
|
|
132
|
-
}}
|
|
133
|
-
/>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function DayRow({ label, dayOfWeek, day, onToggle, onTimeChange }) {
|
|
138
|
-
return (
|
|
139
|
-
<div
|
|
140
|
-
style={{
|
|
141
|
-
display: 'flex',
|
|
142
|
-
alignItems: 'center',
|
|
143
|
-
gap: 12,
|
|
144
|
-
padding: '10px 14px',
|
|
145
|
-
borderRadius: 8,
|
|
146
|
-
background: day.enabled ? COLOR.surfaceEnabled : COLOR.surfaceAlt,
|
|
147
|
-
border: `1px solid ${day.enabled ? COLOR.borderEnabled : COLOR.border}`,
|
|
148
|
-
transition: 'background 0.15s, border-color 0.15s',
|
|
149
|
-
}}
|
|
150
|
-
>
|
|
151
|
-
<Toggle checked={day.enabled} onChange={onToggle} />
|
|
152
|
-
|
|
153
|
-
<span
|
|
154
|
-
style={{
|
|
155
|
-
width: 70,
|
|
156
|
-
fontSize: 14,
|
|
157
|
-
fontWeight: 600,
|
|
158
|
-
color: day.enabled ? COLOR.primary : COLOR.muted,
|
|
159
|
-
flexShrink: 0,
|
|
160
|
-
userSelect: 'none',
|
|
161
|
-
}}
|
|
162
|
-
>
|
|
163
|
-
{label}
|
|
164
|
-
</span>
|
|
67
|
+
<label style={{ display: 'flex', alignItems: 'center', flexShrink: 0, cursor: 'pointer' }}>
|
|
68
|
+
<input
|
|
69
|
+
type="checkbox"
|
|
70
|
+
checked={day.enabled}
|
|
71
|
+
onChange={() => onToggle()}
|
|
72
|
+
style={{ width: 16, height: 16, accentColor: 'var(--color-accent)' }}
|
|
73
|
+
/>
|
|
74
|
+
</label>
|
|
75
|
+
<Text weight="medium" style={{ width: '4.5rem', flexShrink: 0 }}>{label}</Text>
|
|
165
76
|
|
|
166
77
|
{day.enabled ? (
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
78
|
+
<View layout="row" gap="s" alignItems="center" style={{ flex: 1, flexWrap: 'wrap' }}>
|
|
79
|
+
<Input
|
|
80
|
+
type="time"
|
|
81
|
+
value={day.startTime}
|
|
82
|
+
onChange={e => onTimeChange('startTime', e.target.value)}
|
|
83
|
+
/>
|
|
84
|
+
<Text color="secondary">–</Text>
|
|
85
|
+
<Input
|
|
86
|
+
type="time"
|
|
87
|
+
value={day.endTime}
|
|
88
|
+
onChange={e => onTimeChange('endTime', e.target.value)}
|
|
89
|
+
/>
|
|
90
|
+
</View>
|
|
172
91
|
) : (
|
|
173
|
-
<
|
|
92
|
+
<Text color="secondary" size="s">Inte tillgänglig</Text>
|
|
174
93
|
)}
|
|
175
|
-
</
|
|
94
|
+
</View>
|
|
176
95
|
)
|
|
177
96
|
}
|
|
178
97
|
|
|
179
|
-
|
|
180
|
-
// Page
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
|
|
183
|
-
export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
98
|
+
export default function AvailabilitySetupPage ({ workspaceId }) {
|
|
184
99
|
const [dayState, setDayState] = useState(() => buildDayState())
|
|
185
100
|
const [sessionDurations, setSessionDurations] = useState([60])
|
|
186
101
|
const [bufferMinutes, setBufferMinutes] = useState(0)
|
|
@@ -193,7 +108,6 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
|
193
108
|
|
|
194
109
|
const bookingUrl = `https://ossy.se/boka/${workspaceId ?? ''}`
|
|
195
110
|
|
|
196
|
-
// Load existing availability on mount
|
|
197
111
|
useEffect(() => {
|
|
198
112
|
fetch('/actions/booking/get-availability')
|
|
199
113
|
.then(r => (r.ok ? r.json() : null))
|
|
@@ -278,265 +192,134 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
|
|
|
278
192
|
})
|
|
279
193
|
}
|
|
280
194
|
|
|
281
|
-
// ---------------------------------------------------------------------------
|
|
282
|
-
// Styles
|
|
283
|
-
// ---------------------------------------------------------------------------
|
|
284
|
-
|
|
285
|
-
const cardStyle = {
|
|
286
|
-
marginBottom: 16,
|
|
287
|
-
padding: '20px 24px',
|
|
288
|
-
background: COLOR.surface,
|
|
289
|
-
border: `1px solid ${COLOR.border}`,
|
|
290
|
-
borderRadius: 12,
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const sectionHeadingStyle = {
|
|
294
|
-
margin: '0 0 14px',
|
|
295
|
-
fontSize: 12,
|
|
296
|
-
fontWeight: 700,
|
|
297
|
-
color: COLOR.muted,
|
|
298
|
-
textTransform: 'uppercase',
|
|
299
|
-
letterSpacing: '0.06em',
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const selectStyle = {
|
|
303
|
-
padding: '8px 12px',
|
|
304
|
-
border: `1px solid ${COLOR.border}`,
|
|
305
|
-
borderRadius: 8,
|
|
306
|
-
fontFamily: FONT,
|
|
307
|
-
fontSize: 14,
|
|
308
|
-
color: COLOR.primary,
|
|
309
|
-
background: COLOR.surface,
|
|
310
|
-
cursor: 'pointer',
|
|
311
|
-
outline: 'none',
|
|
312
|
-
}
|
|
313
|
-
|
|
314
195
|
if (loading) {
|
|
315
196
|
return (
|
|
316
|
-
<
|
|
317
|
-
Laddar
|
|
318
|
-
</
|
|
197
|
+
<View surface="primary" inset="l" style={{ height: '100%', overflowY: 'auto' }}>
|
|
198
|
+
<Text color="secondary">Laddar…</Text>
|
|
199
|
+
</View>
|
|
319
200
|
)
|
|
320
201
|
}
|
|
321
202
|
|
|
322
203
|
return (
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
padding: '28px 24px',
|
|
328
|
-
boxSizing: 'border-box',
|
|
329
|
-
}}
|
|
204
|
+
<View
|
|
205
|
+
gap="m"
|
|
206
|
+
surface="primary"
|
|
207
|
+
style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto', maxWidth: 620 }}
|
|
330
208
|
>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
</
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
</
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
{/* Weekly schedule */}
|
|
343
|
-
<div style={cardStyle}>
|
|
344
|
-
<p style={sectionHeadingStyle}>Veckoschema</p>
|
|
345
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
209
|
+
<View inset="s" gap="xs">
|
|
210
|
+
<Title variant="secondary">Tillgänglighet</Title>
|
|
211
|
+
<Text style={{ maxWidth: 480 }}>
|
|
212
|
+
Ange när du är tillgänglig för bokningar. Klienter kan bara boka tider inom dessa fönster.
|
|
213
|
+
</Text>
|
|
214
|
+
</View>
|
|
215
|
+
|
|
216
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
217
|
+
<Title variant="tertiary">Veckoschema</Title>
|
|
218
|
+
<View gap="xs">
|
|
346
219
|
{DAYS.map(({ label, dayOfWeek }) => (
|
|
347
220
|
<DayRow
|
|
348
221
|
key={dayOfWeek}
|
|
349
222
|
label={label}
|
|
350
|
-
dayOfWeek={dayOfWeek}
|
|
351
223
|
day={dayState[dayOfWeek]}
|
|
352
224
|
onToggle={() => toggleDay(dayOfWeek)}
|
|
353
225
|
onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
|
|
354
226
|
/>
|
|
355
227
|
))}
|
|
356
|
-
</
|
|
357
|
-
</
|
|
228
|
+
</View>
|
|
229
|
+
</View>
|
|
358
230
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<
|
|
362
|
-
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
231
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
232
|
+
<Title variant="tertiary">Sessionslängd</Title>
|
|
233
|
+
<View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
|
|
363
234
|
{SESSION_DURATIONS.map(dur => {
|
|
364
235
|
const active = sessionDurations.includes(dur)
|
|
365
236
|
return (
|
|
366
|
-
<
|
|
237
|
+
<Button
|
|
367
238
|
key={dur}
|
|
239
|
+
variant={active ? 'tab-active' : 'tab'}
|
|
368
240
|
onClick={() => toggleDuration(dur)}
|
|
369
|
-
style={{
|
|
370
|
-
padding: '8px 20px',
|
|
371
|
-
border: `2px solid ${active ? COLOR.accent : COLOR.border}`,
|
|
372
|
-
borderRadius: 8,
|
|
373
|
-
background: active ? COLOR.accentBg : COLOR.surface,
|
|
374
|
-
color: active ? COLOR.accent : COLOR.secondary,
|
|
375
|
-
fontFamily: FONT,
|
|
376
|
-
fontSize: 14,
|
|
377
|
-
fontWeight: 600,
|
|
378
|
-
cursor: 'pointer',
|
|
379
|
-
transition: 'all 0.15s',
|
|
380
|
-
}}
|
|
381
241
|
>
|
|
382
242
|
{dur} min
|
|
383
|
-
</
|
|
243
|
+
</Button>
|
|
384
244
|
)
|
|
385
245
|
})}
|
|
386
|
-
</
|
|
387
|
-
<
|
|
246
|
+
</View>
|
|
247
|
+
<Text size="s" color="secondary">
|
|
388
248
|
Välj de sessionslängder klienter kan välja bland.
|
|
389
|
-
</
|
|
390
|
-
</
|
|
249
|
+
</Text>
|
|
250
|
+
</View>
|
|
391
251
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
<
|
|
395
|
-
htmlFor="buffer-select"
|
|
396
|
-
style={{ display: 'block', fontWeight: 600, fontSize: 14, color: COLOR.primary, marginBottom: 8 }}
|
|
397
|
-
>
|
|
398
|
-
Bufferttid mellan bokningar
|
|
399
|
-
</label>
|
|
400
|
-
<select
|
|
252
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
253
|
+
<Text weight="medium">Bufferttid mellan bokningar</Text>
|
|
254
|
+
<Select
|
|
401
255
|
id="buffer-select"
|
|
402
256
|
value={bufferMinutes}
|
|
403
257
|
onChange={e => {
|
|
404
258
|
setBufferMinutes(Number(e.target.value))
|
|
405
259
|
setSaved(false)
|
|
406
260
|
}}
|
|
407
|
-
style={{ ...selectStyle, minWidth: 190 }}
|
|
408
261
|
>
|
|
409
262
|
{BUFFER_OPTIONS.map(opt => (
|
|
410
263
|
<option key={opt} value={opt}>
|
|
411
264
|
{opt === 0 ? 'Ingen bufferttid' : `${opt} minuter`}
|
|
412
265
|
</option>
|
|
413
266
|
))}
|
|
414
|
-
</
|
|
415
|
-
<
|
|
267
|
+
</Select>
|
|
268
|
+
<Text size="s" color="secondary">
|
|
416
269
|
Automatisk paustid som läggs till efter varje bokning.
|
|
417
|
-
</
|
|
418
|
-
</
|
|
270
|
+
</Text>
|
|
271
|
+
</View>
|
|
419
272
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
<
|
|
423
|
-
htmlFor="tz-select"
|
|
424
|
-
style={{ display: 'block', fontWeight: 600, fontSize: 14, color: COLOR.primary, marginBottom: 8 }}
|
|
425
|
-
>
|
|
426
|
-
Tidszon
|
|
427
|
-
</label>
|
|
428
|
-
<select
|
|
273
|
+
<View gap="s" surface="secondary" roundness="m" inset="m">
|
|
274
|
+
<Text weight="medium">Tidszon</Text>
|
|
275
|
+
<Select
|
|
429
276
|
id="tz-select"
|
|
430
277
|
value={timezone}
|
|
431
278
|
onChange={e => {
|
|
432
279
|
setTimezone(e.target.value)
|
|
433
280
|
setSaved(false)
|
|
434
281
|
}}
|
|
435
|
-
style={{ ...selectStyle, minWidth: 220 }}
|
|
436
282
|
>
|
|
437
283
|
{TIMEZONE_OPTIONS.map(tz => (
|
|
438
284
|
<option key={tz} value={tz}>
|
|
439
285
|
{tz}
|
|
440
286
|
</option>
|
|
441
287
|
))}
|
|
442
|
-
</
|
|
443
|
-
</
|
|
288
|
+
</Select>
|
|
289
|
+
</View>
|
|
444
290
|
|
|
445
|
-
{
|
|
446
|
-
{error && (
|
|
447
|
-
<div
|
|
448
|
-
style={{
|
|
449
|
-
marginBottom: 16,
|
|
450
|
-
padding: '12px 16px',
|
|
451
|
-
background: COLOR.errorBg,
|
|
452
|
-
border: `1px solid ${COLOR.errorBorder}`,
|
|
453
|
-
borderRadius: 8,
|
|
454
|
-
color: COLOR.error,
|
|
455
|
-
fontSize: 14,
|
|
456
|
-
}}
|
|
457
|
-
>
|
|
458
|
-
{error}
|
|
459
|
-
</div>
|
|
460
|
-
)}
|
|
291
|
+
{error && <Alert variant="danger">{error}</Alert>}
|
|
461
292
|
|
|
462
|
-
|
|
463
|
-
<button
|
|
464
|
-
onClick={handleSave}
|
|
465
|
-
disabled={saving}
|
|
466
|
-
style={{
|
|
467
|
-
padding: '12px 28px',
|
|
468
|
-
background: saving ? COLOR.muted : COLOR.primary,
|
|
469
|
-
color: COLOR.primaryText,
|
|
470
|
-
border: 'none',
|
|
471
|
-
borderRadius: 8,
|
|
472
|
-
fontFamily: FONT,
|
|
473
|
-
fontSize: 15,
|
|
474
|
-
fontWeight: 600,
|
|
475
|
-
cursor: saving ? 'not-allowed' : 'pointer',
|
|
476
|
-
marginBottom: 20,
|
|
477
|
-
transition: 'background 0.15s',
|
|
478
|
-
}}
|
|
479
|
-
>
|
|
293
|
+
<Button variant="primary" disabled={saving} onClick={handleSave}>
|
|
480
294
|
{saving ? 'Sparar…' : 'Spara tillgänglighet'}
|
|
481
|
-
</
|
|
295
|
+
</Button>
|
|
482
296
|
|
|
483
|
-
{/* Booking URL — shown after successful save */}
|
|
484
297
|
{saved && workspaceId && (
|
|
485
|
-
<
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
fontFamily: 'ui-monospace, monospace',
|
|
510
|
-
flex: 1,
|
|
511
|
-
overflow: 'hidden',
|
|
512
|
-
textOverflow: 'ellipsis',
|
|
513
|
-
whiteSpace: 'nowrap',
|
|
514
|
-
}}
|
|
515
|
-
>
|
|
516
|
-
{bookingUrl}
|
|
517
|
-
</code>
|
|
518
|
-
<button
|
|
519
|
-
onClick={handleCopyLink}
|
|
520
|
-
style={{
|
|
521
|
-
padding: '8px 16px',
|
|
522
|
-
background: copied ? COLOR.success : COLOR.primary,
|
|
523
|
-
color: COLOR.primaryText,
|
|
524
|
-
border: 'none',
|
|
525
|
-
borderRadius: 6,
|
|
526
|
-
fontFamily: FONT,
|
|
527
|
-
fontSize: 13,
|
|
528
|
-
fontWeight: 600,
|
|
529
|
-
cursor: 'pointer',
|
|
530
|
-
flexShrink: 0,
|
|
531
|
-
transition: 'background 0.15s',
|
|
532
|
-
whiteSpace: 'nowrap',
|
|
533
|
-
}}
|
|
534
|
-
>
|
|
535
|
-
{copied ? '✓ Kopierad!' : 'Kopiera länk'}
|
|
536
|
-
</button>
|
|
537
|
-
</div>
|
|
538
|
-
</div>
|
|
298
|
+
<Alert variant="success" title="Tillgänglighet sparad">
|
|
299
|
+
<View gap="s">
|
|
300
|
+
<Text color="secondary">
|
|
301
|
+
Din bokningslänk är redo att delas med klienter.
|
|
302
|
+
</Text>
|
|
303
|
+
<View layout="row" gap="s" alignItems="center">
|
|
304
|
+
<Text
|
|
305
|
+
as="code"
|
|
306
|
+
style={{
|
|
307
|
+
flex: 1,
|
|
308
|
+
overflow: 'hidden',
|
|
309
|
+
textOverflow: 'ellipsis',
|
|
310
|
+
whiteSpace: 'nowrap',
|
|
311
|
+
fontFamily: 'ui-monospace, monospace',
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
{bookingUrl}
|
|
315
|
+
</Text>
|
|
316
|
+
<Button variant={copied ? 'cta' : 'primary'} onClick={handleCopyLink}>
|
|
317
|
+
{copied ? 'Kopierad!' : 'Kopiera länk'}
|
|
318
|
+
</Button>
|
|
319
|
+
</View>
|
|
320
|
+
</View>
|
|
321
|
+
</Alert>
|
|
539
322
|
)}
|
|
540
|
-
</
|
|
323
|
+
</View>
|
|
541
324
|
)
|
|
542
325
|
}
|