@edpire/sdk 0.6.10 → 0.6.11
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/bin/create.js +2 -1
- package/dist/index.mjs +7 -7
- package/dist/react.mjs +8 -8
- package/dist/umd/index.global.js +96 -96
- package/examples/nextjs/.env.example +3 -5
- package/examples/nextjs/README.md +76 -66
- package/examples/nextjs/app/api/edpire/assessments/route.ts +36 -0
- package/examples/nextjs/app/api/edpire/token/route.ts +16 -23
- package/examples/nextjs/app/layout.tsx +1 -1
- package/examples/nextjs/app/page.tsx +113 -34
- package/examples/nextjs/docs/screenshots/01-pick.jpg +0 -0
- package/examples/nextjs/docs/screenshots/02-play.jpg +0 -0
- package/examples/nextjs/docs/screenshots/03-graded.jpg +0 -0
- package/examples/nextjs/next.config.ts +3 -2
- package/examples/nextjs/package.json +2 -2
- package/examples/vite-express/.env.example +4 -7
- package/examples/vite-express/README.md +89 -69
- package/examples/vite-express/docs/screenshots/01-pick.jpg +0 -0
- package/examples/vite-express/docs/screenshots/02-play.jpg +0 -0
- package/examples/vite-express/docs/screenshots/03-graded.jpg +0 -0
- package/examples/vite-express/index.html +1 -1
- package/examples/vite-express/package.json +4 -6
- package/examples/vite-express/server.ts +102 -96
- package/examples/vite-express/src/App.tsx +128 -56
- package/examples/vite-express/tsconfig.server.json +6 -6
- package/package.json +1 -1
- package/examples/nextjs/app/globals.css +0 -9
- /package/dist/{dist-UCCG7PHU.mjs → dist-WDU5QOZQ.mjs} +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
# Your Edpire API key
|
|
2
|
-
#
|
|
1
|
+
# Your Edpire API key, from edpire.com -> Integrations -> API keys.
|
|
2
|
+
# Server-side only. Never prefix it with NEXT_PUBLIC_, which would publish it
|
|
3
|
+
# in the browser bundle.
|
|
3
4
|
EDPIRE_API_KEY=edp_live_your_key_here
|
|
4
|
-
|
|
5
|
-
# The assessment UUID to embed (find it in the Edpire dashboard)
|
|
6
|
-
NEXT_PUBLIC_ASSESSMENT_ID=your-assessment-uuid-here
|
|
@@ -1,66 +1,76 @@
|
|
|
1
|
-
# Edpire
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`app/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
|
|
1
|
+
# Edpire in Next.js
|
|
2
|
+
|
|
3
|
+
A working assessment player in a Next.js App Router app. Three files are the
|
|
4
|
+
whole integration.
|
|
5
|
+
|
|
6
|
+
<table>
|
|
7
|
+
<tr>
|
|
8
|
+
<td width="33%"><img src="docs/screenshots/01-pick.jpg" alt="A list of published assessments, each with a title and an exercise count" /></td>
|
|
9
|
+
<td width="33%"><img src="docs/screenshots/02-play.jpg" alt="An assessment open in the Edpire player" /></td>
|
|
10
|
+
<td width="33%"><img src="docs/screenshots/03-graded.jpg" alt="The same assessment after submitting, with per-question marks and the answer key" /></td>
|
|
11
|
+
</tr>
|
|
12
|
+
<tr>
|
|
13
|
+
<td align="center"><sub><b>1.</b> Pick by title, not by ID</sub></td>
|
|
14
|
+
<td align="center"><sub><b>2.</b> The player</sub></td>
|
|
15
|
+
<td align="center"><sub><b>3.</b> Graded in place</sub></td>
|
|
16
|
+
</tr>
|
|
17
|
+
</table>
|
|
18
|
+
|
|
19
|
+
Everything from screen 2 onward is the SDK. You write screen 1 and the two API
|
|
20
|
+
routes.
|
|
21
|
+
|
|
22
|
+
| File | What it does |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `app/api/edpire/assessments/route.ts` | Lists what is published, so the page can show titles |
|
|
25
|
+
| `app/api/edpire/token/route.ts` | Mints a short-lived token for one learner |
|
|
26
|
+
| `app/page.tsx` | Shows the list, then renders the player |
|
|
27
|
+
|
|
28
|
+
## Run it
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cp .env.example .env.local # paste your EDPIRE_API_KEY
|
|
32
|
+
npm install
|
|
33
|
+
npm run dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Open http://localhost:3000. The page lists your published assessments; click one
|
|
37
|
+
to play it.
|
|
38
|
+
|
|
39
|
+
**Add `http://localhost:3000` to your allow-list first**, at edpire.com under
|
|
40
|
+
Integrations, Security, Allowed Embed Origins. Without it the player says
|
|
41
|
+
`Origin not allowed` and names the origin to add, and `onError` fires with
|
|
42
|
+
`ORIGIN_NOT_ALLOWED`. Add your production domain before you deploy.
|
|
43
|
+
|
|
44
|
+
## Why the token endpoint exists
|
|
45
|
+
|
|
46
|
+
Your API key can read and write everything in your organisation, so it cannot go
|
|
47
|
+
in the browser. The endpoint keeps it on the server and hands the browser a
|
|
48
|
+
token instead: single use, one learner, one assessment, expiring in two hours.
|
|
49
|
+
|
|
50
|
+
It also answers *which learner is this?* in a way the browser cannot forge. That
|
|
51
|
+
is `resolveLearner`, and replacing it is the one change you must make:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
resolveLearner: async (req) => {
|
|
55
|
+
const session = await auth.api.getSession({ headers: req.headers })
|
|
56
|
+
return session?.user.id ?? null // null returns 401
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Until you do, every attempt is recorded against `demo-learner-001`.
|
|
61
|
+
|
|
62
|
+
## Two things worth knowing
|
|
63
|
+
|
|
64
|
+
**The player fills its container**, so the container needs a real height. Without
|
|
65
|
+
one it collapses to zero and the player renders invisibly. Hence
|
|
66
|
+
`height: 100vh` in `page.tsx`.
|
|
67
|
+
|
|
68
|
+
**Treat `onComplete` as a UI hint.** It is the fastest way to show a score, and it
|
|
69
|
+
is a client callback. Record the authoritative result server-side from the
|
|
70
|
+
[`submission.graded` webhook](https://docs.edpire.com/developer/webhooks).
|
|
71
|
+
|
|
72
|
+
## Next steps
|
|
73
|
+
|
|
74
|
+
- [Embedded Player](https://docs.edpire.com/developer/sdk/embedded-player), every prop and callback
|
|
75
|
+
- [Webhooks](https://docs.edpire.com/developer/webhooks), for recording results
|
|
76
|
+
- [Troubleshooting](https://docs.edpire.com/developer/sdk/troubleshooting)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assessments this learner can play.
|
|
3
|
+
*
|
|
4
|
+
* Your API key is used here, on the server. The browser receives only titles
|
|
5
|
+
* and IDs, neither of which is secret, and shows them for the learner to pick.
|
|
6
|
+
*
|
|
7
|
+
* `status=published` matters: a draft cannot be played.
|
|
8
|
+
*/
|
|
9
|
+
import { EdpireClient } from "@edpire/sdk/client"
|
|
10
|
+
|
|
11
|
+
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
12
|
+
|
|
13
|
+
export async function GET() {
|
|
14
|
+
try {
|
|
15
|
+
const { items } = await client.getAssessments({ status: "published", limit: 5 })
|
|
16
|
+
|
|
17
|
+
return Response.json({
|
|
18
|
+
assessments: items.map((a) => ({
|
|
19
|
+
id: a.id,
|
|
20
|
+
title: a.title,
|
|
21
|
+
exerciseCount: a.exercise_count ?? null,
|
|
22
|
+
})),
|
|
23
|
+
})
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// "fetch failed" on its own tells the reader nothing, so name the two
|
|
26
|
+
// things it is almost always caused by.
|
|
27
|
+
return Response.json(
|
|
28
|
+
{
|
|
29
|
+
error:
|
|
30
|
+
`Could not list assessments: ${err instanceof Error ? err.message : err}. ` +
|
|
31
|
+
"Check EDPIRE_API_KEY, and that this machine can reach edpire.com.",
|
|
32
|
+
},
|
|
33
|
+
{ status: 502 },
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Mints a short-lived embed token for one learner and one assessment.
|
|
3
3
|
*
|
|
4
|
-
* The browser POSTs { assessmentId }
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Powered by createEdpireTokenHandler() — one call handles auth checking,
|
|
8
|
-
* token minting, and error mapping.
|
|
4
|
+
* The browser POSTs { assessmentId } and receives { token }. Your API key stays
|
|
5
|
+
* here: if it reached the browser it would be readable by anyone.
|
|
9
6
|
*/
|
|
10
7
|
import { createEdpireTokenHandler } from "@edpire/sdk/client"
|
|
11
8
|
|
|
@@ -13,26 +10,22 @@ export const POST = createEdpireTokenHandler({
|
|
|
13
10
|
apiKey: process.env.EDPIRE_API_KEY!,
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* In a real app replace this with your session/auth library:
|
|
13
|
+
* ───────────────────────────────────────────────────────────────────────
|
|
14
|
+
* REPLACE THIS. It decides whose results get recorded.
|
|
15
|
+
* ───────────────────────────────────────────────────────────────────────
|
|
20
16
|
*
|
|
21
|
-
*
|
|
22
|
-
* const session = await getServerSession(req, res, authOptions)
|
|
23
|
-
* return session?.user?.id ?? null
|
|
17
|
+
* Read the learner from your own session:
|
|
24
18
|
*
|
|
25
|
-
* @example Next.js + Better Auth
|
|
26
19
|
* const session = await auth.api.getSession({ headers: req.headers })
|
|
27
|
-
* return session?.user.id ?? null
|
|
20
|
+
* return session?.user.id ?? null // Better Auth
|
|
21
|
+
*
|
|
22
|
+
* const session = await getServerSession(authOptions)
|
|
23
|
+
* return session?.user.id ?? null // NextAuth
|
|
24
|
+
*
|
|
25
|
+
* Returning null rejects the request with 401 and mints nothing.
|
|
28
26
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* Never read the learner from the request body. If the browser can name the
|
|
28
|
+
* learner, any learner can submit results as any other.
|
|
31
29
|
*/
|
|
32
|
-
resolveLearner: (
|
|
33
|
-
// Demo: read userId from a signed cookie. Replace with your auth session.
|
|
34
|
-
const cookie = req.headers.get("cookie") ?? ""
|
|
35
|
-
const match = /(?:^|;\s*)userId=([^;]+)/.exec(cookie)
|
|
36
|
-
return match ? decodeURIComponent(match[1]) : null
|
|
37
|
-
},
|
|
30
|
+
resolveLearner: async () => "demo-learner-001",
|
|
38
31
|
})
|
|
@@ -1,51 +1,130 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* The whole integration, in two steps.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and
|
|
10
|
-
* 3. The player fetches the assessment content from Edpire using the token,
|
|
11
|
-
* renders the full UI, and calls onComplete when the learner submits.
|
|
6
|
+
* 1. Ask your own server what is published, and show the titles.
|
|
7
|
+
* 2. Render <EdpireAssessmentPlayer> for whichever the learner picks. It fetches
|
|
8
|
+
* a token from /api/edpire/token, loads the assessment, grades the
|
|
9
|
+
* submission and shows per-question feedback.
|
|
12
10
|
*
|
|
13
|
-
*
|
|
11
|
+
* Your API key never reaches the browser.
|
|
14
12
|
*/
|
|
13
|
+
import { useEffect, useState } from "react"
|
|
15
14
|
import { EdpireAssessmentPlayer } from "@edpire/sdk/react"
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
type Assessment = { id: string; title: string; exerciseCount: number | null }
|
|
18
17
|
|
|
19
18
|
export default function Page() {
|
|
20
|
-
|
|
19
|
+
const [assessments, setAssessments] = useState<Assessment[] | null>(null)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
21
|
+
const [playing, setPlaying] = useState<string | null>(null)
|
|
22
|
+
|
|
23
|
+
function load() {
|
|
24
|
+
setError(null)
|
|
25
|
+
setAssessments(null)
|
|
26
|
+
fetch("/api/edpire/assessments")
|
|
27
|
+
.then((r) => r.json())
|
|
28
|
+
.then((data) => (data.error ? setError(data.error) : setAssessments(data.assessments)))
|
|
29
|
+
.catch((e) => setError(String(e)))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
useEffect(load, [])
|
|
33
|
+
|
|
34
|
+
if (playing) {
|
|
21
35
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
<EdpireAssessmentPlayer
|
|
37
|
+
tokenEndpoint="/api/edpire/token"
|
|
38
|
+
assessmentId={playing}
|
|
39
|
+
onComplete={(result) => {
|
|
40
|
+
// A UI hint. Record the authoritative result server-side from the
|
|
41
|
+
// submission.graded webhook.
|
|
42
|
+
console.log("complete", result.score, "/", result.max_score)
|
|
43
|
+
}}
|
|
44
|
+
onError={(err) => setError(`${err.code}: ${err.message}`)}
|
|
45
|
+
// The player fills its container, so the container needs a real height.
|
|
46
|
+
// Without one it collapses and the player renders invisibly.
|
|
47
|
+
style={{ width: "100%", height: "100vh" }}
|
|
48
|
+
/>
|
|
26
49
|
)
|
|
27
50
|
}
|
|
28
51
|
|
|
29
52
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
<main style={styles.page}>
|
|
54
|
+
<h1 style={styles.title}>Edpire example</h1>
|
|
55
|
+
|
|
56
|
+
{error ? (
|
|
57
|
+
<>
|
|
58
|
+
<p style={styles.error}>{error}</p>
|
|
59
|
+
<button onClick={load} style={styles.retry}>Try again</button>
|
|
60
|
+
</>
|
|
61
|
+
) : assessments === null ? (
|
|
62
|
+
<p style={styles.muted}>Loading...</p>
|
|
63
|
+
) : assessments.length === 0 ? (
|
|
64
|
+
<Empty />
|
|
65
|
+
) : (
|
|
66
|
+
<>
|
|
67
|
+
<p style={styles.muted}>Published in your organisation. Choose one to play it.</p>
|
|
68
|
+
{assessments.map((a) => (
|
|
69
|
+
<button key={a.id} onClick={() => setPlaying(a.id)} style={styles.card}>
|
|
70
|
+
<span style={styles.cardTitle}>{a.title}</span>
|
|
71
|
+
{a.exerciseCount !== null && (
|
|
72
|
+
<span style={styles.cardMeta}>{a.exerciseCount} exercise(s)</span>
|
|
73
|
+
)}
|
|
74
|
+
</button>
|
|
75
|
+
))}
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
</main>
|
|
50
79
|
)
|
|
51
80
|
}
|
|
81
|
+
|
|
82
|
+
/** A new organisation has nothing published, and the fix is in Edpire. */
|
|
83
|
+
function Empty() {
|
|
84
|
+
return (
|
|
85
|
+
<div style={styles.empty}>
|
|
86
|
+
<p>
|
|
87
|
+
Nothing is published in this organisation yet, so there is nothing to open. Create an
|
|
88
|
+
assessment in Edpire, publish it, then reload. Drafts do not appear here.
|
|
89
|
+
</p>
|
|
90
|
+
<a href="https://docs.edpire.com/quickstart" style={styles.link}>
|
|
91
|
+
docs.edpire.com/quickstart
|
|
92
|
+
</a>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
98
|
+
page: { maxWidth: 640, margin: "0 auto", padding: 32, fontFamily: "system-ui, sans-serif" },
|
|
99
|
+
title: { fontSize: 24, marginBottom: 4 },
|
|
100
|
+
muted: { color: "#565b66", fontSize: 14 },
|
|
101
|
+
error: { color: "#d23b2b", fontSize: 14, whiteSpace: "pre-wrap" },
|
|
102
|
+
card: {
|
|
103
|
+
display: "flex",
|
|
104
|
+
flexDirection: "column",
|
|
105
|
+
alignItems: "flex-start",
|
|
106
|
+
gap: 4,
|
|
107
|
+
width: "100%",
|
|
108
|
+
padding: 16,
|
|
109
|
+
marginTop: 10,
|
|
110
|
+
border: "1px solid #e7e5e0",
|
|
111
|
+
borderRadius: 12,
|
|
112
|
+
background: "#fff",
|
|
113
|
+
cursor: "pointer",
|
|
114
|
+
textAlign: "left",
|
|
115
|
+
font: "inherit",
|
|
116
|
+
},
|
|
117
|
+
cardTitle: { fontSize: 15, fontWeight: 600 },
|
|
118
|
+
cardMeta: { fontSize: 12, color: "#8d93a0" },
|
|
119
|
+
empty: { marginTop: 12, color: "#565b66", fontSize: 14, lineHeight: 1.6 },
|
|
120
|
+
retry: {
|
|
121
|
+
marginTop: 12,
|
|
122
|
+
padding: "8px 18px",
|
|
123
|
+
border: "1px solid #e7e5e0",
|
|
124
|
+
borderRadius: 999,
|
|
125
|
+
background: "#fff",
|
|
126
|
+
cursor: "pointer",
|
|
127
|
+
font: "inherit",
|
|
128
|
+
},
|
|
129
|
+
link: { color: "#d23b2b" },
|
|
130
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { NextConfig } from "next"
|
|
2
2
|
|
|
3
3
|
const nextConfig: NextConfig = {
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Next 16 writes AGENTS.md and CLAUDE.md into the project on first run. This
|
|
5
|
+
// is a template you are meant to copy from, so keep it to your own files.
|
|
6
|
+
agentRules: false,
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export default nextConfig
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
# Your Edpire API key
|
|
2
|
-
#
|
|
1
|
+
# Your Edpire API key, from edpire.com -> Integrations -> API keys.
|
|
2
|
+
# Server-side only. Never prefix it with VITE_, which would publish it in the
|
|
3
|
+
# browser bundle.
|
|
3
4
|
EDPIRE_API_KEY=edp_live_your_key_here
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
# VITE_ prefix is safe here because this is a public ID, not a secret.
|
|
7
|
-
VITE_ASSESSMENT_ID=your-assessment-uuid-here
|
|
8
|
-
|
|
9
|
-
# Port for the Express server
|
|
6
|
+
# Port for the Express server. Vite serves the page on 5173 and proxies here.
|
|
10
7
|
PORT=3001
|
|
@@ -1,69 +1,89 @@
|
|
|
1
|
-
# Edpire
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
# Edpire in Vite and Express
|
|
2
|
+
|
|
3
|
+
A working assessment player with a React frontend and an Express backend. Two
|
|
4
|
+
files are the whole integration.
|
|
5
|
+
|
|
6
|
+
<table>
|
|
7
|
+
<tr>
|
|
8
|
+
<td width="33%"><img src="docs/screenshots/01-pick.jpg" alt="A list of published assessments, each with a title and an exercise count" /></td>
|
|
9
|
+
<td width="33%"><img src="docs/screenshots/02-play.jpg" alt="An assessment open in the Edpire player" /></td>
|
|
10
|
+
<td width="33%"><img src="docs/screenshots/03-graded.jpg" alt="The same assessment after submitting, with per-question marks and the answer key" /></td>
|
|
11
|
+
</tr>
|
|
12
|
+
<tr>
|
|
13
|
+
<td align="center"><sub><b>1.</b> Pick by title, not by ID</sub></td>
|
|
14
|
+
<td align="center"><sub><b>2.</b> The player</sub></td>
|
|
15
|
+
<td align="center"><sub><b>3.</b> Graded in place</sub></td>
|
|
16
|
+
</tr>
|
|
17
|
+
</table>
|
|
18
|
+
|
|
19
|
+
Everything from screen 2 onward is the SDK. You write screen 1 and the server.
|
|
20
|
+
|
|
21
|
+
| File | What it does |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `server.ts` | Lists what is published, and mints tokens |
|
|
24
|
+
| `src/App.tsx` | Shows the list, then renders the player |
|
|
25
|
+
|
|
26
|
+
## Run it
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cp .env.example .env # paste your EDPIRE_API_KEY
|
|
30
|
+
npm install
|
|
31
|
+
npm run dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That starts both: Express on 3001, Vite on 5173. Open http://localhost:5173.
|
|
35
|
+
The page lists your published assessments; click one to play it.
|
|
36
|
+
|
|
37
|
+
**Add `http://localhost:5173` to your allow-list first**, at edpire.com under
|
|
38
|
+
Integrations, Security, Allowed Embed Origins. Without it the player says
|
|
39
|
+
`Origin not allowed` and names the origin to add, and `onError` fires with
|
|
40
|
+
`ORIGIN_NOT_ALLOWED`. Add your production domain before you deploy.
|
|
41
|
+
|
|
42
|
+
Note the port: the browser talks to Vite on **5173**, which proxies `/api` to
|
|
43
|
+
Express. Adding 3001 instead is a common ten-minute detour.
|
|
44
|
+
|
|
45
|
+
## Why the token endpoint exists
|
|
46
|
+
|
|
47
|
+
Your API key can read and write everything in your organisation, so it cannot go
|
|
48
|
+
in the browser. The endpoint keeps it on the server and hands the browser a
|
|
49
|
+
token instead: single use, one learner, one assessment, expiring in two hours.
|
|
50
|
+
|
|
51
|
+
It also answers *which learner is this?* in a way the browser cannot forge. That
|
|
52
|
+
is `resolveLearner`, and replacing it is the one change you must make:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
resolveLearner: async (req) => {
|
|
56
|
+
const sid = parseCookie(req.headers.get("cookie"), "sid")
|
|
57
|
+
return sid ? (await sessionStore.get(sid))?.userId ?? null : null
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Until you do, every attempt is recorded against `demo-learner-001`.
|
|
62
|
+
|
|
63
|
+
## Three things worth knowing
|
|
64
|
+
|
|
65
|
+
**`req` is a Web-standard Request**, not an Express one, so `req.session` and
|
|
66
|
+
`req.cookies` do not exist inside `resolveLearner`. Read the raw cookie header
|
|
67
|
+
and do your own lookup.
|
|
68
|
+
|
|
69
|
+
**Mount the token route before any body parser.** The handler reads the request
|
|
70
|
+
stream itself, and a parser that already drained it makes the request hang.
|
|
71
|
+
|
|
72
|
+
**The player fills its container**, so the container needs a real height. Without
|
|
73
|
+
one it collapses to zero and the player renders invisibly. Hence
|
|
74
|
+
`height: 100vh` in `App.tsx`.
|
|
75
|
+
|
|
76
|
+
## Production
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm run build && npm run preview
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Express then serves the built page and the two API routes from one origin, so
|
|
83
|
+
the Vite proxy is no longer involved.
|
|
84
|
+
|
|
85
|
+
## Next steps
|
|
86
|
+
|
|
87
|
+
- [Embedded Player](https://docs.edpire.com/developer/sdk/embedded-player), every prop and callback
|
|
88
|
+
- [Webhooks](https://docs.edpire.com/developer/webhooks), for recording results
|
|
89
|
+
- [Troubleshooting](https://docs.edpire.com/developer/sdk/troubleshooting)
|
|
Binary file
|
|
Binary file
|