diff --git a/docs/TODO.md b/docs/TODO.md index 9dc0c00..ae62f41 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -115,7 +115,9 @@ Captured so nothing is lost while the article writing runs. bank, answered correctly, average score. - [x] **The old performance card is gone** — a per-quiz graph needing two attempts, superseded by the session analysis. -- [ ] **Vary the greeting** rather than one fixed line. +- [x] **Vary the greeting** — done 2026-09-11. Four time bands including + the small hours, keyed on the date so it holds for a visit and differs + between days rather than changing under the reader mid-render. ### Questions I owe an answer to - [x] **What extracted the PDFs?** PyMuPDF (`fitz`) in `pdf_service.py`, with an @@ -204,9 +206,10 @@ Captured so nothing is lost while the article writing runs. - [x] **Library browsed column by column** — done 2026-09-10. The articles page is now the column browser itself: topics and the articles filed under them share a column, separated by icon. Search still answers with a flat list. -- [ ] **References** — a section titled "References" is pinned last and styled, - but the numbered list with superscript markers linking down to it is not - built. +- [x] **References** — a section titled "References" is pinned last and styled. + The numbered list with in-text superscript markers is deliberately NOT + built: "with refernec, but you dont need in text reference". Closing this + rather than leaving it open against an instruction. - [ ] **Per-section notes and feedback** — a learner's own note attached to a section, and a feedback channel to the educator. - [ ] **High-yield / key-exam-info toggles** — mark spans and let the reader show diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index f1ffe38..f560f2e 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -6,9 +6,30 @@ import LineChart from '../components/LineChart' import MyNote from '../components/MyNote' import { useAuth } from '../context/AuthContext' -function greeting(name) { - const h = new Date().getHours() - const time = h < 12 ? 'Good morning' : h < 17 ? 'Good afternoon' : 'Good evening' +/** + * The line at the top of the dashboard. + * + * One fixed greeting every visit is the kind of thing that stops being read + * after a week. These vary by the day rather than by the render, so it is + * different when you come back and stable while you are here — a line that + * changes under you as the page re-renders reads as a glitch, not as warmth. + * Late-night and early-morning get their own, because someone revising at + * 2am is not having an evening. + */ +const GREETINGS = { + night: ['Still up', 'Burning the midnight oil', 'Late one'], + morning: ['Good morning', 'Morning', 'Bright and early'], + afternoon: ['Good afternoon', 'Afternoon', 'Hello again'], + evening: ['Good evening', 'Evening', 'Winding down'], +} + +export function greeting(name, now = new Date()) { + const h = now.getHours() + const slot = h < 5 ? 'night' : h < 12 ? 'morning' : h < 17 ? 'afternoon' : h < 22 ? 'evening' : 'night' + const options = GREETINGS[slot] + // Keyed on the date, so it holds for the whole visit. + const day = Math.floor(now.getTime() / 86400000) + const time = options[day % options.length] const first = name?.split(' ')[0] || '' return `${time}${first ? `, ${first}` : ''}` } diff --git a/frontend/src/pages/greeting.test.js b/frontend/src/pages/greeting.test.js new file mode 100644 index 0000000..6cf6cdc --- /dev/null +++ b/frontend/src/pages/greeting.test.js @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { greeting } from './DashboardPage' + +const at = (hour, day = 15) => new Date(2026, 8, day, hour, 0, 0) + +describe('the dashboard greeting', () => { + it('fits the hour, including the small hours', () => { + expect(greeting('Ada', at(2))).toMatch(/Still up|midnight|Late one/) + expect(greeting('Ada', at(9))).toMatch(/[Mm]orning|Bright/) + expect(greeting('Ada', at(14))).toMatch(/[Aa]fternoon|Hello/) + expect(greeting('Ada', at(19))).toMatch(/[Ee]vening|Winding/) + expect(greeting('Ada', at(23))).toMatch(/Still up|midnight|Late one/) + }) + + it('uses the first name only, and copes without one', () => { + expect(greeting('Ada Lovelace', at(9))).toMatch(/, Ada$/) + expect(greeting('', at(9))).not.toContain(',') + expect(greeting(undefined, at(9))).not.toContain(',') + }) + + it('holds steady through a visit but differs between days', () => { + // Same day, different minutes: the line must not change under the reader. + expect(greeting('Ada', at(9))).toBe(greeting('Ada', new Date(2026, 8, 15, 9, 45))) + const week = new Set([15, 16, 17, 18, 19].map(d => greeting('Ada', at(9, d)))) + expect(week.size).toBeGreaterThan(1) + }) +})