🍬 Treat Yourself · App Map
A take-home guide to what was built, where things live, and one practice worth reusing.
What this app is
Treat Yourself is a single web page that helps someone notice their added sugar without guilt. You answer two questions and get a personal daily limit with its source. Then you tap treats to log them, a sugar scale fills toward your limit, and Honey the bear reacts kindly. Every treat has an ⓘ button showing where its number came from. Your profile, goal, and today's log stay in your browser and the log starts fresh each day.
It is plain HTML, CSS, and JavaScript modules, with no framework, no server, and no dependencies. Seven files do the work:
index.htmlholds both screens (Profile and Tracker), the Daily goal panel, the source popup, and the footer.styles.cssis the pastel candy-shop look, the layout, and the reduced-motion fallback.js/app.jswires the page together: it loads saved state, renders, handles taps, and checks for a new day.js/logic.jsis the rules as pure functions:recommendedLimit,scaleState,isValidGoal,todayKey.js/storage.jsreads and writes localStorage safely, so blocked or corrupt data never crashes the app.js/treats.jsis the treat list: added sugar, serving, basis, and a source link for each.js/honey.jsdraws Honey as SVG in four moods.
One action, end to end: tapping a treat
Pink boxes are what you see. White boxes are code. The green box is the browser's storage, which is the only place anything is saved.
Reference route: follow a treat tap through the code
This is a reference route to read at your own pace, not a record of a guided tour.
js/app.js, search for function logTreat
A click on the treat grid is caught by one listener that finds the card's data-treat id, looks the treat up in TREATS, and calls logTreat. That function pushes a copy of the treat (name, emoji, grams, time) onto state.log, calls persistLog() to save it, then re-renders the log and the scale. The ⓘ button is handled first in the same listener and returns early, which is why it never logs anything.
js/logic.js, search for export function scaleState
Given today's total and the daily limit, it returns everything the screen needs: the percent, grams left or over, the fill height, the color step (mint, butter, peach, rose at 40, 70, and 100 percent), and Honey's mood. It has no idea a web page exists, which is why tests/logic.test.js can check it with plain numbers.
js/app.js, search for function renderScale
It calls scaleState, then writes the results into the tube, the readout, and Honey's speech bubble, swapping Honey's drawing with honeySVG(mood) only when the mood changes, and adding the is-waving class so the CSS wave animation plays.
Where it is saved and loaded
js/storage.js wraps every localStorage call in try/catch and checks the shape of what it reads. On start, init() in js/app.js loads the profile, goal, and log; if the saved log's day is not today's todayKey(), the log starts empty. checkDay() repeats that check every 60 seconds and whenever the tab becomes visible again.
To change this, start here
| Want to… | Start at |
|---|---|
| Add or fix a treat | js/treats.js, the TREATS array. Copy an existing record and keep its source link honest. npm test will fail if a record is missing its serving, basis, or https source. |
| Change a daily limit or its source | js/logic.js, recommendedLimit and SOURCES. Then update the limit table in tests/logic.test.js and the README. |
| Change when the scale turns a color | js/logic.js, the color thresholds inside scaleState, and the matching test "color steps at 40, 70, and 100 percent". |
| Change what Honey says | js/app.js, honeyMessage. |
| Change the look | styles.css, the color tokens at the top of the file under :root. |
| Change the goal range (1 to 300) | js/logic.js, GOAL_MIN and GOAL_MAX, plus the min/max on the input in index.html and the error copy. |
A practice to reuse: write the failure down, then test it with garbage
What happened here. The spec listed, under Important Failure Modes, that saved data might be "corrupt or an old shape" and that the app should "ignore it and start fresh at the Profile screen instead of crashing." That one sentence became a test before the feature was finished: tests/storage.test.js feeds ty.profile the text garbage{{{ and a profile with an impossible value, and expects loadProfile() to return null. The build checklist then verified it in the browser by writing garbage into storage and reloading, and the Profile screen appeared with no error.
Why it helps. Naming the failure in the plan made the "done" condition observable: not "storage is robust," but "this exact bad input produces this exact safe result." That is cheap to test and hard to argue with.
On another project. When a spec says data comes from somewhere you don't control (a file, an API, a form, a saved state), write one line about what happens when it is wrong, then write the test that hands the code that wrong thing. Do it before the happy-path UI is polished, while the data shape is still fresh.
Run it again
From the project folder:
python3 -m http.server 8000
Then open http://localhost:8000. Run the tests with:
npm test
This map is a snapshot of the code at commit 169fb39. If the code changes later, the function names and paths above may drift. Reopen this file any time from devpost/app-map.html.