Skip to content

Commit b177f62

Browse files
nedimfclaude
andcommitted
Add useStatus hook and status page API support (v0.1.3)
Add getPublicStatusOverview client method and useStatus hook that fetches status page data and transforms it into StatusData format for the StatusBoard component. Includes auto-refresh polling and new status API types. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 770eeec commit b177f62

File tree

7 files changed

+352
-1
lines changed

7 files changed

+352
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@appgram/react",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "React library for integrating Appgram portal features with pre-built UI components and headless hooks",
55
"main": "dist/index.js",
66
"module": "dist/index.mjs",

src/client/AppgramClient.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
ContactForm,
2929
ContactFormSubmission,
3030
ContactFormSubmitInput,
31+
StatusPageOverview,
3132
} from '../types'
3233

3334
export interface AppgramClientConfig {
@@ -625,6 +626,19 @@ export class AppgramClient {
625626
)
626627
}
627628

629+
// ============================================================================
630+
// Status Pages
631+
// ============================================================================
632+
633+
/**
634+
* Get public status page overview (services, active updates, overall status)
635+
*/
636+
async getPublicStatusOverview(slug = 'status'): Promise<ApiResponse<StatusPageOverview>> {
637+
return this.get<StatusPageOverview>(
638+
`/api/v1/status-pages/public/${this.projectId}/${slug}/overview`
639+
)
640+
}
641+
628642
// ============================================================================
629643
// Surveys
630644
// ============================================================================

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export {
2727
type UseHelpArticleResult,
2828
} from './useHelpCenter'
2929
export { useSupport, type UseSupportOptions, type UseSupportResult } from './useSupport'
30+
export { useStatus, type UseStatusOptions, type UseStatusResult } from './useStatus'
3031
export {
3132
useSurvey,
3233
useSurveySubmit,

src/hooks/useStatus.ts

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* useStatus Hook
3+
*
4+
* Fetches status page data and transforms it for the StatusBoard component.
5+
* Supports auto-refresh polling.
6+
*/
7+
8+
import { useState, useCallback, useEffect, useRef } from 'react'
9+
import { useAppgramContext } from '../provider/context'
10+
import { getErrorMessage } from '../utils'
11+
import type {
12+
StatusPageOverview,
13+
StatusUpdate as ApiStatusUpdate,
14+
StatusPageService,
15+
StatusType,
16+
} from '../types'
17+
import type {
18+
StatusData,
19+
StatusComponent,
20+
StatusIncident,
21+
OverallStatus,
22+
ComponentStatus,
23+
} from '../components/status/StatusBoard'
24+
25+
export interface UseStatusOptions {
26+
/**
27+
* Status page slug
28+
* @default 'status'
29+
*/
30+
slug?: string
31+
32+
/**
33+
* Whether to fetch immediately
34+
* @default true
35+
*/
36+
enabled?: boolean
37+
38+
/**
39+
* Auto-refresh interval in milliseconds (0 to disable)
40+
* @default 30000
41+
*/
42+
refreshInterval?: number
43+
}
44+
45+
export interface UseStatusResult {
46+
/**
47+
* Transformed status data ready for StatusBoard
48+
*/
49+
status: StatusData | null
50+
51+
/**
52+
* Raw API overview response
53+
*/
54+
overview: StatusPageOverview | null
55+
56+
/**
57+
* Loading state
58+
*/
59+
isLoading: boolean
60+
61+
/**
62+
* Error message
63+
*/
64+
error: string | null
65+
66+
/**
67+
* Manually refresh
68+
*/
69+
refetch: () => Promise<void>
70+
}
71+
72+
// Map API status types to the component's simpler types
73+
function mapOverallStatus(apiStatus: StatusType): OverallStatus {
74+
switch (apiStatus) {
75+
case 'operational':
76+
return 'operational'
77+
case 'degraded_performance':
78+
return 'degraded'
79+
case 'partial_outage':
80+
return 'partial_outage'
81+
case 'major_outage':
82+
return 'major_outage'
83+
case 'maintenance':
84+
return 'degraded'
85+
case 'incident':
86+
return 'major_outage'
87+
default:
88+
return 'operational'
89+
}
90+
}
91+
92+
function mapComponentStatus(apiStatus: StatusType): ComponentStatus {
93+
switch (apiStatus) {
94+
case 'operational':
95+
return 'operational'
96+
case 'degraded_performance':
97+
return 'degraded'
98+
case 'partial_outage':
99+
return 'partial_outage'
100+
case 'major_outage':
101+
return 'major_outage'
102+
case 'maintenance':
103+
return 'degraded'
104+
case 'incident':
105+
return 'major_outage'
106+
default:
107+
return 'operational'
108+
}
109+
}
110+
111+
function transformToStatusData(overview: StatusPageOverview): StatusData {
112+
// Transform services into components
113+
const components: StatusComponent[] = (overview.services || []).map(
114+
(service: StatusPageService) => ({
115+
id: service.id,
116+
name: service.name,
117+
description: service.description || undefined,
118+
status: mapComponentStatus(
119+
overview.services_status[service.name] ||
120+
overview.services_status[service.id] ||
121+
'operational'
122+
),
123+
group: service.group_name || undefined,
124+
})
125+
)
126+
127+
// Transform active updates into incidents
128+
const incidents: StatusIncident[] = (overview.active_updates || []).map(
129+
(update: ApiStatusUpdate) => ({
130+
id: update.id,
131+
title: update.title,
132+
status: update.state === 'resolved' ? 'resolved' as const : 'investigating' as const,
133+
impact:
134+
update.status_type === 'major_outage'
135+
? ('critical' as const)
136+
: update.status_type === 'partial_outage' || update.status_type === 'incident'
137+
? ('major' as const)
138+
: ('minor' as const),
139+
created_at: update.created_at,
140+
resolved_at: update.resolved_at,
141+
updates: [
142+
{
143+
id: `${update.id}-initial`,
144+
message: update.description,
145+
status: update.state === 'resolved' ? 'resolved' as const : 'investigating' as const,
146+
created_at: update.created_at,
147+
},
148+
],
149+
affected_components: update.affected_services,
150+
})
151+
)
152+
153+
// Also include recent resolved updates as past incidents
154+
const resolvedIncidents: StatusIncident[] = (overview.recent_updates || [])
155+
.filter((u: ApiStatusUpdate) => u.state === 'resolved')
156+
.map((update: ApiStatusUpdate) => ({
157+
id: update.id,
158+
title: update.title,
159+
status: 'resolved' as const,
160+
impact:
161+
update.status_type === 'major_outage'
162+
? ('critical' as const)
163+
: update.status_type === 'partial_outage' || update.status_type === 'incident'
164+
? ('major' as const)
165+
: ('minor' as const),
166+
created_at: update.created_at,
167+
resolved_at: update.resolved_at,
168+
updates: [
169+
{
170+
id: `${update.id}-initial`,
171+
message: update.description,
172+
status: 'resolved' as const,
173+
created_at: update.created_at,
174+
},
175+
],
176+
affected_components: update.affected_services,
177+
}))
178+
179+
// Merge and deduplicate
180+
const allIncidentIds = new Set(incidents.map((i) => i.id))
181+
const mergedIncidents = [
182+
...incidents,
183+
...resolvedIncidents.filter((i) => !allIncidentIds.has(i.id)),
184+
]
185+
186+
return {
187+
overall_status: mapOverallStatus(overview.current_status),
188+
components,
189+
incidents: mergedIncidents,
190+
last_updated: new Date().toISOString(),
191+
}
192+
}
193+
194+
export function useStatus(options: UseStatusOptions = {}): UseStatusResult {
195+
const { slug = 'status', enabled = true, refreshInterval = 30000 } = options
196+
const { client } = useAppgramContext()
197+
const [overview, setOverview] = useState<StatusPageOverview | null>(null)
198+
const [status, setStatus] = useState<StatusData | null>(null)
199+
const [isLoading, setIsLoading] = useState(false)
200+
const [error, setError] = useState<string | null>(null)
201+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
202+
203+
const fetchStatus = useCallback(
204+
async (skipLoading = false) => {
205+
if (!skipLoading) {
206+
setIsLoading(true)
207+
}
208+
setError(null)
209+
210+
try {
211+
const response = await client.getPublicStatusOverview(slug)
212+
213+
if (response.success && response.data) {
214+
setOverview(response.data)
215+
setStatus(transformToStatusData(response.data))
216+
} else {
217+
setError(getErrorMessage(response.error, 'Failed to fetch status'))
218+
}
219+
} catch (err) {
220+
setError(getErrorMessage(err, 'An error occurred'))
221+
} finally {
222+
if (!skipLoading) {
223+
setIsLoading(false)
224+
}
225+
}
226+
},
227+
[client, slug]
228+
)
229+
230+
// Initial fetch
231+
useEffect(() => {
232+
if (enabled) {
233+
fetchStatus()
234+
}
235+
}, [enabled, fetchStatus])
236+
237+
// Auto-refresh
238+
useEffect(() => {
239+
if (!enabled || refreshInterval <= 0) return
240+
241+
intervalRef.current = setInterval(() => {
242+
fetchStatus(true)
243+
}, refreshInterval)
244+
245+
return () => {
246+
if (intervalRef.current) {
247+
clearInterval(intervalRef.current)
248+
}
249+
}
250+
}, [enabled, refreshInterval, fetchStatus])
251+
252+
return {
253+
status,
254+
overview,
255+
isLoading,
256+
error,
257+
refetch: () => fetchStatus(false),
258+
}
259+
}

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export {
3434
useSurveySubmit,
3535
useContactForm,
3636
useContactFormSubmit,
37+
useStatus,
3738
type UseWishesOptions,
3839
type UseWishesResult,
3940
type UseWishOptions,
@@ -64,6 +65,8 @@ export {
6465
type UseContactFormResult,
6566
type UseContactFormSubmitOptions,
6667
type UseContactFormSubmitResult,
68+
type UseStatusOptions,
69+
type UseStatusResult,
6770
} from './hooks'
6871

6972
// Components
@@ -184,6 +187,13 @@ export type {
184187
ContactFormFieldValidation,
185188
ContactFormSubmission,
186189
ContactFormSubmitInput,
190+
// Status API types
191+
StatusPage,
192+
StatusUpdate as StatusUpdateApi,
193+
StatusPageService,
194+
StatusPageOverview,
195+
StatusType,
196+
StatusState,
187197
// Customization types
188198
CustomColors,
189199
CustomTypography,

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './support'
1212
export * from './customization'
1313
export * from './survey'
1414
export * from './form'
15+
export * from './status'
1516

1617
/**
1718
* API Response wrapper

0 commit comments

Comments
 (0)