@telemetryos/cli 1.7.4 → 1.8.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/CHANGELOG.md +22 -0
- package/dist/commands/init.js +11 -0
- package/dist/services/generate-application.d.ts +1 -0
- package/dist/services/generate-application.js +127 -4
- package/dist/utils/validate-project-name.d.ts +19 -0
- package/dist/utils/validate-project-name.js +44 -0
- package/package.json +2 -2
- package/templates/vite-react-typescript/CLAUDE.md +68 -1244
- package/templates/vite-react-typescript/_claude/settings.local.json +17 -0
- package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +313 -0
- package/templates/vite-react-typescript/_claude/skills/tos-debugging/SKILL.md +299 -0
- package/templates/vite-react-typescript/_claude/skills/tos-media-api/SKILL.md +335 -0
- package/templates/vite-react-typescript/_claude/skills/tos-proxy-fetch/SKILL.md +319 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +332 -0
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +252 -0
- package/templates/vite-react-typescript/_claude/skills/tos-settings-ui/SKILL.md +636 -0
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +359 -0
- package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +384 -0
- package/templates/vite-react-typescript/src/hooks/store.ts +3 -3
- package/templates/vite-react-typescript/src/index.css +0 -1
- package/templates/vite-react-typescript/src/views/Render.css +2 -1
- package/templates/vite-react-typescript/src/views/Render.tsx +2 -3
- package/templates/vite-react-typescript/src/views/Settings.tsx +2 -3
- package/templates/vite-react-typescript/AGENTS.md +0 -7
- /package/templates/vite-react-typescript/{gitignore → _gitignore} +0 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tos-media-api
|
|
3
|
+
description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display user-uploaded media content.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TelemetryOS Media API
|
|
7
|
+
|
|
8
|
+
The Media API provides access to content uploaded to the TelemetryOS Media Library. Users manage their media through Studio and your app can display it.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { media } from '@telemetryos/sdk'
|
|
14
|
+
|
|
15
|
+
// Get all folders
|
|
16
|
+
const folders = await media().getAllFolders()
|
|
17
|
+
|
|
18
|
+
// Get content in a folder
|
|
19
|
+
const content = await media().getAllByFolderId('folder-id')
|
|
20
|
+
|
|
21
|
+
// Get content by tag
|
|
22
|
+
const tagged = await media().getAllByTag('banner')
|
|
23
|
+
|
|
24
|
+
// Get single item by ID
|
|
25
|
+
const item = await media().getById('content-id')
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Response Types
|
|
29
|
+
|
|
30
|
+
### MediaFolder
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
interface MediaFolder {
|
|
34
|
+
id: string
|
|
35
|
+
parentId: string // Parent folder ID (empty for root)
|
|
36
|
+
name: string // Folder name
|
|
37
|
+
size: number // Number of items
|
|
38
|
+
default: boolean // Is this the default folder?
|
|
39
|
+
createdAt: Date
|
|
40
|
+
updatedAt: Date
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### MediaContent
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface MediaContent {
|
|
48
|
+
id: string
|
|
49
|
+
contentFolderId: string // Parent folder
|
|
50
|
+
contentType: string // MIME type (image/jpeg, video/mp4, etc.)
|
|
51
|
+
name: string // File name
|
|
52
|
+
description: string // User description
|
|
53
|
+
thumbnailUrl: string // Thumbnail for preview
|
|
54
|
+
keys: string[] // Storage keys
|
|
55
|
+
publicUrls: string[] // CDN URLs for display
|
|
56
|
+
hidden: boolean // Hidden from listings
|
|
57
|
+
validFrom?: Date // Content scheduling
|
|
58
|
+
validTo?: Date // Content scheduling
|
|
59
|
+
createdAt: Date
|
|
60
|
+
updatedAt: Date
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Common Patterns
|
|
65
|
+
|
|
66
|
+
### Folder Picker in Settings
|
|
67
|
+
|
|
68
|
+
Let users select a folder, then display its contents in Render.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// hooks/store.ts
|
|
72
|
+
import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
|
|
73
|
+
|
|
74
|
+
export const useFolderIdState = createUseInstanceStoreState<string>('folderId', '')
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// views/Settings.tsx
|
|
79
|
+
import { useEffect, useState } from 'react'
|
|
80
|
+
import { media } from '@telemetryos/sdk'
|
|
81
|
+
import {
|
|
82
|
+
SettingsContainer,
|
|
83
|
+
SettingsField,
|
|
84
|
+
SettingsLabel,
|
|
85
|
+
SettingsSelectFrame,
|
|
86
|
+
} from '@telemetryos/sdk/react'
|
|
87
|
+
import { useFolderIdState } from '../hooks/store'
|
|
88
|
+
|
|
89
|
+
interface Folder {
|
|
90
|
+
id: string
|
|
91
|
+
name: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function Settings() {
|
|
95
|
+
const [isLoading, folderId, setFolderId] = useFolderIdState()
|
|
96
|
+
const [folders, setFolders] = useState<Folder[]>([])
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
media().getAllFolders().then(setFolders)
|
|
100
|
+
}, [])
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<SettingsContainer>
|
|
104
|
+
<SettingsField>
|
|
105
|
+
<SettingsLabel>Media Folder</SettingsLabel>
|
|
106
|
+
<SettingsSelectFrame>
|
|
107
|
+
<select
|
|
108
|
+
disabled={isLoading || folders.length === 0}
|
|
109
|
+
value={folderId}
|
|
110
|
+
onChange={(e) => setFolderId(e.target.value)}
|
|
111
|
+
>
|
|
112
|
+
<option value="">Select a folder...</option>
|
|
113
|
+
{folders.map(folder => (
|
|
114
|
+
<option key={folder.id} value={folder.id}>
|
|
115
|
+
{folder.name}
|
|
116
|
+
</option>
|
|
117
|
+
))}
|
|
118
|
+
</select>
|
|
119
|
+
</SettingsSelectFrame>
|
|
120
|
+
</SettingsField>
|
|
121
|
+
</SettingsContainer>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Image Gallery in Render
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// views/Render.tsx
|
|
130
|
+
import { useEffect, useState } from 'react'
|
|
131
|
+
import { media } from '@telemetryos/sdk'
|
|
132
|
+
import { useFolderIdState } from '../hooks/store'
|
|
133
|
+
|
|
134
|
+
interface MediaItem {
|
|
135
|
+
id: string
|
|
136
|
+
name: string
|
|
137
|
+
url: string
|
|
138
|
+
thumbnailUrl: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default function Render() {
|
|
142
|
+
const [isLoading, folderId] = useFolderIdState()
|
|
143
|
+
const [items, setItems] = useState<MediaItem[]>([])
|
|
144
|
+
const [loading, setLoading] = useState(false)
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (isLoading || !folderId) return
|
|
148
|
+
|
|
149
|
+
const fetchMedia = async () => {
|
|
150
|
+
setLoading(true)
|
|
151
|
+
try {
|
|
152
|
+
const content = await media().getAllByFolderId(folderId)
|
|
153
|
+
|
|
154
|
+
// Filter to images only
|
|
155
|
+
const images = content
|
|
156
|
+
.filter(item => item.contentType.startsWith('image/'))
|
|
157
|
+
.map(item => ({
|
|
158
|
+
id: item.id,
|
|
159
|
+
name: item.name,
|
|
160
|
+
url: item.publicUrls[0],
|
|
161
|
+
thumbnailUrl: item.thumbnailUrl,
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
setItems(images)
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error('Failed to load media:', err)
|
|
167
|
+
} finally {
|
|
168
|
+
setLoading(false)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fetchMedia()
|
|
173
|
+
}, [folderId, isLoading])
|
|
174
|
+
|
|
175
|
+
if (isLoading) return <div>Loading config...</div>
|
|
176
|
+
if (!folderId) return <div>Select a folder in Settings</div>
|
|
177
|
+
if (loading) return <div>Loading images...</div>
|
|
178
|
+
if (items.length === 0) return <div>No images in folder</div>
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="gallery">
|
|
182
|
+
{items.map(item => (
|
|
183
|
+
<img
|
|
184
|
+
key={item.id}
|
|
185
|
+
src={item.url}
|
|
186
|
+
alt={item.name}
|
|
187
|
+
loading="lazy"
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Video Player
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// views/Render.tsx
|
|
199
|
+
import { useEffect, useState } from 'react'
|
|
200
|
+
import { media } from '@telemetryos/sdk'
|
|
201
|
+
import { useVideoIdState } from '../hooks/store'
|
|
202
|
+
|
|
203
|
+
export default function Render() {
|
|
204
|
+
const [isLoading, videoId] = useVideoIdState()
|
|
205
|
+
const [videoUrl, setVideoUrl] = useState<string | null>(null)
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (isLoading || !videoId) return
|
|
209
|
+
|
|
210
|
+
media().getById(videoId).then(item => {
|
|
211
|
+
if (item.contentType.startsWith('video/')) {
|
|
212
|
+
setVideoUrl(item.publicUrls[0])
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
}, [videoId, isLoading])
|
|
216
|
+
|
|
217
|
+
if (isLoading) return <div>Loading...</div>
|
|
218
|
+
if (!videoUrl) return <div>No video selected</div>
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<video
|
|
222
|
+
src={videoUrl}
|
|
223
|
+
autoPlay
|
|
224
|
+
loop
|
|
225
|
+
muted
|
|
226
|
+
playsInline
|
|
227
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
228
|
+
/>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Content Picker by Tag
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Settings - let user select from tagged content
|
|
237
|
+
const [items, setItems] = useState<MediaContent[]>([])
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
media().getAllByTag('logo').then(setItems)
|
|
241
|
+
}, [])
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Slideshow with Auto-Advance
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { useEffect, useState } from 'react'
|
|
248
|
+
import { media } from '@telemetryos/sdk'
|
|
249
|
+
import { useFolderIdState, useIntervalState } from '../hooks/store'
|
|
250
|
+
|
|
251
|
+
export default function Render() {
|
|
252
|
+
const [, folderId] = useFolderIdState()
|
|
253
|
+
const [, interval] = useIntervalState() // seconds
|
|
254
|
+
|
|
255
|
+
const [images, setImages] = useState<string[]>([])
|
|
256
|
+
const [currentIndex, setCurrentIndex] = useState(0)
|
|
257
|
+
|
|
258
|
+
// Load images
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!folderId) return
|
|
261
|
+
|
|
262
|
+
media().getAllByFolderId(folderId).then(content => {
|
|
263
|
+
const urls = content
|
|
264
|
+
.filter(item => item.contentType.startsWith('image/'))
|
|
265
|
+
.map(item => item.publicUrls[0])
|
|
266
|
+
setImages(urls)
|
|
267
|
+
})
|
|
268
|
+
}, [folderId])
|
|
269
|
+
|
|
270
|
+
// Auto-advance
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (images.length <= 1) return
|
|
273
|
+
|
|
274
|
+
const timer = setInterval(() => {
|
|
275
|
+
setCurrentIndex(i => (i + 1) % images.length)
|
|
276
|
+
}, interval * 1000)
|
|
277
|
+
|
|
278
|
+
return () => clearInterval(timer)
|
|
279
|
+
}, [images.length, interval])
|
|
280
|
+
|
|
281
|
+
if (images.length === 0) return <div>No images</div>
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<img
|
|
285
|
+
src={images[currentIndex]}
|
|
286
|
+
alt={`Slide ${currentIndex + 1}`}
|
|
287
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
288
|
+
/>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Content Type Filtering
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const content = await media().getAllByFolderId(folderId)
|
|
297
|
+
|
|
298
|
+
// Images only
|
|
299
|
+
const images = content.filter(item => item.contentType.startsWith('image/'))
|
|
300
|
+
|
|
301
|
+
// Videos only
|
|
302
|
+
const videos = content.filter(item => item.contentType.startsWith('video/'))
|
|
303
|
+
|
|
304
|
+
// PDFs only
|
|
305
|
+
const pdfs = content.filter(item => item.contentType === 'application/pdf')
|
|
306
|
+
|
|
307
|
+
// Specific formats
|
|
308
|
+
const jpegs = content.filter(item => item.contentType === 'image/jpeg')
|
|
309
|
+
const mp4s = content.filter(item => item.contentType === 'video/mp4')
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Scheduling Support
|
|
313
|
+
|
|
314
|
+
Media items can have valid date ranges:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
const content = await media().getAllByFolderId(folderId)
|
|
318
|
+
|
|
319
|
+
const now = new Date()
|
|
320
|
+
const activeContent = content.filter(item => {
|
|
321
|
+
// Check if within valid date range
|
|
322
|
+
if (item.validFrom && new Date(item.validFrom) > now) return false
|
|
323
|
+
if (item.validTo && new Date(item.validTo) < now) return false
|
|
324
|
+
return true
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Tips
|
|
329
|
+
|
|
330
|
+
1. **Use publicUrls[0]** - First URL is the primary CDN URL
|
|
331
|
+
2. **Use thumbnailUrl for previews** - Smaller, faster loading
|
|
332
|
+
3. **Filter by contentType** - Ensure you're displaying compatible content
|
|
333
|
+
4. **Handle empty folders** - Show appropriate message when no content
|
|
334
|
+
5. **Lazy load images** - Use `loading="lazy"` for galleries
|
|
335
|
+
6. **Respect hidden flag** - Filter out hidden items unless intentional
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tos-proxy-fetch
|
|
3
|
+
description: REQUIRED for external API calls in TelemetryOS. MUST invoke BEFORE using proxy().fetch() or adding any third-party API integration. Contains CORS workaround patterns, useEffect dependencies, refresh intervals, and error handling.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TelemetryOS Proxy Fetch
|
|
7
|
+
|
|
8
|
+
When external APIs don't include CORS headers, browsers block requests from your app. The TelemetryOS proxy solves this by routing requests through the platform.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
### Use proxy().fetch() when:
|
|
13
|
+
- API returns CORS error in browser console
|
|
14
|
+
- API doesn't include `Access-Control-Allow-Origin` header
|
|
15
|
+
- You need to call APIs that weren't designed for browser use
|
|
16
|
+
|
|
17
|
+
### Use regular fetch() when:
|
|
18
|
+
- API includes CORS headers
|
|
19
|
+
- API is designed for browser/client-side use
|
|
20
|
+
- You want to use the player's advanced caching (regular fetch has better caching on devices)
|
|
21
|
+
|
|
22
|
+
## Quick Reference
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { proxy } from '@telemetryos/sdk'
|
|
26
|
+
|
|
27
|
+
// Simple GET
|
|
28
|
+
const response = await proxy().fetch('https://api.example.com/data')
|
|
29
|
+
const data = await response.json()
|
|
30
|
+
|
|
31
|
+
// With headers
|
|
32
|
+
const response = await proxy().fetch('https://api.example.com/data', {
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': 'Bearer YOUR_API_KEY',
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// POST request
|
|
40
|
+
const response = await proxy().fetch('https://api.example.com/data', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({ key: 'value' })
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Complete Example
|
|
50
|
+
|
|
51
|
+
### Settings (API Key + Endpoint Config)
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// hooks/store.ts
|
|
55
|
+
import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
|
|
56
|
+
|
|
57
|
+
export const useApiKeyState = createUseInstanceStoreState<string>('apiKey', '')
|
|
58
|
+
export const useEndpointState = createUseInstanceStoreState<string>('endpoint', '')
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// views/Settings.tsx
|
|
63
|
+
import {
|
|
64
|
+
SettingsContainer,
|
|
65
|
+
SettingsField,
|
|
66
|
+
SettingsLabel,
|
|
67
|
+
SettingsInputFrame,
|
|
68
|
+
} from '@telemetryos/sdk/react'
|
|
69
|
+
import { useApiKeyState, useEndpointState } from '../hooks/store'
|
|
70
|
+
|
|
71
|
+
export default function Settings() {
|
|
72
|
+
const [isLoadingKey, apiKey, setApiKey] = useApiKeyState()
|
|
73
|
+
const [isLoadingEndpoint, endpoint, setEndpoint] = useEndpointState()
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<SettingsContainer>
|
|
77
|
+
<SettingsField>
|
|
78
|
+
<SettingsLabel>API Key</SettingsLabel>
|
|
79
|
+
<SettingsInputFrame>
|
|
80
|
+
<input
|
|
81
|
+
type="password"
|
|
82
|
+
placeholder="Enter API key..."
|
|
83
|
+
disabled={isLoadingKey}
|
|
84
|
+
value={apiKey}
|
|
85
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
86
|
+
/>
|
|
87
|
+
</SettingsInputFrame>
|
|
88
|
+
</SettingsField>
|
|
89
|
+
|
|
90
|
+
<SettingsField>
|
|
91
|
+
<SettingsLabel>API Endpoint</SettingsLabel>
|
|
92
|
+
<SettingsInputFrame>
|
|
93
|
+
<input
|
|
94
|
+
type="url"
|
|
95
|
+
placeholder="https://api.example.com/data"
|
|
96
|
+
disabled={isLoadingEndpoint}
|
|
97
|
+
value={endpoint}
|
|
98
|
+
onChange={(e) => setEndpoint(e.target.value)}
|
|
99
|
+
/>
|
|
100
|
+
</SettingsInputFrame>
|
|
101
|
+
</SettingsField>
|
|
102
|
+
</SettingsContainer>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Render (Data Display)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// views/Render.tsx
|
|
111
|
+
import { useEffect, useState } from 'react'
|
|
112
|
+
import { proxy } from '@telemetryos/sdk'
|
|
113
|
+
import { useApiKeyState, useEndpointState } from '../hooks/store'
|
|
114
|
+
|
|
115
|
+
interface ApiData {
|
|
116
|
+
// Define your API response type
|
|
117
|
+
items: Array<{
|
|
118
|
+
id: string
|
|
119
|
+
name: string
|
|
120
|
+
value: number
|
|
121
|
+
}>
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default function Render() {
|
|
125
|
+
const [isLoadingKey, apiKey] = useApiKeyState()
|
|
126
|
+
const [isLoadingEndpoint, endpoint] = useEndpointState()
|
|
127
|
+
|
|
128
|
+
const [data, setData] = useState<ApiData | null>(null)
|
|
129
|
+
const [loading, setLoading] = useState(false)
|
|
130
|
+
const [error, setError] = useState<string | null>(null)
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (isLoadingKey || isLoadingEndpoint || !apiKey || !endpoint) return
|
|
134
|
+
|
|
135
|
+
const fetchData = async () => {
|
|
136
|
+
setLoading(true)
|
|
137
|
+
setError(null)
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const response = await proxy().fetch(endpoint, {
|
|
141
|
+
headers: {
|
|
142
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const json = await response.json()
|
|
152
|
+
setData(json)
|
|
153
|
+
} catch (err) {
|
|
154
|
+
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
155
|
+
} finally {
|
|
156
|
+
setLoading(false)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fetchData()
|
|
161
|
+
|
|
162
|
+
// Refresh every 5 minutes
|
|
163
|
+
const interval = setInterval(fetchData, 5 * 60 * 1000)
|
|
164
|
+
return () => clearInterval(interval)
|
|
165
|
+
}, [apiKey, endpoint, isLoadingKey, isLoadingEndpoint])
|
|
166
|
+
|
|
167
|
+
// Loading states
|
|
168
|
+
if (isLoadingKey || isLoadingEndpoint) return <div>Loading config...</div>
|
|
169
|
+
if (!apiKey || !endpoint) return <div>Configure API in Settings</div>
|
|
170
|
+
if (loading && !data) return <div>Loading data...</div>
|
|
171
|
+
if (error && !data) return <div>Error: {error}</div>
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div>
|
|
175
|
+
{data?.items.map(item => (
|
|
176
|
+
<div key={item.id}>
|
|
177
|
+
{item.name}: {item.value}
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Common API Patterns
|
|
186
|
+
|
|
187
|
+
### RSS Feed
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const response = await proxy().fetch('https://example.com/feed.xml')
|
|
191
|
+
const xml = await response.text()
|
|
192
|
+
|
|
193
|
+
// Parse XML (use DOMParser or a library)
|
|
194
|
+
const parser = new DOMParser()
|
|
195
|
+
const doc = parser.parseFromString(xml, 'text/xml')
|
|
196
|
+
const items = doc.querySelectorAll('item')
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### JSON API with Query Params
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const params = new URLSearchParams({
|
|
203
|
+
apiKey: apiKey,
|
|
204
|
+
city: city,
|
|
205
|
+
format: 'json',
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const response = await proxy().fetch(`https://api.example.com/data?${params}`)
|
|
209
|
+
const data = await response.json()
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Sports Data API
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const response = await proxy().fetch(
|
|
216
|
+
`https://api.sportsdata.io/v3/nfl/scores/json/Games/${season}`,
|
|
217
|
+
{
|
|
218
|
+
headers: {
|
|
219
|
+
'Ocp-Apim-Subscription-Key': apiKey,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const games = await response.json()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### News/Headlines API
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const response = await proxy().fetch(
|
|
231
|
+
`https://newsapi.org/v2/top-headlines?country=us&apiKey=${apiKey}`
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const news = await response.json()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Error Handling
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
try {
|
|
241
|
+
const response = await proxy().fetch(endpoint)
|
|
242
|
+
|
|
243
|
+
// Check HTTP status
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
if (response.status === 401) {
|
|
246
|
+
throw new Error('Invalid API key')
|
|
247
|
+
} else if (response.status === 404) {
|
|
248
|
+
throw new Error('Endpoint not found')
|
|
249
|
+
} else if (response.status === 429) {
|
|
250
|
+
throw new Error('Rate limit exceeded')
|
|
251
|
+
} else {
|
|
252
|
+
throw new Error(`API error: ${response.status}`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const data = await response.json()
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err instanceof Error) {
|
|
259
|
+
if (err.message.includes('timeout')) {
|
|
260
|
+
// SDK timeout (30 seconds)
|
|
261
|
+
console.error('Request timed out')
|
|
262
|
+
} else if (err.message.includes('network')) {
|
|
263
|
+
// Network error
|
|
264
|
+
console.error('Network error')
|
|
265
|
+
} else {
|
|
266
|
+
console.error(err.message)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Request Options
|
|
273
|
+
|
|
274
|
+
The proxy supports all standard fetch options:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
await proxy().fetch(url, {
|
|
278
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
|
|
279
|
+
headers: {
|
|
280
|
+
'Content-Type': 'application/json',
|
|
281
|
+
'Authorization': 'Bearer token',
|
|
282
|
+
// Any custom headers
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify(data), // For POST/PUT/PATCH
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Response Handling
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const response = await proxy().fetch(url)
|
|
292
|
+
|
|
293
|
+
// JSON
|
|
294
|
+
const json = await response.json()
|
|
295
|
+
|
|
296
|
+
// Text
|
|
297
|
+
const text = await response.text()
|
|
298
|
+
|
|
299
|
+
// Blob (for binary data)
|
|
300
|
+
const blob = await response.blob()
|
|
301
|
+
|
|
302
|
+
// Check content type
|
|
303
|
+
const contentType = response.headers.get('Content-Type')
|
|
304
|
+
if (contentType?.includes('application/json')) {
|
|
305
|
+
return response.json()
|
|
306
|
+
} else {
|
|
307
|
+
return response.text()
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Tips
|
|
312
|
+
|
|
313
|
+
1. **Check CORS first** - Try regular `fetch()` first; only use proxy if CORS fails
|
|
314
|
+
2. **Handle errors** - Always check `response.ok` before parsing
|
|
315
|
+
3. **Add refresh interval** - External data goes stale; refresh periodically
|
|
316
|
+
4. **Store API keys securely** - Use instance or application store hooks
|
|
317
|
+
5. **Show stale data** - Display last known data while refreshing
|
|
318
|
+
6. **Log errors** - Help users troubleshoot configuration issues
|
|
319
|
+
7. **Timeout awareness** - SDK times out after 30 seconds
|