@pplethai/cli 0.4.1 → 0.4.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/package.json +1 -1
- package/skills/pple-platform/references/design-system.md +7 -2
- package/templates/golden-path/package.json +1 -0
- package/templates/golden-path/web/App.tsx +129 -49
- package/templates/golden-path/web/app.css +13 -22
- package/templates/golden-path/web/main.tsx +1 -1
- package/templates/golden-path/web/pple.ts +13 -2
package/package.json
CHANGED
|
@@ -14,14 +14,14 @@ reinventing a design-system setup from `pple-erp` or other older apps.
|
|
|
14
14
|
import "@pplethai/components/styles.css";
|
|
15
15
|
import "./app.css";
|
|
16
16
|
|
|
17
|
-
import { Button, Card, CardContent,
|
|
17
|
+
import { Button, Card, CardContent, Navbar } from "@pplethai/components";
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Package entry points:
|
|
21
21
|
|
|
22
22
|
| Import | Contents |
|
|
23
23
|
| ------ | -------- |
|
|
24
|
-
| `@pplethai/components` | UI primitives, layout, brand (`Logo`, …), `cn()`, toasts |
|
|
24
|
+
| `@pplethai/components` | UI primitives, layout, brand (`Navbar`, `Logo`, …), `cn()`, toasts |
|
|
25
25
|
| `@pplethai/components/form` | react-hook-form helpers when you need forms |
|
|
26
26
|
| `@pplethai/components/styles.css` | Brand tokens — import once |
|
|
27
27
|
|
|
@@ -34,6 +34,11 @@ the bare `@pplethai/components` import to `dist/index.js`.
|
|
|
34
34
|
- Put **page layout** in `web/app.css` (mobile-first, safe-area padding for
|
|
35
35
|
the WebView). Do not fight the design-system stylesheet with a second
|
|
36
36
|
full Tailwind stack unless you need it.
|
|
37
|
+
- Top-level chrome uses Shared `<Navbar>` (see `web/App.tsx`). Do not hand-roll
|
|
38
|
+
a header/app bar with `Logo`. With a client router, pass `renderHomeLink` /
|
|
39
|
+
`renderLink` (as in pple-erp); otherwise default `<a href>` links are fine.
|
|
40
|
+
Navbar is `position: fixed` below the `md` breakpoint — pad the page content
|
|
41
|
+
accordingly (the template's `.screen` already does).
|
|
37
42
|
- Compose screens from design-system components (`Card`, `Button`, `Input`,
|
|
38
43
|
`Textarea`, `Badge`, `Alert`, …) like the demo cards in `web/App.tsx`.
|
|
39
44
|
- **Colors and type** come from CSS variables in `styles.css`
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Badge,
|
|
3
2
|
Button,
|
|
4
3
|
Card,
|
|
5
4
|
CardContent,
|
|
6
5
|
CardDescription,
|
|
7
6
|
CardHeader,
|
|
8
7
|
CardTitle,
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
Icon,
|
|
9
15
|
Input,
|
|
10
|
-
|
|
16
|
+
Navbar,
|
|
11
17
|
Spinner,
|
|
12
18
|
Textarea,
|
|
13
19
|
showToast,
|
|
14
20
|
} from "@pplethai/components";
|
|
21
|
+
import { ChevronDown, LogOut, Plus, Send, Upload, User } from "lucide-react";
|
|
15
22
|
import { useEffect, useRef, useState } from "react";
|
|
16
23
|
import type { Me, Note, UploadedFile } from "../shared/api";
|
|
17
|
-
import { api, apiJson } from "./pple";
|
|
24
|
+
import { api, apiJson, logout } from "./pple";
|
|
18
25
|
|
|
19
26
|
function bytes(n: number): string {
|
|
20
27
|
if (n < 1024) return `${n} B`;
|
|
@@ -26,35 +33,89 @@ function message(e: unknown): string {
|
|
|
26
33
|
return e instanceof Error ? e.message : String(e);
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
// Starter nav — replace with your routes. With a client router, pass
|
|
37
|
+
// renderHomeLink / renderLink (see pple-erp's Navbar wrapper).
|
|
38
|
+
const NAV_ITEMS = [{ href: "/", label: "หน้าหลัก", end: true }];
|
|
39
|
+
|
|
29
40
|
export function App() {
|
|
30
41
|
const [me, setMe] = useState<Me | null>(null);
|
|
31
42
|
|
|
32
43
|
useEffect(() => {
|
|
33
44
|
apiJson<Me>("/api/me").then(setMe).catch((e) => showToast({
|
|
34
|
-
title: "
|
|
45
|
+
title: "โหลดโปรไฟล์ไม่สำเร็จ",
|
|
35
46
|
description: message(e),
|
|
36
47
|
variant: "error",
|
|
37
48
|
}));
|
|
38
49
|
}, []);
|
|
39
50
|
|
|
51
|
+
const displayName = me?.actor.name ?? me?.actor.sub ?? "";
|
|
52
|
+
const roleLabels = me
|
|
53
|
+
? [me.effective.activeRole, ...me.effective.extraRoles]
|
|
54
|
+
: [];
|
|
55
|
+
|
|
40
56
|
return (
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
|
|
57
|
+
<>
|
|
58
|
+
<Navbar
|
|
59
|
+
title="PPLE App"
|
|
60
|
+
pathname={window.location.pathname}
|
|
61
|
+
items={NAV_ITEMS}
|
|
62
|
+
mobileMenuAriaLabel={{ open: "เปิดเมนู", close: "ปิดเมนู" }}
|
|
63
|
+
navAriaLabel="เมนูหลัก"
|
|
64
|
+
>
|
|
65
|
+
{me && (
|
|
66
|
+
<DropdownMenu>
|
|
67
|
+
<DropdownMenuTrigger asChild>
|
|
68
|
+
<Button variant="outline" size="default" className="gap-2">
|
|
69
|
+
<Icon icon={User} />
|
|
70
|
+
<span>{displayName}</span>
|
|
71
|
+
<Icon icon={ChevronDown} size="sm" />
|
|
72
|
+
</Button>
|
|
73
|
+
</DropdownMenuTrigger>
|
|
74
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
75
|
+
<DropdownMenuLabel>
|
|
76
|
+
<div className="font-medium">{displayName}</div>
|
|
77
|
+
{me.actor.mobile && (
|
|
78
|
+
<div className="text-xs text-muted-foreground font-normal">
|
|
79
|
+
{me.actor.mobile}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
{me.effective.isDelegated && (
|
|
83
|
+
<div className="text-xs text-muted-foreground font-normal">
|
|
84
|
+
(ปฏิบัติงานแทน — สิทธิ์เป็นของหัวหน้า)
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</DropdownMenuLabel>
|
|
88
|
+
<DropdownMenuSeparator />
|
|
89
|
+
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal">
|
|
90
|
+
บทบาทของคุณ
|
|
91
|
+
</DropdownMenuLabel>
|
|
92
|
+
{roleLabels.map((label) => (
|
|
93
|
+
<DropdownMenuItem key={label} disabled className="opacity-70">
|
|
94
|
+
{label}
|
|
95
|
+
</DropdownMenuItem>
|
|
96
|
+
))}
|
|
97
|
+
<DropdownMenuSeparator />
|
|
98
|
+
<DropdownMenuItem onClick={() => void logout()}>
|
|
99
|
+
<Icon icon={LogOut} />
|
|
100
|
+
ออกจากระบบ
|
|
101
|
+
</DropdownMenuItem>
|
|
102
|
+
</DropdownMenuContent>
|
|
103
|
+
</DropdownMenu>
|
|
104
|
+
)}
|
|
105
|
+
</Navbar>
|
|
47
106
|
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
107
|
+
<main className="screen">
|
|
108
|
+
<div className="stack">
|
|
109
|
+
<p className="muted">
|
|
110
|
+
นี่คือเทมเพลตเริ่มต้น แต่ละการ์ดด้านล่างแสดงความสามารถหนึ่งอย่างของ
|
|
111
|
+
แพลตฟอร์ม แทนที่ด้วยแอปของคุณเองได้เลย
|
|
112
|
+
</p>
|
|
113
|
+
<NotesCard />
|
|
114
|
+
<FilesCard />
|
|
115
|
+
<NotifyCard />
|
|
116
|
+
</div>
|
|
117
|
+
</main>
|
|
118
|
+
</>
|
|
58
119
|
);
|
|
59
120
|
}
|
|
60
121
|
|
|
@@ -68,7 +129,7 @@ function NotesCard() {
|
|
|
68
129
|
apiJson<{ notes: Note[] }>("/api/notes")
|
|
69
130
|
.then((r) => setNotes(r.notes))
|
|
70
131
|
.catch((e) => {
|
|
71
|
-
showToast({ title: "
|
|
132
|
+
showToast({ title: "โหลดบันทึกไม่สำเร็จ", description: message(e), variant: "error" });
|
|
72
133
|
// Land on the empty state. Leaving this null would spin forever.
|
|
73
134
|
setNotes([]);
|
|
74
135
|
});
|
|
@@ -89,7 +150,7 @@ function NotesCard() {
|
|
|
89
150
|
setBody("");
|
|
90
151
|
await load();
|
|
91
152
|
} catch (e) {
|
|
92
|
-
showToast({ title: "
|
|
153
|
+
showToast({ title: "บันทึกไม่สำเร็จ", description: message(e), variant: "error" });
|
|
93
154
|
} finally {
|
|
94
155
|
setSaving(false);
|
|
95
156
|
}
|
|
@@ -98,32 +159,35 @@ function NotesCard() {
|
|
|
98
159
|
return (
|
|
99
160
|
<Card className="card">
|
|
100
161
|
<CardHeader>
|
|
101
|
-
<CardTitle
|
|
102
|
-
<CardDescription
|
|
162
|
+
<CardTitle>บันทึก</CardTitle>
|
|
163
|
+
<CardDescription>อ่านและเขียนฐานข้อมูล D1 ของแอป</CardDescription>
|
|
103
164
|
</CardHeader>
|
|
104
165
|
<CardContent>
|
|
105
166
|
<div className="stack">
|
|
106
167
|
<div className="row">
|
|
107
168
|
<Input
|
|
108
169
|
value={body}
|
|
109
|
-
placeholder="
|
|
170
|
+
placeholder="เขียนบันทึก…"
|
|
110
171
|
onChange={(e) => setBody(e.target.value)}
|
|
111
172
|
onKeyDown={(e) => e.key === "Enter" && add()}
|
|
112
173
|
/>
|
|
113
174
|
<Button onClick={add} disabled={saving || !body.trim()}>
|
|
114
|
-
{saving ? <Spinner /> :
|
|
175
|
+
{saving ? <Spinner /> : <Icon icon={Plus} />}
|
|
176
|
+
เพิ่ม
|
|
115
177
|
</Button>
|
|
116
178
|
</div>
|
|
117
179
|
{notes === null ? (
|
|
118
180
|
<Spinner />
|
|
119
181
|
) : notes.length === 0 ? (
|
|
120
|
-
<p className="muted"
|
|
182
|
+
<p className="muted">ยังไม่มีบันทึก</p>
|
|
121
183
|
) : (
|
|
122
184
|
<ul className="list">
|
|
123
185
|
{notes.map((n) => (
|
|
124
186
|
<li key={n.id} className="list__item">
|
|
125
187
|
<span>{n.body}</span>
|
|
126
|
-
<span className="muted">
|
|
188
|
+
<span className="muted">
|
|
189
|
+
{new Date(n.createdAt).toLocaleString("th-TH")}
|
|
190
|
+
</span>
|
|
127
191
|
</li>
|
|
128
192
|
))}
|
|
129
193
|
</ul>
|
|
@@ -144,7 +208,7 @@ function FilesCard() {
|
|
|
144
208
|
apiJson<{ files: UploadedFile[] }>("/api/files")
|
|
145
209
|
.then((r) => setFiles(r.files))
|
|
146
210
|
.catch((e) => {
|
|
147
|
-
showToast({ title: "
|
|
211
|
+
showToast({ title: "โหลดไฟล์ไม่สำเร็จ", description: message(e), variant: "error" });
|
|
148
212
|
// Land on the empty state. Leaving this null would spin forever.
|
|
149
213
|
setFiles([]);
|
|
150
214
|
});
|
|
@@ -159,11 +223,11 @@ function FilesCard() {
|
|
|
159
223
|
const fd = new FormData();
|
|
160
224
|
fd.append("file", file);
|
|
161
225
|
const res = await api("/api/files", { method: "POST", body: fd });
|
|
162
|
-
if (!res.ok) throw new Error(
|
|
226
|
+
if (!res.ok) throw new Error(`อัปโหลดไม่สำเร็จ (${res.status})`);
|
|
163
227
|
if (inputRef.current) inputRef.current.value = "";
|
|
164
228
|
await load();
|
|
165
229
|
} catch (e) {
|
|
166
|
-
showToast({ title: "
|
|
230
|
+
showToast({ title: "อัปโหลดไฟล์ไม่สำเร็จ", description: message(e), variant: "error" });
|
|
167
231
|
} finally {
|
|
168
232
|
setUploading(false);
|
|
169
233
|
}
|
|
@@ -172,27 +236,34 @@ function FilesCard() {
|
|
|
172
236
|
return (
|
|
173
237
|
<Card className="card">
|
|
174
238
|
<CardHeader>
|
|
175
|
-
<CardTitle
|
|
176
|
-
<CardDescription
|
|
239
|
+
<CardTitle>ไฟล์</CardTitle>
|
|
240
|
+
<CardDescription>อัปโหลดไปยังบัคเก็ต R2 ของแอป</CardDescription>
|
|
177
241
|
</CardHeader>
|
|
178
242
|
<CardContent>
|
|
179
243
|
<div className="stack">
|
|
180
244
|
<div className="row">
|
|
181
|
-
{/*
|
|
182
|
-
a concurrent upload. */}
|
|
245
|
+
{/* Hidden native picker; the design-system Button opens it. Disabled
|
|
246
|
+
while uploading so a second pick can't start a concurrent upload. */}
|
|
183
247
|
<input
|
|
184
248
|
ref={inputRef}
|
|
185
249
|
type="file"
|
|
186
|
-
className="
|
|
250
|
+
className="hidden"
|
|
187
251
|
onChange={upload}
|
|
188
252
|
disabled={uploading}
|
|
189
253
|
/>
|
|
190
|
-
|
|
254
|
+
<Button
|
|
255
|
+
variant="outline"
|
|
256
|
+
disabled={uploading}
|
|
257
|
+
onClick={() => inputRef.current?.click()}
|
|
258
|
+
>
|
|
259
|
+
{uploading ? <Spinner /> : <Icon icon={Upload} />}
|
|
260
|
+
เลือกไฟล์
|
|
261
|
+
</Button>
|
|
191
262
|
</div>
|
|
192
263
|
{files === null ? (
|
|
193
264
|
<Spinner />
|
|
194
265
|
) : files.length === 0 ? (
|
|
195
|
-
<p className="muted"
|
|
266
|
+
<p className="muted">ยังไม่มีไฟล์</p>
|
|
196
267
|
) : (
|
|
197
268
|
<ul className="list">
|
|
198
269
|
{files.map((f) => (
|
|
@@ -226,20 +297,20 @@ function NotifyCard() {
|
|
|
226
297
|
});
|
|
227
298
|
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
|
228
299
|
if (res.ok) {
|
|
229
|
-
showToast({ title: "
|
|
300
|
+
showToast({ title: "ส่งการแจ้งเตือนแล้ว", variant: "success" });
|
|
230
301
|
setTitle("");
|
|
231
302
|
setBody("");
|
|
232
303
|
} else if (data.error === "notifications_not_configured") {
|
|
233
304
|
showToast({
|
|
234
|
-
title: "
|
|
235
|
-
description: "
|
|
305
|
+
title: "ยังไม่ได้ตั้งค่าการแจ้งเตือน",
|
|
306
|
+
description: "ตั้งค่า NOTIFY_API_BASE และ NOTIFY_API_KEY เพื่อส่งจริง",
|
|
236
307
|
variant: "warning",
|
|
237
308
|
});
|
|
238
309
|
} else {
|
|
239
|
-
showToast({ title: "
|
|
310
|
+
showToast({ title: "ส่งไม่สำเร็จ", description: data.error, variant: "error" });
|
|
240
311
|
}
|
|
241
312
|
} catch (e) {
|
|
242
|
-
showToast({ title: "
|
|
313
|
+
showToast({ title: "ส่งไม่สำเร็จ", description: message(e), variant: "error" });
|
|
243
314
|
} finally {
|
|
244
315
|
setSending(false);
|
|
245
316
|
}
|
|
@@ -248,18 +319,27 @@ function NotifyCard() {
|
|
|
248
319
|
return (
|
|
249
320
|
<Card className="card">
|
|
250
321
|
<CardHeader>
|
|
251
|
-
<CardTitle
|
|
322
|
+
<CardTitle>แจ้งเตือนผู้ใช้</CardTitle>
|
|
252
323
|
<CardDescription>
|
|
253
|
-
|
|
254
|
-
|
|
324
|
+
ส่งการแจ้งเตือนแบบผูกกับกลุ่มผู้ใช้แอป — ส่งเฉพาะเนื้อหา
|
|
325
|
+
ไม่ส่งหมายเลขโทรศัพท์
|
|
255
326
|
</CardDescription>
|
|
256
327
|
</CardHeader>
|
|
257
328
|
<CardContent>
|
|
258
329
|
<div className="stack">
|
|
259
|
-
<Input
|
|
260
|
-
|
|
330
|
+
<Input
|
|
331
|
+
value={title}
|
|
332
|
+
placeholder="หัวข้อ"
|
|
333
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
334
|
+
/>
|
|
335
|
+
<Textarea
|
|
336
|
+
value={body}
|
|
337
|
+
placeholder="ข้อความ"
|
|
338
|
+
onChange={(e) => setBody(e.target.value)}
|
|
339
|
+
/>
|
|
261
340
|
<Button onClick={send} disabled={sending || !title.trim() || !body.trim()}>
|
|
262
|
-
{sending ? <Spinner /> :
|
|
341
|
+
{sending ? <Spinner /> : <Icon icon={Send} />}
|
|
342
|
+
ส่งการแจ้งเตือน
|
|
263
343
|
</Button>
|
|
264
344
|
</div>
|
|
265
345
|
</CardContent>
|
|
@@ -17,34 +17,23 @@ body {
|
|
|
17
17
|
max-width: 640px;
|
|
18
18
|
margin: 0 auto;
|
|
19
19
|
padding: 16px;
|
|
20
|
+
/* Navbar is position:fixed below the md breakpoint; give content room. */
|
|
21
|
+
padding-top: 4.5rem;
|
|
20
22
|
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
|
21
23
|
min-height: 100vh;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
@media (min-width: 768px) {
|
|
27
|
+
.screen {
|
|
28
|
+
padding-top: 16px;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
.screen--center {
|
|
25
33
|
display: grid;
|
|
26
34
|
place-items: center;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
.appbar {
|
|
30
|
-
display: flex;
|
|
31
|
-
align-items: center;
|
|
32
|
-
gap: 12px;
|
|
33
|
-
padding: 8px 4px 16px;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.appbar__logo {
|
|
37
|
-
height: 28px;
|
|
38
|
-
width: auto;
|
|
39
|
-
color: #ff6a13; /* brand primary */
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.appbar__title {
|
|
43
|
-
font-weight: 600;
|
|
44
|
-
font-size: 18px;
|
|
45
|
-
margin-right: auto;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
37
|
.stack {
|
|
49
38
|
display: flex;
|
|
50
39
|
flex-direction: column;
|
|
@@ -86,7 +75,9 @@ body {
|
|
|
86
75
|
font-size: 14px;
|
|
87
76
|
}
|
|
88
77
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
/* Icon defaults to text-foreground, which overrides the parent Button text
|
|
79
|
+
colour (primary-foreground on filled buttons, …). Inherit so the glyph
|
|
80
|
+
matches its label — including hover states on outline/ghost buttons. */
|
|
81
|
+
button svg.text-foreground {
|
|
82
|
+
color: inherit;
|
|
92
83
|
}
|
|
@@ -26,7 +26,7 @@ function ErrorScreen({ message }: { message: string }) {
|
|
|
26
26
|
return (
|
|
27
27
|
<main className="screen screen--center">
|
|
28
28
|
<Alert variant="destructive" className="card">
|
|
29
|
-
<AlertTitle
|
|
29
|
+
<AlertTitle>เริ่มต้นแอปไม่สำเร็จ</AlertTitle>
|
|
30
30
|
<AlertDescription>{message}</AlertDescription>
|
|
31
31
|
</Alert>
|
|
32
32
|
</main>
|
|
@@ -30,6 +30,7 @@ export type Bootstrap =
|
|
|
30
30
|
| { kind: "error"; message: string };
|
|
31
31
|
|
|
32
32
|
let token: string | null = null;
|
|
33
|
+
let sdk: PPLEMiniApp | null = null;
|
|
33
34
|
|
|
34
35
|
/** Establish the session. Call once at startup, before rendering the app. */
|
|
35
36
|
export async function bootstrap(): Promise<Bootstrap> {
|
|
@@ -48,11 +49,11 @@ export async function bootstrap(): Promise<Bootstrap> {
|
|
|
48
49
|
return {
|
|
49
50
|
kind: "error",
|
|
50
51
|
message:
|
|
51
|
-
"
|
|
52
|
+
"แอปตั้งค่าไม่ถูกต้อง: การตั้งค่าเข้าสู่ระบบ PPLE ID ไม่ได้ถูกตั้งตอนสร้างแอป โปรดดีพลอยใหม่จากแพลตฟอร์ม",
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
sdk = new PPLEMiniApp({
|
|
56
57
|
oauthUrl: OAUTH_URL,
|
|
57
58
|
oauthClientId: OAUTH_CLIENT_ID,
|
|
58
59
|
oauthRedirectUri: OAUTH_REDIRECT_URI,
|
|
@@ -65,6 +66,16 @@ export async function bootstrap(): Promise<Bootstrap> {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** Drop the session and return to the start (re-triggers PPLE ID login). */
|
|
70
|
+
export async function logout(): Promise<void> {
|
|
71
|
+
token = null;
|
|
72
|
+
if (sdk) {
|
|
73
|
+
await sdk.logout();
|
|
74
|
+
sdk = null;
|
|
75
|
+
}
|
|
76
|
+
window.location.href = "/";
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
/**
|
|
69
80
|
* Call your Worker API with the bearer token attached. Paths are relative
|
|
70
81
|
* ("/api/notes"), since the SPA and the Worker share one origin.
|