Skip to main content

max / goingson

5.3 KB · 162 lines History Blame Raw
1 /**
2 * GoingsOn - Finish & Review Module
3 * Modal-based end-of-period reflection ritual for Day, Week, and Month views.
4 * Also owns nudge-dot logic on the Finish & Review button.
5 */
6
7 (function() {
8 'use strict';
9
10 // ============ Settings ============
11
12 function getSettings() {
13 return {
14 workStartHour: parseInt(localStorage.getItem('goingson-work-start-hour') || '9', 10),
15 workEndHour: parseInt(localStorage.getItem('goingson-work-end-hour') || '17', 10),
16 planNudges: localStorage.getItem('goingson-plan-nudges') !== 'disabled',
17 reviewNudges: localStorage.getItem('goingson-review-nudges') !== 'disabled',
18 };
19 }
20
21 // ============ Nudge Dot ============
22
23 /**
24 * Show or hide a nudge dot on the Finish & Review button for a view.
25 * @param {string} viewName - 'day', 'week', or 'month'
26 * @param {boolean} show - Whether to show the dot
27 */
28 function updateDot(viewName, show) {
29 const btn = document.getElementById(`${viewName}-finish-review-btn`);
30 if (!btn) return;
31
32 let dot = btn.querySelector('.toggle-nudge-dot');
33 if (show && !dot) {
34 dot = document.createElement('span');
35 dot.className = 'toggle-nudge-dot';
36 btn.appendChild(dot);
37 } else if (!show && dot) {
38 dot.remove();
39 }
40 }
41
42 // ============ Work Hours ============
43
44 function isDuringWorkHours() {
45 const s = getSettings();
46 const hour = new Date().getHours();
47 return hour >= s.workStartHour && hour < s.workEndHour;
48 }
49
50 function isAfterWorkHours() {
51 const s = getSettings();
52 return new Date().getHours() >= s.workEndHour;
53 }
54
55 // ============ Reflection ============
56
57 /**
58 * Render the reflection prompts card used by Day, Week, and Month modals.
59 * @param {Object} opts
60 * @param {string} opts.idPrefix - Prefix for textarea ids (e.g. "daily")
61 * @param {Array<{key, label, placeholder, value}>} opts.prompts
62 * @returns {string} HTML string
63 */
64 function renderReflection({ idPrefix, prompts }) {
65 const esc = GoingsOn.utils.escapeHtml;
66 const escAttr = GoingsOn.utils.escapeAttr;
67
68 const fields = prompts.map(p => `
69 <div class="reflection-prompt">
70 <label class="form-label" for="${idPrefix}-${p.key}">${esc(p.label)}</label>
71 <textarea id="${idPrefix}-${p.key}" class="reflection-textarea form-textarea" rows="3"
72 placeholder="${escAttr(p.placeholder || '')}">${esc(p.value || '')}</textarea>
73 </div>
74 `).join('');
75
76 return `
77 <div class="card card--static review-card reflection-card">
78 <h3 class="review-card-title">Reflection</h3>
79 <div class="reflection-fields">${fields}</div>
80 </div>
81 `;
82 }
83
84 /**
85 * Wire input listeners to the reflection textareas. Calls `onChange` with a
86 * map of {key: value} whenever the user types (debounced).
87 */
88 function wireReflectionAutosave({ idPrefix, prompts, onChange, debounceMs = 500 }) {
89 const fire = GoingsOn.utils.debounce(() => {
90 const values = {};
91 for (const p of prompts) {
92 const el = document.getElementById(`${idPrefix}-${p.key}`);
93 values[p.key] = el ? el.value : '';
94 }
95 onChange(values);
96 }, debounceMs);
97
98 for (const p of prompts) {
99 const el = document.getElementById(`${idPrefix}-${p.key}`);
100 if (el) el.addEventListener('input', fire);
101 }
102 }
103
104 /**
105 * Auto-grow reflection textareas on touch devices.
106 */
107 function autoGrowReflection({ idPrefix, prompts }) {
108 if (!GoingsOn.touch?.isTouchDevice) return;
109 for (const p of prompts) {
110 const el = document.getElementById(`${idPrefix}-${p.key}`);
111 if (el) GoingsOn.utils.autoGrow(el);
112 }
113 }
114
115 // ============ Status Badge ============
116
117 /**
118 * Set the review-status badge for a view's header.
119 * @param {string} elementId - id of the <span class="review-status"> element
120 * @param {string} periodState - 'current' | 'past' | 'future'
121 * @param {boolean} isReviewed - whether the period has been reviewed
122 */
123 function setStatusBadge(elementId, periodState, isReviewed) {
124 const el = document.getElementById(elementId);
125 if (!el) return;
126
127 el.classList.remove('completed', 'pending', 'unreviewed');
128
129 if (periodState === 'future') {
130 el.classList.add('hidden');
131 el.textContent = '';
132 return;
133 }
134
135 el.classList.remove('hidden');
136 if (isReviewed) {
137 el.classList.add('completed');
138 el.textContent = 'Review Complete';
139 } else if (periodState === 'current') {
140 el.classList.add('pending');
141 el.textContent = 'Review Pending';
142 } else {
143 el.classList.add('unreviewed');
144 el.textContent = 'Not Reviewed';
145 }
146 }
147
148 // ============ Namespace ============
149
150 GoingsOn.planReviewToggle = {
151 updateDot,
152 getSettings,
153 isDuringWorkHours,
154 isAfterWorkHours,
155 renderReflection,
156 wireReflectionAutosave,
157 autoGrowReflection,
158 setStatusBadge,
159 };
160
161 })();
162