api-ape 1.0.1 → 1.0.2
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 +280 -83
- package/example/NextJs/pages/Info.tsx +153 -0
- package/example/NextJs/pages/index.tsx +121 -39
- package/example/NextJs/styles/Chat.module.css +255 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,53 +1,25 @@
|
|
|
1
1
|
# 🦍 api-ape
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/api-ape)
|
|
4
|
+
[](https://github.com/codemeasandwich/api-ape/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/codemeasandwich/api-ape/issues)
|
|
4
6
|
|
|
5
|
-
Call server functions from the browser like local methods. Get real-time broadcasts with zero setup.
|
|
6
|
-
|
|
7
|
-
```js
|
|
8
|
-
// Client: call server function, get result
|
|
9
|
-
const pets = await ape.pets.list()
|
|
10
|
-
|
|
11
|
-
// Client: listen for broadcasts
|
|
12
|
-
ape.on('newPet', ({ data }) => console.log('New pet:', data))
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
file: api/pets/list.js
|
|
16
|
-
```js
|
|
17
|
-
// Server: define function, broadcast to others
|
|
18
|
-
module.exports = function list() {
|
|
19
|
-
return getPetList()
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
file: api/pets/newPet.js
|
|
24
|
-
```js
|
|
25
|
-
// Server: define function, broadcast to others
|
|
26
|
-
module.exports = function newPet(data) {
|
|
27
|
-
// broadcast to all other clients
|
|
28
|
-
this.broadcastOthers('newPet', data)
|
|
29
|
-
return savePet(data)
|
|
30
|
-
}
|
|
31
|
-
```
|
|
7
|
+
**Remote Procedure Events (RPE)** — A lightweight WebSocket framework for building real-time APIs. Call server functions from the browser like local methods. Get real-time broadcasts with zero setup.
|
|
32
8
|
|
|
33
9
|
---
|
|
34
10
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
- **🔌 Auto-wiring** — Drop JS files in a folder, they become API endpoints
|
|
38
|
-
- **📡 Real-time** — Built-in broadcast to all or other clients
|
|
39
|
-
- **🔄 Reconnection** — Client auto-reconnects on disconnect
|
|
40
|
-
- **📦 JJS Encoding** — Supports Date, RegExp, Error, Set, Map, undefined over the wire
|
|
41
|
-
- **🎯 Simple API** — Promise-based calls with chainable paths
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Installation
|
|
11
|
+
## Install
|
|
46
12
|
|
|
47
13
|
```bash
|
|
48
14
|
npm install api-ape
|
|
15
|
+
# or
|
|
16
|
+
pnpm add api-ape
|
|
17
|
+
# or
|
|
18
|
+
yarn add api-ape
|
|
49
19
|
```
|
|
50
20
|
|
|
21
|
+
**Requirements:** Node.js 14+ (for server), modern browsers (for client)
|
|
22
|
+
|
|
51
23
|
---
|
|
52
24
|
|
|
53
25
|
## Quick Start
|
|
@@ -66,9 +38,9 @@ ape(app, { where: 'api' })
|
|
|
66
38
|
app.listen(3000)
|
|
67
39
|
```
|
|
68
40
|
|
|
69
|
-
###
|
|
41
|
+
### Create a Controller
|
|
70
42
|
|
|
71
|
-
|
|
43
|
+
Drop a file in your `api/` folder — it automatically becomes an endpoint:
|
|
72
44
|
|
|
73
45
|
```js
|
|
74
46
|
// api/hello.js
|
|
@@ -77,24 +49,16 @@ module.exports = function(name) {
|
|
|
77
49
|
}
|
|
78
50
|
```
|
|
79
51
|
|
|
80
|
-
```js
|
|
81
|
-
// api/message.js
|
|
82
|
-
module.exports = function(data) {
|
|
83
|
-
// Broadcast to all OTHER connected clients
|
|
84
|
-
this.broadcastOthers('message', data)
|
|
85
|
-
return data
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
52
|
### Client (Browser)
|
|
90
53
|
|
|
91
|
-
Include the bundled client:
|
|
54
|
+
Include the bundled client and start calling:
|
|
92
55
|
|
|
93
56
|
```html
|
|
94
57
|
<script src="/api/ape.js"></script>
|
|
95
58
|
<script>
|
|
96
|
-
// Call server functions
|
|
97
|
-
ape.hello('World')
|
|
59
|
+
// Call server functions like local methods
|
|
60
|
+
const result = await ape.hello('World')
|
|
61
|
+
console.log(result) // "Hello, World!"
|
|
98
62
|
|
|
99
63
|
// Listen for broadcasts
|
|
100
64
|
ape.on('message', ({ data }) => {
|
|
@@ -103,6 +67,19 @@ Include the bundled client:
|
|
|
103
67
|
</script>
|
|
104
68
|
```
|
|
105
69
|
|
|
70
|
+
**That's it!** Your server function is now callable from the browser.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Key Concepts
|
|
75
|
+
|
|
76
|
+
* **Auto-wiring** — Drop JS files in a folder, they become API endpoints automatically
|
|
77
|
+
* **Real-time broadcasts** — Built-in `broadcast()` and `broadcastOthers()` methods for pushing to clients
|
|
78
|
+
* **Promise-based calls** — Chainable paths like `ape.users.list()` map to `api/users/list.js`
|
|
79
|
+
* **Automatic reconnection** — Client auto-reconnects on disconnect with exponential backoff
|
|
80
|
+
* **JJS Encoding** — Extended JSON supporting Date, RegExp, Error, Set, Map, undefined, and circular refs
|
|
81
|
+
* **Connection lifecycle hooks** — Customize behavior on connect, receive, send, error, and disconnect
|
|
82
|
+
|
|
106
83
|
---
|
|
107
84
|
|
|
108
85
|
## API Reference
|
|
@@ -115,8 +92,8 @@ Initialize api-ape on an Express app.
|
|
|
115
92
|
|
|
116
93
|
| Option | Type | Description |
|
|
117
94
|
|--------|------|-------------|
|
|
118
|
-
| `where` | `string` | Directory containing controller files |
|
|
119
|
-
| `onConnent` | `function` | Connection lifecycle hook (see
|
|
95
|
+
| `where` | `string` | Directory containing controller files (default: `'api'`) |
|
|
96
|
+
| `onConnent` | `function` | Connection lifecycle hook (see [Connection Lifecycle](#connection-lifecycle)) |
|
|
120
97
|
|
|
121
98
|
#### Controller Context (`this`)
|
|
122
99
|
|
|
@@ -124,8 +101,8 @@ Inside controller functions, `this` provides:
|
|
|
124
101
|
|
|
125
102
|
| Property | Description |
|
|
126
103
|
|----------|-------------|
|
|
127
|
-
| `this.broadcast(type, data)` | Send to ALL connected clients |
|
|
128
|
-
| `this.broadcastOthers(type, data)` | Send to all EXCEPT the caller |
|
|
104
|
+
| `this.broadcast(type, data)` | Send to **ALL** connected clients |
|
|
105
|
+
| `this.broadcastOthers(type, data)` | Send to all **EXCEPT** the caller |
|
|
129
106
|
| `this.online()` | Get count of connected clients |
|
|
130
107
|
| `this.getClients()` | Get array of connected hostIds |
|
|
131
108
|
| `this.hostId` | Unique ID of the calling client |
|
|
@@ -133,17 +110,61 @@ Inside controller functions, `this` provides:
|
|
|
133
110
|
| `this.socket` | WebSocket instance |
|
|
134
111
|
| `this.agent` | Parsed user-agent (browser, OS, device) |
|
|
135
112
|
|
|
136
|
-
|
|
113
|
+
### Client
|
|
114
|
+
|
|
115
|
+
#### `ape.<path>.<method>(...args)`
|
|
116
|
+
|
|
117
|
+
Call a server function. Returns a Promise.
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
// Calls api/users/list.js
|
|
121
|
+
const users = await ape.users.list()
|
|
122
|
+
|
|
123
|
+
// Calls api/users/create.js with data
|
|
124
|
+
const user = await ape.users.create({ name: 'Alice' })
|
|
125
|
+
|
|
126
|
+
// Nested paths work too
|
|
127
|
+
// ape.admin.users -> api/admin/users.js
|
|
128
|
+
// ape.admin.users.delete -> api/admin/users/delete.js
|
|
129
|
+
await ape.admin.users.delete(userId)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `ape.on(type, handler)`
|
|
133
|
+
|
|
134
|
+
Listen for server broadcasts.
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
ape.on('notification', ({ data, err, type }) => {
|
|
138
|
+
console.log('Received:', data)
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
### Default Options
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
ape(app, {
|
|
150
|
+
where: 'api', // Controller directory
|
|
151
|
+
onConnent: undefined // Lifecycle hook (optional)
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Connection Lifecycle Hook
|
|
156
|
+
|
|
157
|
+
Customize behavior per connection:
|
|
137
158
|
|
|
138
159
|
```js
|
|
139
160
|
ape(app, {
|
|
140
161
|
where: 'api',
|
|
141
|
-
onConnent(socket, req,
|
|
162
|
+
onConnent(socket, req, hostId) {
|
|
142
163
|
return {
|
|
143
164
|
// Embed values into `this` for all controllers
|
|
144
165
|
embed: {
|
|
145
166
|
userId: req.session?.userId,
|
|
146
|
-
clientId:
|
|
167
|
+
clientId: String(hostId)
|
|
147
168
|
},
|
|
148
169
|
|
|
149
170
|
// Before/after hooks
|
|
@@ -164,31 +185,81 @@ ape(app, {
|
|
|
164
185
|
})
|
|
165
186
|
```
|
|
166
187
|
|
|
167
|
-
|
|
188
|
+
---
|
|
168
189
|
|
|
169
|
-
|
|
190
|
+
## Common Recipes
|
|
170
191
|
|
|
171
|
-
|
|
192
|
+
### Broadcast to Other Clients
|
|
172
193
|
|
|
173
194
|
```js
|
|
174
|
-
//
|
|
175
|
-
|
|
195
|
+
// api/message.js
|
|
196
|
+
module.exports = function(data) {
|
|
197
|
+
// Broadcast to all OTHER connected clients (not the sender)
|
|
198
|
+
this.broadcastOthers('message', data)
|
|
199
|
+
return { success: true }
|
|
200
|
+
}
|
|
201
|
+
```
|
|
176
202
|
|
|
177
|
-
|
|
178
|
-
ape.users.create({ name: 'Alice' }).then(user => ...)
|
|
203
|
+
### Broadcast to All Clients
|
|
179
204
|
|
|
180
|
-
|
|
181
|
-
|
|
205
|
+
```js
|
|
206
|
+
// api/announcement.js
|
|
207
|
+
module.exports = function(announcement) {
|
|
208
|
+
// Broadcast to ALL connected clients including sender
|
|
209
|
+
this.broadcast('announcement', announcement)
|
|
210
|
+
return { sent: true }
|
|
211
|
+
}
|
|
182
212
|
```
|
|
183
213
|
|
|
184
|
-
|
|
214
|
+
### Get Online Count
|
|
185
215
|
|
|
186
|
-
|
|
216
|
+
```js
|
|
217
|
+
// api/stats.js
|
|
218
|
+
module.exports = function() {
|
|
219
|
+
return {
|
|
220
|
+
online: this.online(),
|
|
221
|
+
clients: this.getClients()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Access Request Data
|
|
187
227
|
|
|
188
228
|
```js
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
229
|
+
// api/profile.js
|
|
230
|
+
module.exports = function() {
|
|
231
|
+
// Access original HTTP request
|
|
232
|
+
const userId = this.req.session?.userId
|
|
233
|
+
const userAgent = this.agent.browser.name
|
|
234
|
+
|
|
235
|
+
return { userId, userAgent }
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Error Handling
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
// api/data.js
|
|
243
|
+
module.exports = async function(id) {
|
|
244
|
+
try {
|
|
245
|
+
const data = await fetchData(id)
|
|
246
|
+
return data
|
|
247
|
+
} catch (err) {
|
|
248
|
+
// Errors are automatically sent to client
|
|
249
|
+
throw new Error(`Failed to fetch: ${err.message}`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Client-Side Error Handling
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
try {
|
|
258
|
+
const result = await ape.data.get(id)
|
|
259
|
+
console.log(result)
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error('Server error:', err)
|
|
262
|
+
}
|
|
192
263
|
```
|
|
193
264
|
|
|
194
265
|
---
|
|
@@ -207,25 +278,143 @@ api-ape uses **JJS (JSON SuperSet)** encoding, which extends JSON to support:
|
|
|
207
278
|
| `Map` | ✅ Preserved as Map |
|
|
208
279
|
| Circular refs | ✅ Handled via pointers |
|
|
209
280
|
|
|
210
|
-
This is automatic — send a Date, receive a Date.
|
|
281
|
+
This is automatic — send a Date, receive a Date. No configuration needed.
|
|
211
282
|
|
|
212
283
|
---
|
|
213
284
|
|
|
214
|
-
## Examples
|
|
285
|
+
## Examples & Demos
|
|
286
|
+
|
|
287
|
+
The repository contains working examples:
|
|
215
288
|
|
|
216
|
-
|
|
289
|
+
* **`example/ExpressJs/`** — Simple real-time chat app
|
|
290
|
+
- Minimal setup with Express.js
|
|
291
|
+
- Broadcast messages to other clients
|
|
292
|
+
- Message history
|
|
217
293
|
|
|
218
|
-
|
|
219
|
-
-
|
|
294
|
+
* **`example/NextJs/`** — Production-ready chat application
|
|
295
|
+
- Custom Next.js server integration
|
|
296
|
+
- React hooks integration
|
|
297
|
+
- User presence tracking
|
|
298
|
+
- Docker support
|
|
299
|
+
- Connection lifecycle hooks
|
|
220
300
|
|
|
221
|
-
Run
|
|
301
|
+
### Run an Example
|
|
222
302
|
|
|
303
|
+
**ExpressJs:**
|
|
223
304
|
```bash
|
|
224
305
|
cd example/ExpressJs
|
|
225
306
|
npm install
|
|
226
307
|
npm start
|
|
308
|
+
# Open http://localhost:3000
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**NextJs:**
|
|
312
|
+
```bash
|
|
313
|
+
cd example/NextJs
|
|
314
|
+
npm install
|
|
315
|
+
npm run dev
|
|
316
|
+
# Open http://localhost:3000
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Or with Docker:
|
|
320
|
+
```bash
|
|
321
|
+
cd example/NextJs
|
|
322
|
+
docker-compose up --build
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Troubleshooting & FAQ
|
|
328
|
+
|
|
329
|
+
### CORS Errors in Browser
|
|
330
|
+
|
|
331
|
+
Ensure your Express server allows WebSocket connections from your origin. api-ape uses `express-ws` which handles CORS automatically, but verify your Express CORS middleware allows WebSocket upgrade requests.
|
|
332
|
+
|
|
333
|
+
### Controller Not Found
|
|
334
|
+
|
|
335
|
+
* Check that your controller file is in the `where` directory (default: `api/`)
|
|
336
|
+
* Ensure the file exports a function: `module.exports = function(...) { ... }`
|
|
337
|
+
* File paths map directly: `api/users/list.js` → `ape.users.list()`
|
|
338
|
+
|
|
339
|
+
### Connection Drops Frequently
|
|
340
|
+
|
|
341
|
+
The client automatically reconnects with exponential backoff. If connections drop often:
|
|
342
|
+
* Check server WebSocket timeout settings
|
|
343
|
+
* Verify network stability
|
|
344
|
+
* Check server logs for errors
|
|
345
|
+
|
|
346
|
+
### Binary Data / File Uploads
|
|
347
|
+
|
|
348
|
+
JJS encoding supports complex types, but for large binary data, consider:
|
|
349
|
+
* Sending file URLs instead of raw data
|
|
350
|
+
* Using a separate file upload endpoint
|
|
351
|
+
* Chunking large payloads
|
|
352
|
+
|
|
353
|
+
### TypeScript Support
|
|
354
|
+
|
|
355
|
+
Type definitions are included (`index.d.ts`). For full type safety, you may need to:
|
|
356
|
+
* Define interfaces for your controller parameters and return types
|
|
357
|
+
* Use type assertions when calling `ape.<path>.<method>()`
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Tests & CI
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npm test # Run test suite
|
|
365
|
+
npm run test:watch # Watch mode
|
|
366
|
+
npm run test:cover # Coverage report
|
|
227
367
|
```
|
|
228
368
|
|
|
369
|
+
**Test Commands:**
|
|
370
|
+
- `npm test` — Run all tests
|
|
371
|
+
- `npm run test:watch` — Watch mode for development
|
|
372
|
+
- `npm run test:cover` — Generate coverage report
|
|
373
|
+
- `npm run test:update` — Update snapshots
|
|
374
|
+
|
|
375
|
+
**Supported:** Node.js 14+, modern browsers (Chrome, Firefox, Safari, Edge)
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Contributing
|
|
380
|
+
|
|
381
|
+
Contributions welcome! Here's how to help:
|
|
382
|
+
|
|
383
|
+
1. **Fork the repository**
|
|
384
|
+
2. **Create a branch:** `git checkout -b feature/your-feature-name`
|
|
385
|
+
3. **Make your changes** and add tests
|
|
386
|
+
4. **Run tests:** `npm test`
|
|
387
|
+
5. **Commit:** Follow conventional commit messages
|
|
388
|
+
6. **Push and open a PR** with a clear description
|
|
389
|
+
|
|
390
|
+
**Guidelines:**
|
|
391
|
+
* Add tests for new features
|
|
392
|
+
* Keep code style consistent
|
|
393
|
+
* Update documentation if needed
|
|
394
|
+
* Ensure all tests pass
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Releases / Changelog
|
|
399
|
+
|
|
400
|
+
Versioning follows [Semantic Versioning](https://semver.org/).
|
|
401
|
+
|
|
402
|
+
**Current version:** See `package.json` or npm registry
|
|
403
|
+
|
|
404
|
+
**Release notes:** Check [GitHub releases](https://github.com/codemeasandwich/api-ape/releases) for detailed changelog.
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Security
|
|
409
|
+
|
|
410
|
+
**Reporting vulnerabilities:** Please report security issues via [GitHub Security Advisories](https://github.com/codemeasandwich/api-ape/security/advisories) or email the maintainer.
|
|
411
|
+
|
|
412
|
+
**Security considerations:**
|
|
413
|
+
* Validate all input in controllers
|
|
414
|
+
* Use authentication/authorization in `onConnent` hooks
|
|
415
|
+
* Sanitize data before broadcasting
|
|
416
|
+
* Keep dependencies up to date
|
|
417
|
+
|
|
229
418
|
---
|
|
230
419
|
|
|
231
420
|
## Project Structure
|
|
@@ -245,7 +434,7 @@ api-ape/
|
|
|
245
434
|
│ │ ├── receive.js # Incoming message handler
|
|
246
435
|
│ │ └── send.js # Outgoing message handler
|
|
247
436
|
│ └── security/
|
|
248
|
-
│ └── reply.js
|
|
437
|
+
│ └── reply.js # Duplicate request protection
|
|
249
438
|
├── utils/
|
|
250
439
|
│ ├── jss.js # JSON SuperSet encoder/decoder
|
|
251
440
|
│ └── messageHash.js # Request deduplication
|
|
@@ -256,6 +445,14 @@ api-ape/
|
|
|
256
445
|
|
|
257
446
|
---
|
|
258
447
|
|
|
259
|
-
## License
|
|
448
|
+
## License & Authors
|
|
449
|
+
|
|
450
|
+
**License:** MIT
|
|
451
|
+
|
|
452
|
+
**Author:** [Brian Shannon](https://github.com/codemeasandwich)
|
|
453
|
+
|
|
454
|
+
**Repository:** [github.com/codemeasandwich/api-ape](https://github.com/codemeasandwich/api-ape)
|
|
455
|
+
|
|
456
|
+
---
|
|
260
457
|
|
|
261
|
-
|
|
458
|
+
**Made with 🦍 by the api-ape community**
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import styles from '../styles/Chat.module.css'
|
|
2
|
+
|
|
3
|
+
export default function Info() {
|
|
4
|
+
return (
|
|
5
|
+
<div className={styles.codeSection}>
|
|
6
|
+
<h3 className={styles.codeTitle}>📚 How api-ape Works</h3>
|
|
7
|
+
|
|
8
|
+
<div className={styles.gridContainer}>
|
|
9
|
+
<div className={styles.gridLayout}>
|
|
10
|
+
{/* Top Left: Key Concepts */}
|
|
11
|
+
<div>
|
|
12
|
+
<h4 className={styles.sectionHeading}>
|
|
13
|
+
💡 Key Concepts
|
|
14
|
+
</h4>
|
|
15
|
+
<pre className={styles.code}>
|
|
16
|
+
{`• Proxy Pattern: api.message() → api/message.js
|
|
17
|
+
• Auto-wiring: Drop files in api/ folder, they become endpoints
|
|
18
|
+
• Promises: All calls return Promises automatically
|
|
19
|
+
• Broadcasts: Use this.broadcast() or this.broadcastOthers()
|
|
20
|
+
• Context: this.broadcast, this.hostId, this.req available in controllers
|
|
21
|
+
• Auto-reconnect: Client reconnects automatically on disconnect`}
|
|
22
|
+
</pre>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{/* Top Right: Data Flow */}
|
|
26
|
+
<div>
|
|
27
|
+
<h4 className={styles.sectionHeadingLarge}>
|
|
28
|
+
🔄 Data Flow
|
|
29
|
+
</h4>
|
|
30
|
+
<div className={styles.dataFlowGrid}>
|
|
31
|
+
{/* Column Headers */}
|
|
32
|
+
<div className={styles.columnHeaderClient}>
|
|
33
|
+
Client
|
|
34
|
+
</div>
|
|
35
|
+
<div className={styles.gridCell}></div>
|
|
36
|
+
<div className={styles.columnHeaderServer}>
|
|
37
|
+
Server
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Step 1: Client sends */}
|
|
41
|
+
<div className={styles.clientBoxSpan3}>
|
|
42
|
+
api.message(data)
|
|
43
|
+
</div>
|
|
44
|
+
<div className={styles.arrowContainerRow2}>
|
|
45
|
+
<div className={styles.arrowLineSend}></div>
|
|
46
|
+
<span className={styles.arrowLabelBlue}>Send</span>
|
|
47
|
+
<div className={styles.arrowHeadRight}></div>
|
|
48
|
+
</div>
|
|
49
|
+
<div className={styles.emptyGridCell}></div>
|
|
50
|
+
|
|
51
|
+
{/* Step 2: Server receives */}
|
|
52
|
+
<div className={styles.emptyGridCellRow3}></div>
|
|
53
|
+
<div className={styles.arrowContainerRow3}>
|
|
54
|
+
<div className={styles.arrowHeadLeft}></div>
|
|
55
|
+
<span className={styles.arrowLabelGreen}>Return</span>
|
|
56
|
+
<div className={styles.arrowLineReturn}></div>
|
|
57
|
+
</div>
|
|
58
|
+
<div className={styles.serverBoxSpan2}>
|
|
59
|
+
api/message.js
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Step 3: Server broadcasts */}
|
|
63
|
+
<div className={styles.emptyGridCellRow4}></div>
|
|
64
|
+
<div className={styles.arrowContainerRow4}>
|
|
65
|
+
<div className={styles.arrowLineBroadcast}></div>
|
|
66
|
+
<span className={styles.arrowLabelGreen}>Broadcast</span>
|
|
67
|
+
<div className={styles.arrowHeadRight}></div>
|
|
68
|
+
</div>
|
|
69
|
+
<div className={styles.serverBoxSpan3}>
|
|
70
|
+
Broadcast to others
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Step 4: Other clients receive */}
|
|
74
|
+
<div className={styles.clientBoxSingle}>
|
|
75
|
+
Other clients
|
|
76
|
+
</div>
|
|
77
|
+
<div className={styles.arrowContainerRow5}>
|
|
78
|
+
<div className={styles.arrowHeadLeftBlue}></div>
|
|
79
|
+
<span className={styles.arrowLabelBlue}>Broadcast</span>
|
|
80
|
+
<div className={styles.arrowLineBroadcastReturn}></div>
|
|
81
|
+
</div>
|
|
82
|
+
<div className={styles.emptyGridCellRow5}></div>
|
|
83
|
+
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Bottom Left: Client-Side */}
|
|
88
|
+
<div>
|
|
89
|
+
<h4 className={styles.sectionHeading}>
|
|
90
|
+
🔵 Client-Side (Browser)
|
|
91
|
+
</h4>
|
|
92
|
+
<pre className={styles.code}>
|
|
93
|
+
{`// 1. Initialize api-ape client
|
|
94
|
+
const client = await getApeClient()
|
|
95
|
+
const api = client.sender // Proxy object
|
|
96
|
+
|
|
97
|
+
// 2. Call server function - property name = file path
|
|
98
|
+
// api.message() → calls api/message.js
|
|
99
|
+
api.message({ user: 'Alice', text: 'Hello!' })
|
|
100
|
+
.then(response => {
|
|
101
|
+
// Server returned: { ok: true, message: {...} }
|
|
102
|
+
console.log('Response:', response)
|
|
103
|
+
})
|
|
104
|
+
.catch(err => {
|
|
105
|
+
// Server threw an error
|
|
106
|
+
console.error('Error:', err)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// 3. Listen for server broadcasts
|
|
110
|
+
client.setOnReciver('message', ({ data }) => {
|
|
111
|
+
// Server called: this.broadcastOthers('message', data)
|
|
112
|
+
// This fires for ALL clients except the sender
|
|
113
|
+
console.log('Broadcast received:', data.message)
|
|
114
|
+
})`}
|
|
115
|
+
</pre>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Bottom Right: Server-Side */}
|
|
119
|
+
<div>
|
|
120
|
+
<h4 className={styles.sectionHeading}>
|
|
121
|
+
🟢 Server-Side (api/message.js)
|
|
122
|
+
</h4>
|
|
123
|
+
<pre className={styles.code}>
|
|
124
|
+
{`// File: api/message.js
|
|
125
|
+
// This function is called when client does: api.message(data)
|
|
126
|
+
|
|
127
|
+
module.exports = function message(data) {
|
|
128
|
+
const { user, text } = data
|
|
129
|
+
|
|
130
|
+
// Validate input
|
|
131
|
+
if (!user || !text) {
|
|
132
|
+
throw new Error('Missing user or text')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const msg = {
|
|
136
|
+
user,
|
|
137
|
+
text,
|
|
138
|
+
time: new Date().toISOString()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Broadcast to ALL OTHER clients (not the sender)
|
|
142
|
+
this.broadcastOthers('message', { message: msg })
|
|
143
|
+
|
|
144
|
+
// Return response to sender (fulfills Promise)
|
|
145
|
+
return { ok: true, message: msg }
|
|
146
|
+
}`}
|
|
147
|
+
</pre>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦍 api-ape Next.js Chat Example
|
|
3
|
+
*
|
|
4
|
+
* This component demonstrates how to use api-ape in a React/Next.js application:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Client Initialization**: Connect to api-ape WebSocket server
|
|
7
|
+
* 2. **Proxy Pattern**: Use `client.sender` as a Proxy to call server functions
|
|
8
|
+
* 3. **Event Listeners**: Listen for server broadcasts using `setOnReciver`
|
|
9
|
+
* 4. **Promise-based Calls**: Server functions return Promises automatically
|
|
10
|
+
*
|
|
11
|
+
* Server-side: api/message.js handles incoming messages and broadcasts to other clients
|
|
12
|
+
* Client-side: This component sends messages and receives broadcasts
|
|
13
|
+
*
|
|
14
|
+
* Key api-ape concepts:
|
|
15
|
+
* - `client.sender` is a Proxy - accessing `sender.message()` calls server function
|
|
16
|
+
* - Property name (`message`) maps to server file: `api/message.js`
|
|
17
|
+
* - `setOnReciver(type, handler)` listens for server broadcasts
|
|
18
|
+
* - All calls return Promises - server response is automatically matched by queryId
|
|
19
|
+
*/
|
|
20
|
+
|
|
1
21
|
import Head from 'next/head'
|
|
2
22
|
import { useState, useEffect, useRef } from 'react'
|
|
3
23
|
import styles from '../styles/Chat.module.css'
|
|
4
24
|
import { getApeClient } from '../ape/client'
|
|
25
|
+
import Info from './Info'
|
|
5
26
|
|
|
6
27
|
export default function Home() {
|
|
28
|
+
// Component state
|
|
7
29
|
const [messages, setMessages] = useState([])
|
|
8
30
|
const [input, setInput] = useState('')
|
|
9
31
|
const [username, setUsername] = useState('')
|
|
@@ -11,67 +33,133 @@ export default function Home() {
|
|
|
11
33
|
const [userCount, setUserCount] = useState(0)
|
|
12
34
|
const [sending, setSending] = useState(false)
|
|
13
35
|
const [connected, setConnected] = useState(false)
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
|
|
37
|
+
// Refs
|
|
38
|
+
const apiRef = useRef(null) // Stores the api-ape sender Proxy
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Initialize api-ape client on component mount
|
|
42
|
+
*
|
|
43
|
+
* This effect:
|
|
44
|
+
* 1. Gets the api-ape client singleton (connects to WebSocket)
|
|
45
|
+
* 2. Stores the `sender` Proxy in a ref for later use
|
|
46
|
+
* 3. Sets up event listeners for server broadcasts
|
|
47
|
+
*
|
|
48
|
+
* The client auto-reconnects if the connection drops.
|
|
49
|
+
*/
|
|
18
50
|
useEffect(() => {
|
|
51
|
+
// Skip on server-side rendering
|
|
19
52
|
if (typeof window === 'undefined') return
|
|
20
53
|
|
|
21
54
|
getApeClient().then((client) => {
|
|
22
55
|
if (!client) return
|
|
23
56
|
|
|
24
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Store the sender Proxy
|
|
59
|
+
*
|
|
60
|
+
* `client.sender` is a Proxy object that allows you to call server functions
|
|
61
|
+
* by accessing properties. For example:
|
|
62
|
+
* - `sender.message(data)` calls `api/message.js` on the server
|
|
63
|
+
* - The property name (`message`) maps to the server file path
|
|
64
|
+
* - All calls return Promises that resolve with the server's response
|
|
65
|
+
*/
|
|
25
66
|
apiRef.current = client.sender
|
|
26
67
|
setConnected(true)
|
|
27
|
-
console.log('🦍 api-ape
|
|
68
|
+
console.log('🦍 api-ape client connected')
|
|
28
69
|
|
|
29
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Set up event listeners for server broadcasts
|
|
72
|
+
*
|
|
73
|
+
* `setOnReciver(type, handler)` listens for broadcasts from the server.
|
|
74
|
+
* The server can broadcast using `this.broadcast()` or `this.broadcastOthers()`
|
|
75
|
+
* in controller functions (see api/message.js).
|
|
76
|
+
*
|
|
77
|
+
* Broadcast types:
|
|
78
|
+
* - 'init': Initial data when client connects (history, user count)
|
|
79
|
+
* - 'message': New message from another client
|
|
80
|
+
* - 'users': Updated user count
|
|
81
|
+
*/
|
|
30
82
|
client.setOnReciver('init', ({ data }) => {
|
|
83
|
+
// Server sent initial data (happens on connect)
|
|
31
84
|
setMessages(data.history || [])
|
|
32
85
|
setUserCount(data.users || 0)
|
|
33
|
-
console.log('🦍 Initialized')
|
|
86
|
+
console.log('🦍 Initialized with', data.history?.length || 0, 'messages')
|
|
34
87
|
})
|
|
35
88
|
|
|
36
89
|
client.setOnReciver('message', ({ data }) => {
|
|
90
|
+
// Server broadcasted a new message from another client
|
|
91
|
+
// This is NOT the response to our own send - it's a broadcast!
|
|
37
92
|
setMessages(prev => [...prev, data.message])
|
|
38
93
|
})
|
|
39
94
|
|
|
40
95
|
client.setOnReciver('users', ({ data }) => {
|
|
96
|
+
// Server broadcasted updated user count
|
|
41
97
|
setUserCount(data.count)
|
|
42
98
|
})
|
|
43
99
|
})
|
|
44
100
|
}, [])
|
|
45
101
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Send a message to the server
|
|
104
|
+
*
|
|
105
|
+
* This demonstrates the api-ape Proxy pattern:
|
|
106
|
+
*
|
|
107
|
+
* 1. Access `api.message()` - the property name 'message' maps to `api/message.js`
|
|
108
|
+
* 2. Call it with data - returns a Promise
|
|
109
|
+
* 3. Server processes the request in `api/message.js`
|
|
110
|
+
* 4. Server can:
|
|
111
|
+
* - Return a value (fulfills the Promise)
|
|
112
|
+
* - Broadcast to other clients using `this.broadcastOthers()`
|
|
113
|
+
* - Throw an error (rejects the Promise)
|
|
114
|
+
*
|
|
115
|
+
* The Promise resolves with whatever the server function returns.
|
|
116
|
+
* The server also broadcasts to other clients (see api/message.js).
|
|
117
|
+
*/
|
|
54
118
|
const sendMessage = (e) => {
|
|
55
119
|
e.preventDefault()
|
|
56
120
|
if (!input.trim() || !apiRef.current || sending) return
|
|
57
121
|
|
|
58
122
|
const api = apiRef.current
|
|
59
|
-
|
|
60
123
|
setSending(true)
|
|
61
124
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Call server function using Proxy pattern
|
|
127
|
+
*
|
|
128
|
+
* `api.message({ user, text })`:
|
|
129
|
+
* - Calls the `message` function in `api/message.js`
|
|
130
|
+
* - Sends `{ user, text }` as the function argument
|
|
131
|
+
* - Returns a Promise that resolves with the server's return value
|
|
132
|
+
* - Server automatically broadcasts to other clients (see api/message.js)
|
|
133
|
+
*
|
|
134
|
+
* The server function receives the data and can:
|
|
135
|
+
* - Validate input
|
|
136
|
+
* - Store the message
|
|
137
|
+
* - Broadcast to others: `this.broadcastOthers('message', { message: msg })`
|
|
138
|
+
* - Return a response: `return { ok: true, message: msg }`
|
|
139
|
+
*/
|
|
65
140
|
api.message({ user: username, text: input })
|
|
66
141
|
.then((response) => {
|
|
67
|
-
|
|
68
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Server responded successfully
|
|
144
|
+
*
|
|
145
|
+
* The response is whatever the server function returned.
|
|
146
|
+
* In this case, api/message.js returns: `{ ok: true, message: msg }`
|
|
147
|
+
*
|
|
148
|
+
* Note: Other clients receive the message via broadcast (setOnReciver above),
|
|
149
|
+
* but we add it here from the server's response to show it immediately.
|
|
150
|
+
*/
|
|
69
151
|
if (response?.message) {
|
|
70
152
|
setMessages(prev => [...prev, response.message])
|
|
71
153
|
}
|
|
72
154
|
setSending(false)
|
|
73
155
|
})
|
|
74
156
|
.catch((err) => {
|
|
157
|
+
/**
|
|
158
|
+
* Server function threw an error or connection failed
|
|
159
|
+
*
|
|
160
|
+
* Errors from server functions are automatically caught and
|
|
161
|
+
* the Promise is rejected with the error.
|
|
162
|
+
*/
|
|
75
163
|
console.error('Send failed:', err)
|
|
76
164
|
setSending(false)
|
|
77
165
|
})
|
|
@@ -79,6 +167,10 @@ export default function Home() {
|
|
|
79
167
|
setInput('')
|
|
80
168
|
}
|
|
81
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Handle user joining the chat
|
|
172
|
+
* Simply sets the joined state to show the chat interface
|
|
173
|
+
*/
|
|
82
174
|
const handleJoin = (e) => {
|
|
83
175
|
e.preventDefault()
|
|
84
176
|
if (username.trim()) {
|
|
@@ -98,7 +190,13 @@ export default function Home() {
|
|
|
98
190
|
🦍 <span className={styles.gradient}>api-ape</span> Chat
|
|
99
191
|
</h1>
|
|
100
192
|
<p className={styles.subtitle}>
|
|
101
|
-
{connected ?
|
|
193
|
+
{connected ? (
|
|
194
|
+
userCount === 1
|
|
195
|
+
? '✅ Connected • Only You are online'
|
|
196
|
+
: userCount > 1
|
|
197
|
+
? `✅ Connected • You + ${userCount - 1} are online`
|
|
198
|
+
: '✅ Connected'
|
|
199
|
+
) : '⏳ Connecting...'}
|
|
102
200
|
</p>
|
|
103
201
|
|
|
104
202
|
{!joined ? (
|
|
@@ -140,7 +238,6 @@ export default function Home() {
|
|
|
140
238
|
</span>
|
|
141
239
|
</div>
|
|
142
240
|
))}
|
|
143
|
-
<div ref={messagesEndRef} />
|
|
144
241
|
</div>
|
|
145
242
|
|
|
146
243
|
<form onSubmit={sendMessage} className={styles.inputForm}>
|
|
@@ -160,22 +257,7 @@ export default function Home() {
|
|
|
160
257
|
</div>
|
|
161
258
|
)}
|
|
162
259
|
|
|
163
|
-
<
|
|
164
|
-
<h3 className={styles.codeTitle}>✨ api-ape Proxy Pattern</h3>
|
|
165
|
-
<pre className={styles.code}>
|
|
166
|
-
{`// Sender is a Proxy - prop name = type
|
|
167
|
-
const api = client.sender
|
|
168
|
-
|
|
169
|
-
// Send with Promise - queryId auto-matched
|
|
170
|
-
setSending(true)
|
|
171
|
-
api.message({ user, text })
|
|
172
|
-
.then(() => setSending(false))
|
|
173
|
-
.catch(err => console.error(err))
|
|
174
|
-
|
|
175
|
-
// createdAt auto-added by client
|
|
176
|
-
// queryId hash matches request/response`}
|
|
177
|
-
</pre>
|
|
178
|
-
</div>
|
|
260
|
+
<Info />
|
|
179
261
|
</main>
|
|
180
262
|
</div>
|
|
181
263
|
)
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.main {
|
|
9
|
-
max-width:
|
|
9
|
+
max-width: 1400px;
|
|
10
10
|
margin: 0 auto;
|
|
11
11
|
padding: 2rem;
|
|
12
|
+
width: 100%;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
.title {
|
|
@@ -192,3 +193,256 @@
|
|
|
192
193
|
padding: 0.2rem 0.5rem;
|
|
193
194
|
border-radius: 4px;
|
|
194
195
|
}
|
|
196
|
+
|
|
197
|
+
.gridContainer {
|
|
198
|
+
max-width: 1200px;
|
|
199
|
+
margin: 1.5rem auto 0;
|
|
200
|
+
padding: 0 1rem;
|
|
201
|
+
width: 100%;
|
|
202
|
+
box-sizing: border-box;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.gridLayout {
|
|
206
|
+
display: grid;
|
|
207
|
+
grid-template-columns: 1fr;
|
|
208
|
+
gap: 2rem;
|
|
209
|
+
width: 100%;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@media (min-width: 768px) {
|
|
213
|
+
.gridLayout {
|
|
214
|
+
grid-template-columns: 1fr 1fr;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@media (max-width: 767px) {
|
|
219
|
+
.gridContainer {
|
|
220
|
+
padding: 0 0.5rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.main {
|
|
224
|
+
padding: 1rem;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Info component styles */
|
|
229
|
+
.sectionHeading {
|
|
230
|
+
margin-bottom: 0.5rem;
|
|
231
|
+
font-size: 0.9rem;
|
|
232
|
+
font-weight: bold;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.sectionHeadingLarge {
|
|
236
|
+
margin-bottom: 1rem;
|
|
237
|
+
font-size: 0.9rem;
|
|
238
|
+
font-weight: bold;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.dataFlowGrid {
|
|
242
|
+
display: grid;
|
|
243
|
+
grid-template-columns: 200px 1fr 200px;
|
|
244
|
+
grid-template-rows: auto auto auto auto auto;
|
|
245
|
+
gap: 1rem;
|
|
246
|
+
align-items: stretch;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.columnHeaderClient {
|
|
250
|
+
font-size: 0.8rem;
|
|
251
|
+
font-weight: bold;
|
|
252
|
+
text-align: center;
|
|
253
|
+
grid-row: 1;
|
|
254
|
+
grid-column: 1;
|
|
255
|
+
color: #00d2ff;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.columnHeaderServer {
|
|
259
|
+
font-size: 0.8rem;
|
|
260
|
+
font-weight: bold;
|
|
261
|
+
text-align: center;
|
|
262
|
+
grid-row: 1;
|
|
263
|
+
grid-column: 3;
|
|
264
|
+
color: #00e676;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.gridCell {
|
|
268
|
+
grid-row: 1;
|
|
269
|
+
grid-column: 2;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.clientBoxSpan3 {
|
|
273
|
+
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
|
|
274
|
+
padding: 0.75rem 1rem;
|
|
275
|
+
border-radius: 8px;
|
|
276
|
+
color: #fff;
|
|
277
|
+
font-size: 0.75rem;
|
|
278
|
+
font-weight: bold;
|
|
279
|
+
box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
grid-column: 1;
|
|
284
|
+
grid-row: 2 / 5;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.clientBoxSingle {
|
|
288
|
+
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
|
|
289
|
+
padding: 0.75rem 1rem;
|
|
290
|
+
border-radius: 8px;
|
|
291
|
+
color: #fff;
|
|
292
|
+
font-size: 0.75rem;
|
|
293
|
+
font-weight: bold;
|
|
294
|
+
box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: center;
|
|
298
|
+
grid-column: 1;
|
|
299
|
+
grid-row: 5;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.serverBoxSpan2 {
|
|
303
|
+
background: linear-gradient(135deg, #00c851, #00e676);
|
|
304
|
+
padding: 0.75rem 1rem;
|
|
305
|
+
border-radius: 8px;
|
|
306
|
+
color: #fff;
|
|
307
|
+
font-size: 0.75rem;
|
|
308
|
+
font-weight: bold;
|
|
309
|
+
box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
grid-column: 3;
|
|
314
|
+
grid-row: 2 / 4;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.serverBoxSpan3 {
|
|
318
|
+
background: linear-gradient(135deg, #00c851, #00e676);
|
|
319
|
+
padding: 0.75rem 1rem;
|
|
320
|
+
border-radius: 8px;
|
|
321
|
+
color: #fff;
|
|
322
|
+
font-size: 0.75rem;
|
|
323
|
+
font-weight: bold;
|
|
324
|
+
box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
justify-content: center;
|
|
328
|
+
grid-column: 3;
|
|
329
|
+
grid-row: 4 / 6;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.arrowContainerRow2 {
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: center;
|
|
336
|
+
gap: 0.5rem;
|
|
337
|
+
grid-column: 2;
|
|
338
|
+
grid-row: 2;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.arrowContainerRow3 {
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
justify-content: center;
|
|
345
|
+
gap: 0.5rem;
|
|
346
|
+
grid-column: 2;
|
|
347
|
+
grid-row: 3;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.arrowContainerRow4 {
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
justify-content: center;
|
|
354
|
+
gap: 0.5rem;
|
|
355
|
+
grid-column: 2;
|
|
356
|
+
grid-row: 4;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.arrowContainerRow5 {
|
|
360
|
+
display: flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
justify-content: center;
|
|
363
|
+
gap: 0.5rem;
|
|
364
|
+
grid-column: 2;
|
|
365
|
+
grid-row: 5;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.arrowLineSend {
|
|
369
|
+
flex: 1;
|
|
370
|
+
height: 2px;
|
|
371
|
+
background: linear-gradient(90deg, #00d2ff, #00e676);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.arrowLineReturn {
|
|
375
|
+
flex: 1;
|
|
376
|
+
height: 2px;
|
|
377
|
+
background: linear-gradient(90deg, #00e676, transparent);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.arrowLineBroadcast {
|
|
381
|
+
flex: 1;
|
|
382
|
+
height: 2px;
|
|
383
|
+
background: linear-gradient(90deg, transparent, #00e676);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.arrowLineBroadcastReturn {
|
|
387
|
+
flex: 1;
|
|
388
|
+
height: 2px;
|
|
389
|
+
background: linear-gradient(90deg, #00d2ff, transparent);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.arrowLabelBlue {
|
|
393
|
+
font-size: 0.7rem;
|
|
394
|
+
white-space: nowrap;
|
|
395
|
+
padding: 0 0.5rem;
|
|
396
|
+
color: #00d2ff;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.arrowLabelGreen {
|
|
400
|
+
font-size: 0.7rem;
|
|
401
|
+
white-space: nowrap;
|
|
402
|
+
padding: 0 0.5rem;
|
|
403
|
+
color: #00e676;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.arrowHeadRight {
|
|
407
|
+
width: 0;
|
|
408
|
+
height: 0;
|
|
409
|
+
border-top: 4px solid transparent;
|
|
410
|
+
border-bottom: 4px solid transparent;
|
|
411
|
+
border-left: 8px solid #00e676;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.arrowHeadLeft {
|
|
415
|
+
width: 0;
|
|
416
|
+
height: 0;
|
|
417
|
+
border-top: 4px solid transparent;
|
|
418
|
+
border-bottom: 4px solid transparent;
|
|
419
|
+
border-right: 8px solid #00e676;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.arrowHeadLeftBlue {
|
|
423
|
+
width: 0;
|
|
424
|
+
height: 0;
|
|
425
|
+
border-top: 4px solid transparent;
|
|
426
|
+
border-bottom: 4px solid transparent;
|
|
427
|
+
border-right: 8px solid #00d2ff;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.emptyGridCell {
|
|
431
|
+
grid-row: 2;
|
|
432
|
+
grid-column: 3;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.emptyGridCellRow3 {
|
|
436
|
+
grid-row: 3;
|
|
437
|
+
grid-column: 1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.emptyGridCellRow4 {
|
|
441
|
+
grid-row: 4;
|
|
442
|
+
grid-column: 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.emptyGridCellRow5 {
|
|
446
|
+
grid-row: 5;
|
|
447
|
+
grid-column: 3;
|
|
448
|
+
}
|