@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.
@@ -1,46 +1,20 @@
1
- import React, { useState, useEffect } from 'react'
2
-
3
- export const path = { sv: '/tillganglighet', en: '/availability' }
4
- export const id = 'booking/availability-setup'
5
-
6
- // ---------------------------------------------------------------------------
7
- // Constants
8
- // ---------------------------------------------------------------------------
9
-
10
- const FONT = 'system-ui, -apple-system, sans-serif'
11
-
12
- const COLOR = {
13
- bg: '#f5f5f5',
14
- surface: '#ffffff',
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',
1
+ import React, { useState, useEffect, useMemo } from 'react'
2
+ import {
3
+ View,
4
+ Title,
5
+ Text,
6
+ Button,
7
+ Input,
8
+ Select,
9
+ Alert,
10
+ useLocale,
11
+ } from '@ossy/design-system'
12
+
13
+ export const metadata = {
14
+ id: 'booking/availability-setup',
15
+ path: { sv: '/tillganglighet', en: '/availability' },
32
16
  }
33
17
 
34
- const DAYS = [
35
- { label: 'Måndag', dayOfWeek: 1 },
36
- { label: 'Tisdag', dayOfWeek: 2 },
37
- { label: 'Onsdag', dayOfWeek: 3 },
38
- { label: 'Torsdag', dayOfWeek: 4 },
39
- { label: 'Fredag', dayOfWeek: 5 },
40
- { label: 'Lördag', dayOfWeek: 6 },
41
- { label: 'Söndag', dayOfWeek: 0 },
42
- ]
43
-
44
18
  const SESSION_DURATIONS = [30, 60, 90]
45
19
  const BUFFER_OPTIONS = [0, 15, 30]
46
20
  const TIMEZONE_OPTIONS = [
@@ -53,11 +27,7 @@ const TIMEZONE_OPTIONS = [
53
27
  'Asia/Tokyo',
54
28
  ]
55
29
 
56
- // ---------------------------------------------------------------------------
57
- // Helpers
58
- // ---------------------------------------------------------------------------
59
-
60
- function buildDayState(weeklyWindows = []) {
30
+ function buildDayState (weeklyWindows = []) {
61
31
  const byDay = {}
62
32
  for (const w of weeklyWindows) {
63
33
  byDay[w.dayOfWeek] = {
@@ -66,121 +36,67 @@ function buildDayState(weeklyWindows = []) {
66
36
  endTime: w.endTime ?? '17:00',
67
37
  }
68
38
  }
69
- return Object.fromEntries(
70
- DAYS.map(d => [
71
- d.dayOfWeek,
72
- byDay[d.dayOfWeek] ?? { enabled: false, startTime: '09:00', endTime: '17:00' },
73
- ]),
74
- )
39
+ const dayOrder = [
40
+ { dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
41
+ { dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
42
+ { dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
43
+ { dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
44
+ { dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
45
+ { dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
46
+ { dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
47
+ ]
48
+ return dayOrder.map(({ dayOfWeek }) => ({
49
+ dayOfWeek,
50
+ ...(byDay[dayOfWeek] ?? { enabled: false, startTime: '09:00', endTime: '17:00' }),
51
+ }))
75
52
  }
76
53
 
77
- // ---------------------------------------------------------------------------
78
- // Sub-components
79
- // ---------------------------------------------------------------------------
80
-
81
- function Toggle({ checked, onChange }) {
54
+ function DayRow ({ label, day, onToggle, onTimeChange, t }) {
82
55
  return (
83
- <button
84
- role="switch"
85
- aria-checked={checked}
86
- onClick={() => onChange(!checked)}
87
- style={{
88
- width: 36,
89
- height: 20,
90
- borderRadius: 10,
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
- }}
56
+ <View
57
+ layout="row"
58
+ gap="m"
59
+ alignItems="center"
60
+ surface={day.enabled ? 'secondary' : 'primary'}
61
+ roundness="m"
62
+ inset="s"
63
+ style={{ border: '1px solid var(--separator)' }}
99
64
  >
100
- <span
101
- style={{
102
- position: 'absolute',
103
- top: 2,
104
- left: checked ? 18 : 2,
105
- width: 16,
106
- height: 16,
107
- borderRadius: '50%',
108
- background: '#ffffff',
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>
65
+ <label style={{ display: 'flex', alignItems: 'center', flexShrink: 0, cursor: 'pointer' }}>
66
+ <input
67
+ type="checkbox"
68
+ checked={day.enabled}
69
+ onChange={() => onToggle()}
70
+ style={{ width: 16, height: 16, accentColor: 'var(--color-accent)' }}
71
+ />
72
+ </label>
73
+ <Text weight="medium" style={{ width: '4.5rem', flexShrink: 0 }}>{label}</Text>
165
74
 
166
75
  {day.enabled ? (
167
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
168
- <TimeInput value={day.startTime} onChange={v => onTimeChange('startTime', v)} />
169
- <span style={{ fontSize: 13, color: COLOR.muted, userSelect: 'none' }}>–</span>
170
- <TimeInput value={day.endTime} onChange={v => onTimeChange('endTime', v)} />
171
- </div>
76
+ <View layout="row" gap="s" alignItems="center" style={{ flex: 1, flexWrap: 'wrap' }}>
77
+ <Input type="time" value={day.startTime} onChange={e => onTimeChange('startTime', e.target.value)} />
78
+ <Text color="secondary">–</Text>
79
+ <Input type="time" value={day.endTime} onChange={e => onTimeChange('endTime', e.target.value)} />
80
+ </View>
172
81
  ) : (
173
- <span style={{ fontSize: 13, color: COLOR.muted }}>Inte tillgänglig</span>
82
+ <Text color="secondary" size="s">{t('booking.availabilitySetup.unavailable')}</Text>
174
83
  )}
175
- </div>
84
+ </View>
176
85
  )
177
86
  }
178
87
 
179
- // ---------------------------------------------------------------------------
180
- // Page
181
- // ---------------------------------------------------------------------------
88
+ export default function AvailabilitySetupPage ({ workspaceId }) {
89
+ const { t } = useLocale()
90
+ const dayDefs = useMemo(() => [
91
+ { dayOfWeek: 1, key: 'booking.availabilitySetup.day.monday' },
92
+ { dayOfWeek: 2, key: 'booking.availabilitySetup.day.tuesday' },
93
+ { dayOfWeek: 3, key: 'booking.availabilitySetup.day.wednesday' },
94
+ { dayOfWeek: 4, key: 'booking.availabilitySetup.day.thursday' },
95
+ { dayOfWeek: 5, key: 'booking.availabilitySetup.day.friday' },
96
+ { dayOfWeek: 6, key: 'booking.availabilitySetup.day.saturday' },
97
+ { dayOfWeek: 0, key: 'booking.availabilitySetup.day.sunday' },
98
+ ], [])
182
99
 
183
- export default function AvailabilitySetupPage({ workspaceId, ...props }) {
184
100
  const [dayState, setDayState] = useState(() => buildDayState())
185
101
  const [sessionDurations, setSessionDurations] = useState([60])
186
102
  const [bufferMinutes, setBufferMinutes] = useState(0)
@@ -193,7 +109,6 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
193
109
 
194
110
  const bookingUrl = `https://ossy.se/boka/${workspaceId ?? ''}`
195
111
 
196
- // Load existing availability on mount
197
112
  useEffect(() => {
198
113
  fetch('/actions/booking/get-availability')
199
114
  .then(r => (r.ok ? r.json() : null))
@@ -215,18 +130,14 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
215
130
  }, [])
216
131
 
217
132
  const toggleDay = (dayOfWeek) => {
218
- setDayState(prev => ({
219
- ...prev,
220
- [dayOfWeek]: { ...prev[dayOfWeek], enabled: !prev[dayOfWeek].enabled },
221
- }))
133
+ setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek
134
+ ? { ...d, enabled: !d.enabled }
135
+ : d))
222
136
  setSaved(false)
223
137
  }
224
138
 
225
139
  const updateDayTime = (dayOfWeek, field, value) => {
226
- setDayState(prev => ({
227
- ...prev,
228
- [dayOfWeek]: { ...prev[dayOfWeek], [field]: value },
229
- }))
140
+ setDayState(prev => prev.map(d => d.dayOfWeek === dayOfWeek ? { ...d, [field]: value } : d))
230
141
  setSaved(false)
231
142
  }
232
143
 
@@ -245,10 +156,8 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
245
156
  setSaving(true)
246
157
  setError(null)
247
158
 
248
- const weeklyWindows = DAYS.filter(d => dayState[d.dayOfWeek]?.enabled).map(d => ({
249
- dayOfWeek: d.dayOfWeek,
250
- startTime: dayState[d.dayOfWeek].startTime,
251
- endTime: dayState[d.dayOfWeek].endTime,
159
+ const weeklyWindows = dayState.filter(d => d.enabled).map(({ dayOfWeek, startTime, endTime }) => ({
160
+ dayOfWeek, startTime, endTime,
252
161
  }))
253
162
 
254
163
  try {
@@ -260,7 +169,7 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
260
169
 
261
170
  if (!res.ok) {
262
171
  const data = await res.json().catch(() => ({}))
263
- throw new Error(data?.message ?? `Kunde inte spara (${res.status})`)
172
+ throw new Error(data?.message ?? t('booking.availabilitySetup.errorSave', { status: res.status }))
264
173
  }
265
174
 
266
175
  setSaved(true)
@@ -278,265 +187,106 @@ export default function AvailabilitySetupPage({ workspaceId, ...props }) {
278
187
  })
279
188
  }
280
189
 
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
190
  if (loading) {
315
191
  return (
316
- <div style={{ fontFamily: FONT, padding: '32px 24px', color: COLOR.secondary, fontSize: 14 }}>
317
- Laddar…
318
- </div>
192
+ <View surface="primary" inset="l" style={{ height: '100%', overflowY: 'auto' }}>
193
+ <Text color="secondary">{t('booking.availabilitySetup.loading')}</Text>
194
+ </View>
319
195
  )
320
196
  }
321
197
 
322
198
  return (
323
- <div
324
- style={{
325
- fontFamily: FONT,
326
- maxWidth: 620,
327
- padding: '28px 24px',
328
- boxSizing: 'border-box',
329
- }}
199
+ <View
200
+ gap="m"
201
+ surface="primary"
202
+ style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto', maxWidth: 620 }}
330
203
  >
331
- {/* Page header */}
332
- <div style={{ marginBottom: 24 }}>
333
- <h1 style={{ margin: '0 0 6px', fontSize: 22, fontWeight: 700, color: COLOR.primary }}>
334
- Tillgänglighet
335
- </h1>
336
- <p style={{ margin: 0, fontSize: 14, color: COLOR.secondary, maxWidth: 480 }}>
337
- Ange när du är tillgänglig för bokningar. Klienter kan bara boka tider inom dessa
338
- fönster.
339
- </p>
340
- </div>
341
-
342
- {/* Weekly schedule */}
343
- <div style={cardStyle}>
344
- <p style={sectionHeadingStyle}>Veckoschema</p>
345
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
346
- {DAYS.map(({ label, dayOfWeek }) => (
347
- <DayRow
348
- key={dayOfWeek}
349
- label={label}
350
- dayOfWeek={dayOfWeek}
351
- day={dayState[dayOfWeek]}
352
- onToggle={() => toggleDay(dayOfWeek)}
353
- onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
354
- />
355
- ))}
356
- </div>
357
- </div>
358
-
359
- {/* Session durations */}
360
- <div style={cardStyle}>
361
- <p style={sectionHeadingStyle}>Sessionslängd</p>
362
- <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
363
- {SESSION_DURATIONS.map(dur => {
364
- const active = sessionDurations.includes(dur)
204
+ <View inset="s" gap="xs">
205
+ <Title variant="secondary">{t('booking.availabilitySetup.title')}</Title>
206
+ <Text style={{ maxWidth: 480 }}>{t('booking.availabilitySetup.description')}</Text>
207
+ </View>
208
+
209
+ <View gap="s" surface="secondary" roundness="m" inset="m">
210
+ <Title variant="tertiary">{t('booking.availabilitySetup.weeklySchedule')}</Title>
211
+ <View gap="xs">
212
+ {dayDefs.map(({ dayOfWeek, key }) => {
213
+ const day = dayState.find(d => d.dayOfWeek === dayOfWeek)
365
214
  return (
366
- <button
367
- key={dur}
368
- 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
- >
382
- {dur} min
383
- </button>
215
+ <DayRow
216
+ key={dayOfWeek}
217
+ label={t(key)}
218
+ day={day}
219
+ onToggle={() => toggleDay(dayOfWeek)}
220
+ onTimeChange={(field, value) => updateDayTime(dayOfWeek, field, value)}
221
+ t={t}
222
+ />
384
223
  )
385
224
  })}
386
- </div>
387
- <p style={{ margin: '10px 0 0', fontSize: 12, color: COLOR.muted }}>
388
- Välj de sessionslängder klienter kan välja bland.
389
- </p>
390
- </div>
225
+ </View>
226
+ </View>
227
+
228
+ <View gap="s" surface="secondary" roundness="m" inset="m">
229
+ <Title variant="tertiary">{t('booking.availabilitySetup.sessionDuration')}</Title>
230
+ <View layout="row" gap="s" style={{ flexWrap: 'wrap' }}>
231
+ {SESSION_DURATIONS.map(dur => (
232
+ <Button
233
+ key={dur}
234
+ variant={sessionDurations.includes(dur) ? 'tab-active' : 'tab'}
235
+ onClick={() => toggleDuration(dur)}
236
+ >
237
+ {t('booking.availabilitySetup.durationMin', { minutes: dur })}
238
+ </Button>
239
+ ))}
240
+ </View>
241
+ <Text size="s" color="secondary">{t('booking.availabilitySetup.sessionDurationHint')}</Text>
242
+ </View>
391
243
 
392
- {/* Buffer time */}
393
- <div style={cardStyle}>
394
- <label
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
244
+ <View gap="s" surface="secondary" roundness="m" inset="m">
245
+ <Text weight="medium">{t('booking.availabilitySetup.bufferLabel')}</Text>
246
+ <Select
401
247
  id="buffer-select"
402
248
  value={bufferMinutes}
403
- onChange={e => {
404
- setBufferMinutes(Number(e.target.value))
405
- setSaved(false)
406
- }}
407
- style={{ ...selectStyle, minWidth: 190 }}
249
+ onChange={e => { setBufferMinutes(Number(e.target.value)); setSaved(false) }}
408
250
  >
409
251
  {BUFFER_OPTIONS.map(opt => (
410
252
  <option key={opt} value={opt}>
411
- {opt === 0 ? 'Ingen bufferttid' : `${opt} minuter`}
253
+ {opt === 0 ? t('booking.availabilitySetup.bufferNone') : t('booking.availabilitySetup.bufferMinutes', { minutes: opt })}
412
254
  </option>
413
255
  ))}
414
- </select>
415
- <p style={{ margin: '8px 0 0', fontSize: 12, color: COLOR.muted }}>
416
- Automatisk paustid som läggs till efter varje bokning.
417
- </p>
418
- </div>
256
+ </Select>
257
+ <Text size="s" color="secondary">{t('booking.availabilitySetup.bufferHint')}</Text>
258
+ </View>
419
259
 
420
- {/* Timezone */}
421
- <div style={cardStyle}>
422
- <label
423
- htmlFor="tz-select"
424
- style={{ display: 'block', fontWeight: 600, fontSize: 14, color: COLOR.primary, marginBottom: 8 }}
425
- >
426
- Tidszon
427
- </label>
428
- <select
429
- id="tz-select"
430
- value={timezone}
431
- onChange={e => {
432
- setTimezone(e.target.value)
433
- setSaved(false)
434
- }}
435
- style={{ ...selectStyle, minWidth: 220 }}
436
- >
260
+ <View gap="s" surface="secondary" roundness="m" inset="m">
261
+ <Text weight="medium">{t('booking.availabilitySetup.timezone')}</Text>
262
+ <Select id="tz-select" value={timezone} onChange={e => { setTimezone(e.target.value); setSaved(false) }}>
437
263
  {TIMEZONE_OPTIONS.map(tz => (
438
- <option key={tz} value={tz}>
439
- {tz}
440
- </option>
264
+ <option key={tz} value={tz}>{tz}</option>
441
265
  ))}
442
- </select>
443
- </div>
266
+ </Select>
267
+ </View>
444
268
 
445
- {/* Error message */}
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
- )}
269
+ {error && <Alert variant="danger">{error}</Alert>}
461
270
 
462
- {/* Save button */}
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
- >
480
- {saving ? 'Sparar…' : 'Spara tillgänglighet'}
481
- </button>
271
+ <Button variant="primary" disabled={saving} onClick={handleSave}>
272
+ {saving ? t('booking.availabilitySetup.saving') : t('booking.availabilitySetup.save')}
273
+ </Button>
482
274
 
483
- {/* Booking URL — shown after successful save */}
484
275
  {saved && workspaceId && (
485
- <div
486
- style={{
487
- padding: '20px 24px',
488
- background: COLOR.successBg,
489
- border: `1px solid ${COLOR.successBorder}`,
490
- borderRadius: 12,
491
- }}
492
- >
493
- <p style={{ margin: '0 0 4px', fontWeight: 700, fontSize: 14, color: COLOR.success }}>
494
- ✓ Tillgänglighet sparad
495
- </p>
496
- <p style={{ margin: '0 0 14px', fontSize: 13, color: COLOR.secondary }}>
497
- Din bokningslänk är redo att delas med klienter.
498
- </p>
499
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
500
- <code
501
- style={{
502
- display: 'block',
503
- padding: '8px 12px',
504
- background: COLOR.surface,
505
- border: `1px solid ${COLOR.border}`,
506
- borderRadius: 6,
507
- fontSize: 13,
508
- color: COLOR.primary,
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>
276
+ <Alert variant="success" title={t('booking.availabilitySetup.savedTitle')}>
277
+ <View gap="s">
278
+ <Text color="secondary">{t('booking.availabilitySetup.savedBody')}</Text>
279
+ <View layout="row" gap="s" alignItems="center">
280
+ <Text as="code" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'ui-monospace, monospace' }}>
281
+ {bookingUrl}
282
+ </Text>
283
+ <Button variant={copied ? 'cta' : 'primary'} onClick={handleCopyLink}>
284
+ {copied ? t('booking.availabilitySetup.copied') : t('booking.availabilitySetup.copyLink')}
285
+ </Button>
286
+ </View>
287
+ </View>
288
+ </Alert>
539
289
  )}
540
- </div>
290
+ </View>
541
291
  )
542
292
  }