@nuasite/components 0.11.1 → 0.12.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.
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -1
- package/src/index.ts +7 -0
- package/src/reservation/availability/index.astro +116 -0
- package/src/reservation/availability/types.ts +10 -0
- package/src/reservation/checkout/index.astro +92 -0
- package/src/reservation/checkout/types.ts +10 -0
- package/src/reservation/status/index.astro +114 -0
- package/src/reservation/status/types.ts +11 -0
- package/src/reservation/types.ts +21 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@nuasite/components",
|
|
3
3
|
"description": "Nua Site astro components.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.12.0",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist/**",
|
|
8
8
|
"src/**",
|
|
@@ -36,6 +36,21 @@
|
|
|
36
36
|
"types": "./src/image/index.astro",
|
|
37
37
|
"import": "./src/image/index.astro",
|
|
38
38
|
"default": "./src/image/index.astro"
|
|
39
|
+
},
|
|
40
|
+
"./ReservationCheckout.astro": {
|
|
41
|
+
"types": "./src/reservation/checkout/index.astro",
|
|
42
|
+
"import": "./src/reservation/checkout/index.astro",
|
|
43
|
+
"default": "./src/reservation/checkout/index.astro"
|
|
44
|
+
},
|
|
45
|
+
"./ReservationAvailability.astro": {
|
|
46
|
+
"types": "./src/reservation/availability/index.astro",
|
|
47
|
+
"import": "./src/reservation/availability/index.astro",
|
|
48
|
+
"default": "./src/reservation/availability/index.astro"
|
|
49
|
+
},
|
|
50
|
+
"./ReservationStatus.astro": {
|
|
51
|
+
"types": "./src/reservation/status/index.astro",
|
|
52
|
+
"import": "./src/reservation/status/index.astro",
|
|
53
|
+
"default": "./src/reservation/status/index.astro"
|
|
39
54
|
}
|
|
40
55
|
},
|
|
41
56
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -2,3 +2,10 @@ export { default as Form } from './form/index.astro'
|
|
|
2
2
|
export type { FormProps } from './form/types'
|
|
3
3
|
export { default as Image } from './image/index.astro'
|
|
4
4
|
export type { CloudflareImageTransformOptions, ImageProps } from './image/types'
|
|
5
|
+
export { default as ReservationCheckout } from './reservation/checkout/index.astro'
|
|
6
|
+
export type { ReservationCheckoutProps } from './reservation/checkout/types'
|
|
7
|
+
export { default as ReservationAvailability } from './reservation/availability/index.astro'
|
|
8
|
+
export type { ReservationAvailabilityProps } from './reservation/availability/types'
|
|
9
|
+
export { default as ReservationStatus } from './reservation/status/index.astro'
|
|
10
|
+
export type { ReservationStatusProps } from './reservation/status/types'
|
|
11
|
+
export type { ReservationProduct, ReservationTimeSlot, ReservationError } from './reservation/types'
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ReservationAvailabilityProps } from './types'
|
|
3
|
+
|
|
4
|
+
export type Props = ReservationAvailabilityProps
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
reservationId,
|
|
8
|
+
action = `/_nua/reservation/availability/${reservationId}`,
|
|
9
|
+
loadingMessage = 'Loading availability...',
|
|
10
|
+
errorMessage = 'Failed to load availability.',
|
|
11
|
+
pollInterval,
|
|
12
|
+
} = Astro.props
|
|
13
|
+
|
|
14
|
+
if (!action && !reservationId) {
|
|
15
|
+
throw new Error('ReservationAvailability requires either "reservationId" or "action" prop')
|
|
16
|
+
}
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<nua-reservation-availability
|
|
20
|
+
data-action={action}
|
|
21
|
+
data-loading-message={loadingMessage}
|
|
22
|
+
data-error-message={errorMessage}
|
|
23
|
+
data-poll-interval={pollInterval}
|
|
24
|
+
data-state="loading"
|
|
25
|
+
>
|
|
26
|
+
<div data-status="loading">
|
|
27
|
+
<span>{loadingMessage}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div data-status="loaded" class="hidden">
|
|
30
|
+
<slot />
|
|
31
|
+
</div>
|
|
32
|
+
<div data-status="error" class="hidden">
|
|
33
|
+
<span>{errorMessage}</span>
|
|
34
|
+
</div>
|
|
35
|
+
</nua-reservation-availability>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
import type { ReservationProduct } from '../types'
|
|
39
|
+
|
|
40
|
+
class NuaReservationAvailability extends HTMLElement {
|
|
41
|
+
private products: ReservationProduct[] = []
|
|
42
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null
|
|
43
|
+
private fetchController: AbortController | null = null
|
|
44
|
+
|
|
45
|
+
connectedCallback() {
|
|
46
|
+
this.fetchAvailability()
|
|
47
|
+
|
|
48
|
+
const pollInterval = this.dataset.pollInterval
|
|
49
|
+
if (pollInterval) {
|
|
50
|
+
const ms = parseInt(pollInterval, 10)
|
|
51
|
+
if (ms > 0) {
|
|
52
|
+
this.pollTimer = setInterval(() => this.fetchAvailability(), ms)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
if (this.pollTimer) {
|
|
59
|
+
clearInterval(this.pollTimer)
|
|
60
|
+
this.pollTimer = null
|
|
61
|
+
}
|
|
62
|
+
this.fetchController?.abort()
|
|
63
|
+
this.fetchController = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getProducts(): ReservationProduct[] {
|
|
67
|
+
return this.products
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async refresh() {
|
|
71
|
+
await this.fetchAvailability()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private showState(state: 'loading' | 'loaded' | 'error') {
|
|
75
|
+
this.dataset.state = state
|
|
76
|
+
const loading = this.querySelector('[data-status="loading"]')
|
|
77
|
+
const loaded = this.querySelector('[data-status="loaded"]')
|
|
78
|
+
const error = this.querySelector('[data-status="error"]')
|
|
79
|
+
|
|
80
|
+
loading?.classList.toggle('hidden', state !== 'loading')
|
|
81
|
+
loaded?.classList.toggle('hidden', state !== 'loaded')
|
|
82
|
+
error?.classList.toggle('hidden', state !== 'error')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async fetchAvailability() {
|
|
86
|
+
this.fetchController?.abort()
|
|
87
|
+
this.fetchController = new AbortController()
|
|
88
|
+
|
|
89
|
+
this.showState('loading')
|
|
90
|
+
const action = this.dataset.action!
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(action, { signal: this.fetchController.signal })
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
this.showState('error')
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = await response.json()
|
|
101
|
+
this.products = data.products || []
|
|
102
|
+
|
|
103
|
+
this.showState('loaded')
|
|
104
|
+
this.dispatchEvent(new CustomEvent('availability:loaded', {
|
|
105
|
+
bubbles: true,
|
|
106
|
+
detail: { products: this.products },
|
|
107
|
+
}))
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
110
|
+
this.showState('error')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
customElements.define('nua-reservation-availability', NuaReservationAvailability)
|
|
116
|
+
</script>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ReservationCheckoutProps } from './types'
|
|
3
|
+
|
|
4
|
+
export type Props = ReservationCheckoutProps
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
reservationId,
|
|
8
|
+
action = `/_nua/reservation/checkout/${reservationId}`,
|
|
9
|
+
submittingMessage = 'Processing...',
|
|
10
|
+
errorMessage = 'Something went wrong. Please try again.',
|
|
11
|
+
unavailableMessage = 'This product is no longer available.',
|
|
12
|
+
} = Astro.props
|
|
13
|
+
|
|
14
|
+
if (!action && !reservationId) {
|
|
15
|
+
throw new Error('ReservationCheckout requires either "reservationId" or "action" prop')
|
|
16
|
+
}
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<nua-reservation-checkout
|
|
20
|
+
data-action={action}
|
|
21
|
+
data-submitting-message={submittingMessage}
|
|
22
|
+
data-error-message={errorMessage}
|
|
23
|
+
data-unavailable-message={unavailableMessage}
|
|
24
|
+
>
|
|
25
|
+
<form method="POST" action={action}>
|
|
26
|
+
<slot />
|
|
27
|
+
</form>
|
|
28
|
+
<div data-status="error" class="hidden">
|
|
29
|
+
<span>{errorMessage}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</nua-reservation-checkout>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
class NuaReservationCheckout extends HTMLElement {
|
|
35
|
+
connectedCallback() {
|
|
36
|
+
const form = this.querySelector('form')
|
|
37
|
+
if (!form) return
|
|
38
|
+
|
|
39
|
+
const submittingMessage = this.dataset.submittingMessage!
|
|
40
|
+
const unavailableMessage = this.dataset.unavailableMessage!
|
|
41
|
+
|
|
42
|
+
form.addEventListener('submit', (e) => {
|
|
43
|
+
const productId = form.querySelector<HTMLInputElement>('[name="productId"]')
|
|
44
|
+
if (!productId || !productId.value) {
|
|
45
|
+
e.preventDefault()
|
|
46
|
+
this.showError(unavailableMessage)
|
|
47
|
+
this.dispatchEvent(new CustomEvent('checkout:error', {
|
|
48
|
+
bubbles: true,
|
|
49
|
+
detail: { message: unavailableMessage },
|
|
50
|
+
}))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.hideError()
|
|
55
|
+
|
|
56
|
+
const submitButton = form.querySelector<HTMLButtonElement>('button[type="submit"], button:not([type])')
|
|
57
|
+
if (submitButton) {
|
|
58
|
+
submitButton.disabled = true
|
|
59
|
+
if (submittingMessage) {
|
|
60
|
+
submitButton.dataset.originalText = submitButton.textContent || ''
|
|
61
|
+
submitButton.textContent = submittingMessage
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
window.addEventListener('pageshow', (e) => {
|
|
67
|
+
if (!e.persisted) return
|
|
68
|
+
const submitButton = form.querySelector<HTMLButtonElement>('button[type="submit"], button:not([type])')
|
|
69
|
+
if (submitButton && submitButton.dataset.originalText) {
|
|
70
|
+
submitButton.disabled = false
|
|
71
|
+
submitButton.textContent = submitButton.dataset.originalText
|
|
72
|
+
delete submitButton.dataset.originalText
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private showError(message: string) {
|
|
78
|
+
const errorDiv = this.querySelector('[data-status="error"]')
|
|
79
|
+
if (!errorDiv) return
|
|
80
|
+
const span = errorDiv.querySelector('span')
|
|
81
|
+
if (span) span.textContent = message
|
|
82
|
+
errorDiv.classList.remove('hidden')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private hideError() {
|
|
86
|
+
const errorDiv = this.querySelector('[data-status="error"]')
|
|
87
|
+
errorDiv?.classList.add('hidden')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
customElements.define('nua-reservation-checkout', NuaReservationCheckout)
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ReservationStatusProps } from './types'
|
|
3
|
+
|
|
4
|
+
export type Props = ReservationStatusProps
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
reservationId,
|
|
8
|
+
action = `/_nua/reservation/status/${reservationId}`,
|
|
9
|
+
loadingMessage = 'Loading booking details...',
|
|
10
|
+
errorMessage = 'Failed to load booking details.',
|
|
11
|
+
missingBookingMessage = 'No booking found.',
|
|
12
|
+
notFoundMessage = 'Booking not found.',
|
|
13
|
+
} = Astro.props
|
|
14
|
+
|
|
15
|
+
if (!action && !reservationId) {
|
|
16
|
+
throw new Error('ReservationStatus requires either "reservationId" or "action" prop')
|
|
17
|
+
}
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<nua-reservation-status
|
|
21
|
+
data-action={action}
|
|
22
|
+
data-loading-message={loadingMessage}
|
|
23
|
+
data-error-message={errorMessage}
|
|
24
|
+
data-missing-session-message={missingBookingMessage}
|
|
25
|
+
data-not-found-message={notFoundMessage}
|
|
26
|
+
data-state="loading"
|
|
27
|
+
>
|
|
28
|
+
<div data-status="loading">
|
|
29
|
+
<span>{loadingMessage}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div data-status="loaded" class="hidden">
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
<div data-status="error" class="hidden">
|
|
35
|
+
<span>{errorMessage}</span>
|
|
36
|
+
</div>
|
|
37
|
+
</nua-reservation-status>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
class NuaReservationStatus extends HTMLElement {
|
|
41
|
+
private fetchController: AbortController | null = null
|
|
42
|
+
|
|
43
|
+
connectedCallback() {
|
|
44
|
+
this.fetchStatus()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnectedCallback() {
|
|
48
|
+
this.fetchController?.abort()
|
|
49
|
+
this.fetchController = null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private showState(state: 'loading' | 'loaded' | 'error', message?: string) {
|
|
53
|
+
this.dataset.state = state
|
|
54
|
+
const loading = this.querySelector('[data-status="loading"]')
|
|
55
|
+
const loaded = this.querySelector('[data-status="loaded"]')
|
|
56
|
+
const error = this.querySelector('[data-status="error"]')
|
|
57
|
+
|
|
58
|
+
loading?.classList.toggle('hidden', state !== 'loading')
|
|
59
|
+
loaded?.classList.toggle('hidden', state !== 'loaded')
|
|
60
|
+
error?.classList.toggle('hidden', state !== 'error')
|
|
61
|
+
|
|
62
|
+
if (message && state === 'error') {
|
|
63
|
+
const span = error?.querySelector('span')
|
|
64
|
+
if (span) span.textContent = message
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async fetchStatus() {
|
|
69
|
+
this.fetchController?.abort()
|
|
70
|
+
this.fetchController = new AbortController()
|
|
71
|
+
|
|
72
|
+
const action = this.dataset.action!
|
|
73
|
+
const missingBookingMessage = this.dataset.missingBookingMessage!
|
|
74
|
+
const notFoundMessage = this.dataset.notFoundMessage!
|
|
75
|
+
const errorMessage = this.dataset.errorMessage!
|
|
76
|
+
|
|
77
|
+
const params = new URLSearchParams(window.location.search)
|
|
78
|
+
const bookingId = params.get('booking_id')
|
|
79
|
+
|
|
80
|
+
if (!bookingId) {
|
|
81
|
+
this.showState('error', missingBookingMessage)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const url = `${action}?booking_id=${encodeURIComponent(bookingId)}`
|
|
87
|
+
const response = await fetch(url, { signal: this.fetchController.signal })
|
|
88
|
+
|
|
89
|
+
if (response.status === 404) {
|
|
90
|
+
this.showState('error', notFoundMessage)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
this.showState('error', errorMessage)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = await response.json()
|
|
100
|
+
|
|
101
|
+
this.showState('loaded')
|
|
102
|
+
this.dispatchEvent(new CustomEvent('status:loaded', {
|
|
103
|
+
bubbles: true,
|
|
104
|
+
detail: { booking: data.booking || data },
|
|
105
|
+
}))
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
108
|
+
this.showState('error', errorMessage)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
customElements.define('nua-reservation-status', NuaReservationStatus)
|
|
114
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ReservationStatusProps =
|
|
2
|
+
& {
|
|
3
|
+
loadingMessage?: string
|
|
4
|
+
errorMessage?: string
|
|
5
|
+
missingBookingMessage?: string
|
|
6
|
+
notFoundMessage?: string
|
|
7
|
+
}
|
|
8
|
+
& (
|
|
9
|
+
| { reservationId: string; action?: string }
|
|
10
|
+
| { reservationId?: string; action: string }
|
|
11
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ReservationTimeSlot {
|
|
2
|
+
id: string
|
|
3
|
+
startTime: string
|
|
4
|
+
endTime: string
|
|
5
|
+
available: boolean
|
|
6
|
+
remainingCapacity?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ReservationProduct {
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
description?: string
|
|
13
|
+
price: number
|
|
14
|
+
currency: string
|
|
15
|
+
timeSlots?: ReservationTimeSlot[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ReservationError {
|
|
19
|
+
error: string
|
|
20
|
+
code?: string
|
|
21
|
+
}
|