Readme

Email-based (spaced repetition) course creation tool

πŸ—οΈ Work-in-progress! πŸ—οΈ

The idea is to create a reusable course generator Val designed with effective, research-backed learning techniques in mind. That includes techniques like spaced repetition (via an interval between email lessons), retrieval practice (quizzing and fill-in-the-blank), elaboration and reflection (writing exercises). The Val(s) will include:

  • Email signup and verification
  • Template to fill in with course content
  • SQLite tables to store students and track progress
  • Cron job to send lessons to students

As the first use case for this generalizable course creator, I plan to make a course about Make It Stick, which is a practical book about learning research that gave me the idea.

I'm writing more about the implementation on my digital garden, which is also where you'll find the signup form.

1
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { fetchConfirmationHtml } from "https://esm.town/v/petermillspaugh/fetchConfirmationHtml";
import { fetchSignupHtml } from "https://esm.town/v/petermillspaugh/fetchSignupHtml";
import { markLessonComplete } from "https://esm.town/v/petermillspaugh/markLessonComplete";
import { refreshToken } from "https://esm.town/v/petermillspaugh/refreshToken";
import { sendLesson } from "https://esm.town/v/petermillspaugh/sendLesson";
import { sendLessonResponses } from "https://esm.town/v/petermillspaugh/sendLessonResponses";
import { sendVerification } from "https://esm.town/v/petermillspaugh/sendVerification";
import { upsertStudent } from "https://esm.town/v/petermillspaugh/upsertStudent";
import { email as sendEmail } from "https://esm.town/v/std/email?v=11";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { Hono } from "npm:hono";
import { JSX } from "npm:react";
export async function emailSubscription(req: Request) {
const app = new Hono();
app.get("/", async (c) => {
return c.html(fetchSignupHtml());
});
app.post("/send-verification", async c => {
const formData = await c.req.formData();
const name = formData.get("name");
const email = formData.get("email");
if (typeof name !== "string" || typeof email !== "string") {
return Response.json({ message: "Unexpected missing value for name or email." });
}
const token = crypto.randomUUID();
await upsertStudent({ name, email, token });
// Lack of await is intentional: send optimistic success response, then send email and notify myself async
sendVerification({ emailAddress: email, html: fetchConfirmationHtml({ email, token }) });
return Response.json({ success: true, message: "Sent verification email." });
});
app.put("/confirm-verification", async c => {
const email = c.req.query("email");
const token = c.req.query("token");
const { newToken, didRefresh } = await refreshToken({ email, token });
if (didRefresh) {
// Lack of await is intentional: send optimistic success response, then send email and notify myself async
sendVerification({
emailAddress: email,
html: fetchConfirmationHtml({ email, token: newToken, reVerifying: true }),
});
return Response.json({ message: "Resent confirmation email." });
}
await sqlite.execute({
sql: `UPDATE students SET verified = 1 WHERE email = ?;`,
args: [email],
});
// Send the first lesson
await sendLesson({ emailAddress: email, lesson: 0 });
// No need to await: just emailing myself a notification
sendEmail({
subject: `${email} verified for subscription to: Make It Stick (in 10 days, via email)`,
text: `Verification complete for ${email}'s subscription to: Make It Stick (in 10 days, via email)`,
});
return Response.json({ confirmed: true, message: "Verified email address." });
});
/*
* TODO: POST requests are blocked for security reasons on mobile email clients.
* I could use a GET request with a req body, but that's a hack/anti-pattern.
* I don't want to move the form to a webpage because it should be in the lesson flow.
* TBD how to handle this. Perhaps an email handler Val?
*/
app.post("/complete-lesson", async c => {
const email = c.req.query("email");
if (typeof email !== "string") {
// Notify myself of the unexpected case
sendEmail({
subject: "Unexpected missing email query param on /complete-lesson",
text: "Email query param should be populated: check lesson form submission.",
});
return;
}
await markLessonComplete(email);
const lesson = c.req.query("lesson");
const formData = await c.req.formData();
await sendLessonResponses({ email, lesson, formData });
const responseHtml = `
<main>
<p>Thanks for completing the lesson! Your responses should be in your inbox any second, and the next lesson will go out tomorrow.</p>
<p>In the meantime, feel free to send any feedback to <a href="mailto:pete@petemillspaugh.com">pete@petemillspaugh.com</a>, or by replying directly to any lesson email.</p>
</main>
`;
πŸ‘† This is a val. Vals are TypeScript snippets of code, written in the browser and run on our servers. Create scheduled functions, email yourself, and persist small pieces of data β€” all from the browser.