@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
package/src/bookings.page.jsx
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
<
|
|
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
|
-
}}
|
|
108
|
+
<View
|
|
109
|
+
gap="m"
|
|
110
|
+
surface="primary"
|
|
111
|
+
style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
|
|
244
112
|
>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<
|
|
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
|
-
</
|
|
253
|
-
</
|
|
117
|
+
</Text>
|
|
118
|
+
</View>
|
|
254
119
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
<
|
|
128
|
+
<Text color="secondary" style={{ textAlign: 'center', padding: 'var(--space-l)' }}>
|
|
129
|
+
Laddar…
|
|
130
|
+
</Text>
|
|
268
131
|
) : 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>
|
|
132
|
+
<Alert variant="danger">{error}</Alert>
|
|
280
133
|
) : visible.length === 0 ? (
|
|
281
|
-
<
|
|
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
|
-
<
|
|
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
|
-
</
|
|
150
|
+
</View>
|
|
295
151
|
)}
|
|
296
|
-
</
|
|
297
|
-
</
|
|
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
|
+
}
|
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
|
+
]
|
package/src/home.page.jsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { cover } from './cover.js'
|
|
3
|
+
import { features } from './features.js'
|
|
4
|
+
import SalesSection from './SalesSection.jsx'
|
|
3
5
|
|
|
4
6
|
export const metadata = {
|
|
5
7
|
id: 'booking/home',
|
|
@@ -8,366 +10,4 @@ export const metadata = {
|
|
|
8
10
|
path: { sv: '/boka', en: '/booking' },
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const painPoints = [
|
|
14
|
-
{
|
|
15
|
-
icon: 'email',
|
|
16
|
-
title: 'E-postpingpong',
|
|
17
|
-
text: 'Fram och tillbaka i flera dagar bara för att hitta ett gemensamt tidslucka.',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
icon: 'warning',
|
|
21
|
-
title: 'Dubbelbokningar',
|
|
22
|
-
text: 'En kalender här, en länk där – det är lätt att råka boka samma tid två gånger.',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
icon: 'alert',
|
|
26
|
-
title: 'Missade bokningar',
|
|
27
|
-
text: 'Påminnelser glöms bort. Kunden dyker inte upp. Tid och pengar går till spillo.',
|
|
28
|
-
},
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
// ─── Features ────────────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
const features = [
|
|
34
|
-
{
|
|
35
|
-
icon: 'share',
|
|
36
|
-
title: 'Din bokningssida',
|
|
37
|
-
text: 'En personlig sida på ossy.se/boka/ditt-namn. Dela länken med kunder – det är allt som krävs.',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
icon: 'check-circle',
|
|
41
|
-
title: 'Automatiska bekräftelser',
|
|
42
|
-
text: 'Kunden får en bekräftelse med kalenderinbjudan direkt. Inga manuella steg från din sida.',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
icon: 'calendar',
|
|
46
|
-
title: 'Hantera din tid',
|
|
47
|
-
text: 'Ange när du är tillgänglig. Vi visar bara lediga tider – aldrig dubbelbokade.',
|
|
48
|
-
},
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
// ─── Personas ─────────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
const personas = [
|
|
54
|
-
{ icon: 'person', label: 'Coach', text: 'Boka coachingsessioner utan mellanhänder.' },
|
|
55
|
-
{ icon: 'code', label: 'IT-konsult', text: 'Låt kunder boka möten direkt i din kalender.' },
|
|
56
|
-
{ icon: 'brush', label: 'Designer', text: 'Fyll din agenda med kundmöten på dina villkor.' },
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
// ─── Pricing ─────────────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
const included = [
|
|
62
|
-
{ icon: 'infinity', text: 'Obegränsade bokningar' },
|
|
63
|
-
{ icon: 'bell', text: 'Automatiska påminnelser' },
|
|
64
|
-
{ icon: 'calendar', text: 'Kalenderinbjudningar' },
|
|
65
|
-
{ icon: 'link', text: 'Din personliga bokningssida' },
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
// ─── Page ─────────────────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
export default function BookingHomePage() {
|
|
71
|
-
return (
|
|
72
|
-
<>
|
|
73
|
-
<style href="@ossy/booking-sales-page" precedence="high">{`
|
|
74
|
-
.bsp-hero {
|
|
75
|
-
min-height: min(52vh, 560px);
|
|
76
|
-
padding: var(--space-xl) var(--space-m);
|
|
77
|
-
background: linear-gradient(
|
|
78
|
-
155deg,
|
|
79
|
-
hsl(200, 100%, 97%) 0%,
|
|
80
|
-
hsl(170, 60%, 94%) 60%,
|
|
81
|
-
hsl(160, 70%, 90%) 100%
|
|
82
|
-
);
|
|
83
|
-
border-radius: var(--space-m);
|
|
84
|
-
text-align: center;
|
|
85
|
-
display: flex;
|
|
86
|
-
flex-direction: column;
|
|
87
|
-
align-items: center;
|
|
88
|
-
justify-content: center;
|
|
89
|
-
gap: var(--space-l);
|
|
90
|
-
overflow: hidden;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.bsp-section {
|
|
94
|
-
padding: var(--space-xl) var(--space-m);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.bsp-grid-3 {
|
|
98
|
-
display: grid;
|
|
99
|
-
grid-template-columns: repeat(3, 1fr);
|
|
100
|
-
gap: var(--space-m);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.bsp-grid-2 {
|
|
104
|
-
display: grid;
|
|
105
|
-
grid-template-columns: repeat(3, 1fr);
|
|
106
|
-
gap: var(--space-m);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
@media (max-width: 860px) {
|
|
110
|
-
.bsp-grid-3,
|
|
111
|
-
.bsp-grid-2 {
|
|
112
|
-
grid-template-columns: 1fr;
|
|
113
|
-
}
|
|
114
|
-
.bsp-hero {
|
|
115
|
-
padding: var(--space-l) var(--space-s);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
@media (min-width: 860px) and (max-width: 1100px) {
|
|
120
|
-
.bsp-grid-3 {
|
|
121
|
-
grid-template-columns: repeat(3, 1fr);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
`}</style>
|
|
125
|
-
|
|
126
|
-
<PageSection maxWidth="l" gap="l" style={{ padding: 'var(--space-m) var(--space-m) var(--space-xl)' }}>
|
|
127
|
-
|
|
128
|
-
{/* ── Hero ─────────────────────────────────────────────────────── */}
|
|
129
|
-
<div className="bsp-hero">
|
|
130
|
-
<View gap="m" alignItems="center" style={{ maxWidth: 680 }}>
|
|
131
|
-
<Title as="h1" variant="display" style={{ textWrap: 'balance', letterSpacing: '-0.5px' }}>
|
|
132
|
-
Låt kunder boka tid direkt
|
|
133
|
-
</Title>
|
|
134
|
-
<Text style={{ fontSize: '1.2rem', lineHeight: 1.6, opacity: 0.8 }}>
|
|
135
|
-
Dela en länk. Kunder väljer tid. Du fokuserar på jobbet.
|
|
136
|
-
</Text>
|
|
137
|
-
</View>
|
|
138
|
-
|
|
139
|
-
<View layout="row" gap="m" justifyContent="center" style={{ flexWrap: 'wrap' }}>
|
|
140
|
-
<Button variant="cta" href="/sign-up" style={{ padding: '12px 32px', fontSize: '1rem' }}>
|
|
141
|
-
Kom igång gratis
|
|
142
|
-
</Button>
|
|
143
|
-
<Button variant="secondary" href="/boka/demo" style={{ padding: '12px 32px', fontSize: '1rem' }}>
|
|
144
|
-
Se ett exempel
|
|
145
|
-
</Button>
|
|
146
|
-
</View>
|
|
147
|
-
</div>
|
|
148
|
-
|
|
149
|
-
{/* ── Problem ──────────────────────────────────────────────────── */}
|
|
150
|
-
<View gap="l" className="bsp-section" style={{ textAlign: 'center' }}>
|
|
151
|
-
<View gap="s" alignItems="center">
|
|
152
|
-
<Title as="h2" variant="secondary">
|
|
153
|
-
Trött på e-postpingpong för att hitta en tid som passar?
|
|
154
|
-
</Title>
|
|
155
|
-
<Text style={{ maxWidth: 560, opacity: 0.75 }}>
|
|
156
|
-
Du är inte ensam. De flesta konsulter och frilansare slösar timmar i veckan på att koordinera möten.
|
|
157
|
-
</Text>
|
|
158
|
-
</View>
|
|
159
|
-
|
|
160
|
-
<div className="bsp-grid-3">
|
|
161
|
-
{painPoints.map(({ icon, title, text }) => (
|
|
162
|
-
<View
|
|
163
|
-
key={title}
|
|
164
|
-
gap="m"
|
|
165
|
-
inset="l"
|
|
166
|
-
roundness="m"
|
|
167
|
-
style={{
|
|
168
|
-
border: '1px solid var(--separator)',
|
|
169
|
-
background: 'hsl(0, 0%, 99%)',
|
|
170
|
-
textAlign: 'left',
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
<View
|
|
174
|
-
style={{
|
|
175
|
-
width: 44,
|
|
176
|
-
height: 44,
|
|
177
|
-
borderRadius: 12,
|
|
178
|
-
background: 'hsl(0, 80%, 95%)',
|
|
179
|
-
display: 'flex',
|
|
180
|
-
alignItems: 'center',
|
|
181
|
-
justifyContent: 'center',
|
|
182
|
-
}}
|
|
183
|
-
>
|
|
184
|
-
<Icon2 name={icon} size="s" />
|
|
185
|
-
</View>
|
|
186
|
-
<Title as="h3" variant="tertiary">{title}</Title>
|
|
187
|
-
<Text variant="small" style={{ opacity: 0.8, lineHeight: 1.6 }}>{text}</Text>
|
|
188
|
-
</View>
|
|
189
|
-
))}
|
|
190
|
-
</div>
|
|
191
|
-
</View>
|
|
192
|
-
|
|
193
|
-
{/* ── Features ─────────────────────────────────────────────────── */}
|
|
194
|
-
<View
|
|
195
|
-
gap="l"
|
|
196
|
-
inset="l"
|
|
197
|
-
roundness="m"
|
|
198
|
-
className="bsp-section"
|
|
199
|
-
style={{
|
|
200
|
-
background: 'hsl(170, 30%, 97%)',
|
|
201
|
-
border: '1px solid hsl(170, 30%, 90%)',
|
|
202
|
-
}}
|
|
203
|
-
>
|
|
204
|
-
<View gap="s" alignItems="center" style={{ textAlign: 'center' }}>
|
|
205
|
-
<Title as="h2" variant="secondary">Allt du behöver, inget du inte behöver</Title>
|
|
206
|
-
<Text style={{ opacity: 0.75 }}>Enkelt att komma igång. Kraftfullt nog att lita på varje dag.</Text>
|
|
207
|
-
</View>
|
|
208
|
-
|
|
209
|
-
<div className="bsp-grid-3">
|
|
210
|
-
{features.map(({ icon, title, text }) => (
|
|
211
|
-
<View
|
|
212
|
-
key={title}
|
|
213
|
-
gap="m"
|
|
214
|
-
inset="l"
|
|
215
|
-
roundness="m"
|
|
216
|
-
style={{
|
|
217
|
-
background: 'hsl(0, 0%, 100%)',
|
|
218
|
-
border: '1px solid hsl(170, 20%, 88%)',
|
|
219
|
-
boxShadow: '0 2px 8px hsl(170, 30%, 88%)',
|
|
220
|
-
}}
|
|
221
|
-
>
|
|
222
|
-
<View
|
|
223
|
-
style={{
|
|
224
|
-
width: 48,
|
|
225
|
-
height: 48,
|
|
226
|
-
borderRadius: 14,
|
|
227
|
-
background: 'hsl(167, 89%, 43%)',
|
|
228
|
-
display: 'flex',
|
|
229
|
-
alignItems: 'center',
|
|
230
|
-
justifyContent: 'center',
|
|
231
|
-
}}
|
|
232
|
-
>
|
|
233
|
-
<Icon2 name={icon} size="s" style={{ color: 'white' }} />
|
|
234
|
-
</View>
|
|
235
|
-
<Title as="h3" variant="tertiary">{title}</Title>
|
|
236
|
-
<Text variant="small" style={{ opacity: 0.8, lineHeight: 1.6 }}>{text}</Text>
|
|
237
|
-
</View>
|
|
238
|
-
))}
|
|
239
|
-
</div>
|
|
240
|
-
</View>
|
|
241
|
-
|
|
242
|
-
{/* ── Social proof ─────────────────────────────────────────────── */}
|
|
243
|
-
<View gap="l" className="bsp-section" style={{ textAlign: 'center' }}>
|
|
244
|
-
<View gap="s" alignItems="center">
|
|
245
|
-
<Title as="h2" variant="secondary">Byggd för konsulter och frilansare</Title>
|
|
246
|
-
<Text style={{ opacity: 0.75, maxWidth: 500 }}>
|
|
247
|
-
Oavsett vad du jobbar med – om du säljer din tid, passar Ossy.
|
|
248
|
-
</Text>
|
|
249
|
-
</View>
|
|
250
|
-
|
|
251
|
-
<div className="bsp-grid-3">
|
|
252
|
-
{personas.map(({ icon, label, text }) => (
|
|
253
|
-
<View
|
|
254
|
-
key={label}
|
|
255
|
-
gap="m"
|
|
256
|
-
inset="l"
|
|
257
|
-
roundness="m"
|
|
258
|
-
alignItems="center"
|
|
259
|
-
style={{
|
|
260
|
-
border: '1px solid var(--separator)',
|
|
261
|
-
textAlign: 'center',
|
|
262
|
-
}}
|
|
263
|
-
>
|
|
264
|
-
<View
|
|
265
|
-
style={{
|
|
266
|
-
width: 52,
|
|
267
|
-
height: 52,
|
|
268
|
-
borderRadius: '50%',
|
|
269
|
-
background: 'hsl(200, 60%, 93%)',
|
|
270
|
-
display: 'flex',
|
|
271
|
-
alignItems: 'center',
|
|
272
|
-
justifyContent: 'center',
|
|
273
|
-
}}
|
|
274
|
-
>
|
|
275
|
-
<Icon2 name={icon} size="m" />
|
|
276
|
-
</View>
|
|
277
|
-
<Title as="h3" variant="tertiary">{label}</Title>
|
|
278
|
-
<Text variant="small" style={{ opacity: 0.75, lineHeight: 1.6 }}>{text}</Text>
|
|
279
|
-
</View>
|
|
280
|
-
))}
|
|
281
|
-
</div>
|
|
282
|
-
</View>
|
|
283
|
-
|
|
284
|
-
{/* ── Pricing ──────────────────────────────────────────────────── */}
|
|
285
|
-
<View gap="l" className="bsp-section" alignItems="center" style={{ textAlign: 'center' }}>
|
|
286
|
-
<View gap="s" alignItems="center">
|
|
287
|
-
<Title as="h2" variant="secondary">Enkel och transparent prissättning</Title>
|
|
288
|
-
<Text style={{ opacity: 0.75 }}>Inga överraskningar. Avsluta när du vill.</Text>
|
|
289
|
-
</View>
|
|
290
|
-
|
|
291
|
-
<View
|
|
292
|
-
gap="l"
|
|
293
|
-
inset="xl"
|
|
294
|
-
roundness="m"
|
|
295
|
-
alignItems="center"
|
|
296
|
-
style={{
|
|
297
|
-
background: 'hsl(0, 0%, 100%)',
|
|
298
|
-
border: '2px solid hsl(167, 89%, 43%)',
|
|
299
|
-
boxShadow: '0 4px 24px hsl(167, 89%, 88%)',
|
|
300
|
-
maxWidth: 420,
|
|
301
|
-
width: '100%',
|
|
302
|
-
}}
|
|
303
|
-
>
|
|
304
|
-
<View gap="xs" alignItems="center">
|
|
305
|
-
<Text variant="small" style={{ opacity: 0.6, letterSpacing: 1, textTransform: 'uppercase' }}>
|
|
306
|
-
Bokningskalender
|
|
307
|
-
</Text>
|
|
308
|
-
<Title as="p" variant="display" style={{ lineHeight: 1, letterSpacing: '-1px' }}>
|
|
309
|
-
299
|
|
310
|
-
<span style={{ fontSize: '1.2rem', fontWeight: 500, opacity: 0.7 }}> SEK/mån</span>
|
|
311
|
-
</Title>
|
|
312
|
-
</View>
|
|
313
|
-
|
|
314
|
-
<View gap="s" style={{ width: '100%', textAlign: 'left' }}>
|
|
315
|
-
{included.map(({ icon, text }) => (
|
|
316
|
-
<View key={text} layout="row" gap="s" alignItems="center">
|
|
317
|
-
<Icon2 name={icon} size="xs" style={{ color: 'hsl(167, 89%, 43%)', flexShrink: 0 }} />
|
|
318
|
-
<Text variant="small">{text}</Text>
|
|
319
|
-
</View>
|
|
320
|
-
))}
|
|
321
|
-
</View>
|
|
322
|
-
|
|
323
|
-
<Button
|
|
324
|
-
variant="cta"
|
|
325
|
-
href="/sign-up"
|
|
326
|
-
style={{ width: '100%', padding: '14px 0', fontSize: '1rem', textAlign: 'center' }}
|
|
327
|
-
>
|
|
328
|
-
Starta din bokningssida
|
|
329
|
-
</Button>
|
|
330
|
-
|
|
331
|
-
<Text variant="small" style={{ opacity: 0.5 }}>
|
|
332
|
-
Inget betalkort krävs för att komma igång.
|
|
333
|
-
</Text>
|
|
334
|
-
</View>
|
|
335
|
-
</View>
|
|
336
|
-
|
|
337
|
-
{/* ── Footer CTA ───────────────────────────────────────────────── */}
|
|
338
|
-
<View
|
|
339
|
-
gap="l"
|
|
340
|
-
inset="xl"
|
|
341
|
-
roundness="m"
|
|
342
|
-
alignItems="center"
|
|
343
|
-
style={{
|
|
344
|
-
background: 'linear-gradient(135deg, hsl(200, 100%, 17%) 0%, hsl(170, 70%, 22%) 100%)',
|
|
345
|
-
textAlign: 'center',
|
|
346
|
-
}}
|
|
347
|
-
>
|
|
348
|
-
<View gap="m" alignItems="center" style={{ maxWidth: 540 }}>
|
|
349
|
-
<Title
|
|
350
|
-
as="h2"
|
|
351
|
-
variant="secondary"
|
|
352
|
-
style={{ color: 'hsl(0, 0%, 98%)', letterSpacing: '-0.25px' }}
|
|
353
|
-
>
|
|
354
|
-
Redo att sluta jaga tider?
|
|
355
|
-
</Title>
|
|
356
|
-
<Text style={{ color: 'hsl(0, 0%, 80%)', fontSize: '1.05rem', lineHeight: 1.6 }}>
|
|
357
|
-
Kom igång på under fem minuter. Din bokningssida är live direkt.
|
|
358
|
-
</Text>
|
|
359
|
-
</View>
|
|
360
|
-
|
|
361
|
-
<Button
|
|
362
|
-
variant="cta"
|
|
363
|
-
href="/sign-up"
|
|
364
|
-
style={{ padding: '14px 40px', fontSize: '1.05rem' }}
|
|
365
|
-
>
|
|
366
|
-
Kom igång gratis
|
|
367
|
-
</Button>
|
|
368
|
-
</View>
|
|
369
|
-
|
|
370
|
-
</PageSection>
|
|
371
|
-
</>
|
|
372
|
-
)
|
|
373
|
-
}
|
|
13
|
+
export default () => <SalesSection cover={cover} features={features} />
|