add authors note and links to my email and source code
Nightly build & deploy / build-and-deploy (push) Successful in 9m42s

This commit is contained in:
LockeShor
2026-09-20 00:52:43 -04:00
parent 9ef967574e
commit 1ef9ddab8e
4 changed files with 55 additions and 11 deletions
+8
View File
@@ -1,3 +1,11 @@
## Author's Note
"author" is a bit of a misnomer because yes, this was completely vibecoded - I don't have a lot of experience in that area, and wanted to see what I could do. It's also a small tool I dont care much about.
It was also a very interesting challenge considering the structure of the CMU Userweb system - obviously I can only host static files, so everything had to be in browser. except one thing.
I wanted the link to have a fancy embed from the new discord documentation that was released recently (see my cyoa-discord repository for another exploration of this), which would have to be updated daily.
To achieve this, I have a somewhat complex Gitea action that has to log into the CMU AFS using sftp, build the new version of the site with the updated date, put it in the www directory, then trigger a Userweb update through their website. Interestingly enough - linux.andrew.cmu.edu is only accessible on-premise. So, instead of going for a complicated VPN route, I have an actions runner on my always-on server on campus. This required some additional logistics - but nothing insane (I have yet to hook up ethernet...).
Anyway, all I really care about is if this can be a useful and easy to use tool for people who want to understand the best way to spend their meal dollars. For me, it makes it a lot easier to track throughout the year so I don't end up with extra (or lacking) meal blocks at the end of the semester.
# CMU Meal Estimator # CMU Meal Estimator
A small single-page web app that estimates how many **meal blocks** and **FLEX dollars** a A small single-page web app that estimates how many **meal blocks** and **FLEX dollars** a
+2
View File
@@ -792,6 +792,7 @@
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -1309,6 +1310,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
+35 -5
View File
@@ -110,13 +110,20 @@ function buildLayout(store: Store, config: Config): HTMLElement {
const footer = el("footer", "footer"); const footer = el("footer", "footer");
footer.append( footer.append(
footerLine( footerLine(
"Estimates spread each plan evenly across on-campus days. Informational only, not official balances." "Found a problem? Like the project? Hate it? Email me at ",
createLink("mailto:jslivka@lockeshor.com", "jslivka@lockeshor.com"),
". Also, check out the source code on ",
createLink(
"https://gitea.lockeshor.com/lockeshor/cmu-meal-estimator",
"My Gitea"
),
"."
), ),
footerLine( footerLine(
"Not affiliated with, endorsed by, or officially connected to Carnegie Mellon University or CMU Dining. An independent student tool.", "Not affiliated with, endorsed by, or officially connected to Carnegie Mellon University or CMU Dining. An independent student tool.",
"footer-disclaimer" "footer-disclaimer"
) )
); );
wrap.append(header, grid, footer); wrap.append(header, grid, footer);
return wrap; return wrap;
@@ -528,9 +535,32 @@ function field(label: string): HTMLElement {
return f; return f;
} }
function footerLine(text: string, extraClass = ""): HTMLElement { function createLink(href: string, text: string): HTMLAnchorElement {
const p = el("p", `footer-line${extraClass ? ` ${extraClass}` : ""}`); const a = document.createElement("a");
p.textContent = text; a.className = "tartan";
a.href = href;
a.textContent = text;
return a;
}
// Accepts text strings or DOM Nodes, with optional class name logic
function footerLine(...args: (Node | string)[]): HTMLParagraphElement {
let className = "";
const children: (Node | string)[] = [...args];
// If the last argument is a class identifier (e.g., "footer-disclaimer")
const lastArg = children[children.length - 1];
if (
children.length > 1 &&
typeof lastArg === "string" &&
lastArg.startsWith("footer-")
) {
className = children.pop() as string;
}
const p = document.createElement("p");
if (className) p.className = className;
p.append(...children);
return p; return p;
} }
+5 -1
View File
@@ -64,10 +64,14 @@ body {
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
.title .tartan { .tartan {
color: var(--tartan); color: var(--tartan);
} }
.tartan:hover {
color: var(--tartan-soft);
}
.subtitle { .subtitle {
margin: 0; margin: 0;
color: var(--text-dim); color: var(--text-dim);