Avatar

@tal

1 like16 public vals
Joined May 22, 2023

Creates and iCal feed that calendars can subscribe to for all of the various school lunches.

Readme
Fork
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
import { buildDoeMenuCalendar } from "https://esm.town/v/tal/buildDoeMenuCalendar";
import { doeMenuResponseStore } from "https://esm.town/v/tal/doeMenuResponseStore";
import { ensureDataForKey } from "https://esm.town/v/tal/ensureDataForKey";
import { dateCalendarKey } from "https://esm.town/v/tal/dateCalendarKey";
import { currentDateValues } from "https://esm.town/v/tal/currentDateValues";
export async function doeMenuCalendar(req: Request) {
const url = new URL(req.url);
const menuType = url.pathname.substring(1);
const today = currentDateValues();
const key = dateCalendarKey({
...today,
menuType,
});
await ensureDataForKey({
...today,
menuType,
});
const storeValue = doeMenuResponseStore[key];
if (!storeValue) {
return new Response(`not found for key ${key}`);
}
const context = {
menuType,
};
const calendar = await buildDoeMenuCalendar(
context as any,
storeValue,
);
const body = calendar.toString();
return new Response(body, {
headers: {
"Content-Type": "text/calendar",
},
});
}
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
import { formatMenuLines } from "https://esm.town/v/tal/formatMenuLines";
import { DOEMenuTypes } from "https://esm.town/v/tal/DOEMenuTypes";
type Line = {
dateStr: string;
menuStr: string;
date: {
type: "header" | "Daily Offerings" | "date";
date: Date;
prefix: string;
suffix: string;
};
menu: {
items: string[];
};
};
type Context = {
menuType: keyof typeof DOEMenuTypes;
dailyOfferings?: Line;
};
export async function calendarItemForLine(context: Context, line: Line) {
const menuType = DOEMenuTypes[context.menuType];
if (line.date.type == "date") {
const fullMenu = formatMenuLines(line.menu.items);
let descriptionLines: string[] = [
`*${line.date.suffix} Menu*`,
`${menuType.title} for ${line.date.prefix}`,
"\n",
...fullMenu,
];
if (context.dailyOfferings) {
const dailyOfferingItems = formatMenuLines(
context.dailyOfferings.menu.items,
);
if (dailyOfferingItems.length > 0) {
descriptionLines = [
...descriptionLines,
"\n*Daily Offerings*\n",
...dailyOfferingItems,
];
}
}
const description = descriptionLines.join("\n");
const start = new Date(line.date.date);
return ({
start,
allDay: true,
summary: `DoE Lunch`,
description,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { calendarItemForLine } from "https://esm.town/v/tal/calendarItemForLine";
type Line = Parameters<typeof calendarItemForLine>[1];
type Context = Parameters<typeof calendarItemForLine>[0];
export async function buildDoeMenuCalendar(context: Context, storeValue) {
const ical = await import("npm:ical-generator");
let dailyOfferings: Line | undefined;
let events = [];
const calendar = ical.default({
name: `NYC DoE Lunch Menu ${context.menuType}`,
});
for (let line of storeValue.lines) {
if (line.date.type === "Daily Offerings") {
context.dailyOfferings = line;
}
let eventData = await calendarItemForLine(context, line);
if (eventData) {
events.push(eventData);
calendar.createEvent(eventData);
}
}
return calendar;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const icsTestServe = async () => {
const ical = await import("npm:ical-generator");
const calendar = ical.default({ name: "Tal's testing calendar" });
calendar.method(ical.ICalCalendarMethod.PUBLISH);
const startTime = new Date();
calendar.createEvent({
start: startTime,
allDay: true,
summary: "Example Event",
description: "It works ;)",
location: "my room",
url: "https://val.town/",
});
return new Response(calendar.toString(), {
headers: {
"Content-Type": "text/calendar",
},
});
};
1
2
3
4
5
6
7
8
9
10
import { fetchAndStore } from "https://esm.town/v/tal/fetchAndStore";
import { currentDateValues } from "https://esm.town/v/tal/currentDateValues";
export let storeTodaysDate = (async () => {
const today = currentDateValues();
await fetchAndStore({
...today,
menuType: "pre-k---8-lunch-menu",
});
})();
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
import { set } from "https://esm.town/v/std/set?v=11";
import { doeMenuResponseStore } from "https://esm.town/v/tal/doeMenuResponseStore";
import { dateCalendarKey } from "https://esm.town/v/tal/dateCalendarKey";
import { parseMenuLine } from "https://esm.town/v/tal/parseMenuLine";
import { fetchDOEMenu } from "https://esm.town/v/tal/fetchDOEMenu";
export async function fetchAndStore(opts: {
schoolYear: `${number}-${number}`;
month: string;
menuType: string;
}) {
const menuText = await fetchDOEMenu(opts);
const lines: ReturnType<typeof parseMenuLine>[] = await Promise
.all(menuText.split("\r\n").map(parseMenuLine));
const key = dateCalendarKey(opts);
doeMenuResponseStore[key] = {
lastFetchedAt: new Date(),
lines,
};
await set(
"doeMenuResponseStore",
doeMenuResponseStore,
);
return {
lines,
};
}
1
2
3
4
5
6
7
8
export async function parseMenuOffering(text: string) {
/** get rid of the wrapping {} */
const stripped = text.slice(1, -1);
const items = stripped.split("|");
return {
items,
};
}
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
type LineType = "header" | "Daily Offerings" | "date";
export async function parseMenuDate(text: string) {
const dateFns = await import("npm:date-fns");
let type: LineType;
let firstBreakAt = text.indexOf("?");
if (firstBreakAt == -1) {
if (text === "Daily Offerings") {
type = "Daily Offerings";
}
else {
type = "header";
}
return { type };
}
let prefix = text.substring(0, firstBreakAt);
let suffix = text.substring(firstBreakAt + 1);
const date = dateFns.parse(prefix, "MMMM d; yyyy", new Date());
type = "date";
return {
type,
date,
prefix,
suffix,
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { parseMenuOffering } from "https://esm.town/v/tal/parseMenuOffering";
import { parseMenuDate } from "https://esm.town/v/tal/parseMenuDate";
export async function parseMenuLine(line: string) {
let firstBreakAt = line.indexOf(",");
const dateStr = line.substring(0, firstBreakAt);
const menuStr = line.substring(firstBreakAt + 1);
return {
dateStr,
date: await parseMenuDate(dateStr),
menuStr,
menu: await parseMenuOffering(menuStr),
};
}
1
2
3
4
5
6
7
8
9
10
import { DOEMenuTypes } from "https://esm.town/v/tal/DOEMenuTypes";
type MenuType = (typeof DOEMenuTypes)[number];
export function dateCalendarKey(opts: {
schoolYear: `${number}-${number}`;
month: string;
menuType: MenuType;
}) {
return [opts.schoolYear, opts.month, opts.menuType].join("|");
}