@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,26 +1,8 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react'
2
+ import { View, Title, Text, Tabs, Alert } 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,126 +11,19 @@ export const metadata = {
29
11
  },
30
12
  }
31
13
 
32
- // ---------------------------------------------------------------------------
33
- // Tab bar
34
- // ---------------------------------------------------------------------------
35
14
  const TABS = [
36
15
  { id: 'pending', label: 'Förfrågningar' },
37
16
  { id: 'confirmed', label: 'Bekräftade' },
38
17
  { id: 'all', label: 'Alla' },
39
18
  ]
40
19
 
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
- )
20
+ const EMPTY_MESSAGES = {
21
+ pending: { heading: 'Inga förfrågningar', body: 'Du har inga väntande bokningsförfrågningar just nu.' },
22
+ confirmed: { heading: 'Inga bekräftade bokningar', body: 'Bekräftade bokningar visas här.' },
23
+ all: { heading: 'Inga bokningar', body: 'Bokningar från dina klienter visas här.' },
90
24
  }
91
25
 
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 }) {
26
+ export default function BookingsPage () {
152
27
  const [activeTab, setActiveTab] = useState('pending')
153
28
  const [bookings, setBookings] = useState([])
154
29
  const [loading, setLoading] = useState(true)
@@ -180,14 +55,10 @@ export default function BookingsPage({ workspaceId }) {
180
55
  loadBookings()
181
56
  }, [loadBookings])
182
57
 
183
- // Filtered views
184
58
  const pending = bookings.filter(b => b.status === 'pending')
185
59
  const confirmed = bookings.filter(b => b.status === 'confirmed')
186
60
  const visible = activeTab === 'pending' ? pending : activeTab === 'confirmed' ? confirmed : bookings
187
61
 
188
- const counts = { pending: pending.length, confirmed: confirmed.length, all: bookings.length }
189
-
190
- // Actions
191
62
  const handleConfirm = async (booking) => {
192
63
  const res = await fetch('/actions/booking/confirm', {
193
64
  method: 'POST',
@@ -231,56 +102,41 @@ export default function BookingsPage({ workspaceId }) {
231
102
  window.location.href = `/bokningar/${booking.id}`
232
103
  }
233
104
 
105
+ const empty = EMPTY_MESSAGES[activeTab] ?? EMPTY_MESSAGES.all
106
+
234
107
  return (
235
- <div
236
- style={{
237
- fontFamily: FONT,
238
- height: '100%',
239
- overflowY: 'auto',
240
- background: C.bg,
241
- padding: 'var(--space-m, 16px) var(--space-l, 24px)',
242
- boxSizing: 'border-box',
243
- }}
108
+ <View
109
+ gap="m"
110
+ surface="primary"
111
+ style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
244
112
  >
245
- {/* Page header */}
246
- <div style={{ marginBottom: 24 }}>
247
- <h1 style={{ margin: 0, fontSize: 20, fontWeight: 700, color: C.primary }}>
248
- Bokningar
249
- </h1>
250
- <p style={{ margin: '4px 0 0', fontSize: 14, color: C.secondary }}>
113
+ <View inset="s" gap="s">
114
+ <Title>{Definition.title} Bokningar</Title>
115
+ <Text style={{ maxWidth: '400px' }}>
251
116
  Hantera bokningsförfrågningar och bekräftade möten.
252
- </p>
253
- </div>
117
+ </Text>
118
+ </View>
254
119
 
255
- {/* Content card */}
256
- <div
257
- style={{
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} />
120
+ <View gap="m" inset="s" surface="secondary" roundness="m">
121
+ <Tabs
122
+ tabs={TABS}
123
+ activeTabId={activeTab}
124
+ onTabClick={(_, tab) => setActiveTab(tab.id)}
125
+ />
265
126
 
266
127
  {loading ? (
267
- <Spinner />
128
+ <Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>
129
+ Laddar…
130
+ </Text>
268
131
  ) : error ? (
269
- <div
270
- style={{
271
- padding: '24px',
272
- fontSize: 14,
273
- color: C.errorText,
274
- textAlign: 'center',
275
- fontFamily: FONT,
276
- }}
277
- >
278
- {error}
279
- </div>
132
+ <Alert variant="danger">{error}</Alert>
280
133
  ) : visible.length === 0 ? (
281
- <EmptyState tab={activeTab} />
134
+ <View gap="xs" alignItems="center" inset="l">
135
+ <Title variant="tertiary">{empty.heading}</Title>
136
+ <Text color="secondary">{empty.body}</Text>
137
+ </View>
282
138
  ) : (
283
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
139
+ <View gap="s">
284
140
  {visible.map(booking => (
285
141
  <BookingCard
286
142
  key={booking.id}
@@ -291,9 +147,9 @@ export default function BookingsPage({ workspaceId }) {
291
147
  onView={handleView}
292
148
  />
293
149
  ))}
294
- </div>
150
+ </View>
295
151
  )}
296
- </div>
297
- </div>
152
+ </View>
153
+ </View>
298
154
  )
299
155
  }
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,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
+ ]
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+ import { cover } from './cover.js'
3
+ import { features } from './features.js'
4
+ import SalesSection from './SalesSection.jsx'
5
+
6
+ export const metadata = {
7
+ id: 'booking/home',
8
+ title: 'Bokningskalender – Ossy',
9
+ description: 'Låt kunder boka tid direkt. Dela en länk, kunder väljer tid, du fokuserar på jobbet.',
10
+ path: { sv: '/boka', en: '/booking' },
11
+ }
12
+
13
+ export default () => <SalesSection cover={cover} features={features} />