@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.
@@ -1,44 +1,27 @@
1
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',
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', dayOfWeek: 1 },
36
- { label: 'Tisdag', dayOfWeek: 2 },
37
- { label: 'Onsdag', dayOfWeek: 3 },
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', dayOfWeek: 5 },
40
- { label: 'Lördag', dayOfWeek: 6 },
41
- { label: 'Söndag', dayOfWeek: 0 },
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
- <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
- }}
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
- <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>
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
- <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>
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
- <span style={{ fontSize: 13, color: COLOR.muted }}>Inte tillgänglig</span>
92
+ <Text color="secondary" size="s">Inte tillgänglig</Text>
174
93
  )}
175
- </div>
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
- <div style={{ fontFamily: FONT, padding: '32px 24px', color: COLOR.secondary, fontSize: 14 }}>
317
- Laddar
318
- </div>
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
- <div
324
- style={{
325
- fontFamily: FONT,
326
- maxWidth: 620,
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
- {/* 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 }}>
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
- </div>
357
- </div>
228
+ </View>
229
+ </View>
358
230
 
359
- {/* Session durations */}
360
- <div style={cardStyle}>
361
- <p style={sectionHeadingStyle}>Sessionslängd</p>
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
- <button
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
- </button>
243
+ </Button>
384
244
  )
385
245
  })}
386
- </div>
387
- <p style={{ margin: '10px 0 0', fontSize: 12, color: COLOR.muted }}>
246
+ </View>
247
+ <Text size="s" color="secondary">
388
248
  Välj de sessionslängder klienter kan välja bland.
389
- </p>
390
- </div>
249
+ </Text>
250
+ </View>
391
251
 
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
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
- </select>
415
- <p style={{ margin: '8px 0 0', fontSize: 12, color: COLOR.muted }}>
267
+ </Select>
268
+ <Text size="s" color="secondary">
416
269
  Automatisk paustid som läggs till efter varje bokning.
417
- </p>
418
- </div>
270
+ </Text>
271
+ </View>
419
272
 
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
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
- </select>
443
- </div>
288
+ </Select>
289
+ </View>
444
290
 
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
- )}
291
+ {error && <Alert variant="danger">{error}</Alert>}
461
292
 
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
- >
293
+ <Button variant="primary" disabled={saving} onClick={handleSave}>
480
294
  {saving ? 'Sparar…' : 'Spara tillgänglighet'}
481
- </button>
295
+ </Button>
482
296
 
483
- {/* Booking URL — shown after successful save */}
484
297
  {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>
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
- </div>
323
+ </View>
541
324
  )
542
325
  }