Skip to main content

max / makenotwork

6.3 KB · 159 lines History Blame Raw
1 /* What's New modal + "New" badge mechanism.
2 *
3 * Two related UI surfaces that share localStorage as their source of truth:
4 *
5 * 1. Auto-show modal on a "feature version" bump. FEATURE_VERSION is an
6 * opaque string controlled here (not CARGO_PKG_VERSION). Edit it when
7 * you want to fire the modal — typically at the end of a sprint when
8 * the changelog has something worth surfacing. Skipping a bump is
9 * fine: nothing happens if FEATURE_VERSION matches the last-seen
10 * version in localStorage.
11 *
12 * 2. "New" badge on individual links/buttons. Mark any element with
13 * data-new-until="YYYY-MM-DD" — JS adds a `is-new` class while today
14 * is before the date. CSS renders the dot. Once the date passes the
15 * class is removed at page load and the dot disappears.
16 *
17 * Both deliberately avoid server cooperation: no API to call, no template
18 * plumbing, no version-detection brittleness. The whole feature lives in
19 * this file plus the CSS for `.is-new`.
20 */
21
22 (function () {
23 'use strict';
24
25 // ── What's New modal ──────────────────────────────────────────────
26
27 /// Bump this when /changelog has shipped something users should see.
28 /// Setting it to a NEW value triggers the modal once per user.
29 var FEATURE_VERSION = 'v0.8-synckit-per-key';
30
31 /// Summary shown in the modal body. Keep to 1–3 sentences; the link
32 /// to /changelog is the canonical full list.
33 var FEATURE_HEADLINE = 'SyncKit per-key storage';
34 var FEATURE_BODY =
35 "SyncKit apps now bill per developer-defined key rather than per app, " +
36 "with mini-gauges in the dashboard and per-key warning emails. " +
37 "Existing SyncKit clients keep working — the SDK signature is updated " +
38 "for new integrations.";
39
40 var STORAGE_KEY = 'mnw_seen_feature_version';
41
42 function safeGet(key) {
43 try { return localStorage.getItem(key); } catch (e) { return null; }
44 }
45 function safeSet(key, value) {
46 try { localStorage.setItem(key, value); } catch (e) { /* ignore */ }
47 }
48
49 /// Show the modal regardless of seen state. Used by the "What's new"
50 /// link in the footer.
51 function showWhatsNewModal() {
52 var existing = document.getElementById('whats-new-modal');
53 if (existing) { existing.remove(); return; }
54
55 var overlay = document.createElement('div');
56 overlay.id = 'whats-new-modal';
57 overlay.className = 'modal-overlay';
58 overlay.style.display = 'flex';
59 overlay.onclick = function (e) {
60 if (e.target === overlay) {
61 overlay.remove();
62 safeSet(STORAGE_KEY, FEATURE_VERSION);
63 }
64 };
65
66 var content = document.createElement('div');
67 content.className = 'modal-content';
68 content.style.maxWidth = '480px';
69 content.style.padding = '2rem';
70
71 var header = document.createElement('div');
72 header.className = 'modal-header';
73 header.style.marginBottom = '1rem';
74 var h2 = document.createElement('h2');
75 h2.textContent = "What's new: " + FEATURE_HEADLINE;
76 header.appendChild(h2);
77 var closeBtn = document.createElement('button');
78 closeBtn.type = 'button';
79 closeBtn.className = 'modal-close';
80 closeBtn.setAttribute('aria-label', 'Dismiss');
81 closeBtn.innerHTML = '×';
82 closeBtn.onclick = function () {
83 overlay.remove();
84 safeSet(STORAGE_KEY, FEATURE_VERSION);
85 };
86 header.appendChild(closeBtn);
87
88 var body = document.createElement('p');
89 body.textContent = FEATURE_BODY;
90
91 var link = document.createElement('a');
92 link.href = '/changelog';
93 link.textContent = 'Full changelog →';
94 link.className = 'section-link';
95 link.style.display = 'inline-block';
96 link.style.marginTop = '0.75rem';
97
98 content.appendChild(header);
99 content.appendChild(body);
100 content.appendChild(link);
101 overlay.appendChild(content);
102 document.body.appendChild(overlay);
103 }
104
105 /// Auto-show on first visit after a feature-version bump. Records the
106 /// seen version in localStorage so dismissal sticks across reloads.
107 function maybeAutoShowWhatsNew() {
108 var seen = safeGet(STORAGE_KEY);
109 if (seen === FEATURE_VERSION) return;
110 // First-ever visit also seeds the storage — no modal flash for
111 // genuinely new users (they're already getting onboarded).
112 if (seen === null) {
113 safeSet(STORAGE_KEY, FEATURE_VERSION);
114 return;
115 }
116 showWhatsNewModal();
117 }
118
119 // Expose for footer link click + onboarding flows.
120 window.showWhatsNewModal = showWhatsNewModal;
121
122 // ── "New" badge ────────────────────────────────────────────────────
123
124 /// Walk every `[data-new-until]` element. If today < that date, mark
125 /// it `.is-new` so the CSS dot renders. Otherwise strip the attribute
126 /// so it doesn't get re-evaluated on later page loads.
127 function applyNewBadges() {
128 var today = new Date();
129 today.setHours(0, 0, 0, 0);
130 var nodes = document.querySelectorAll('[data-new-until]');
131 for (var i = 0; i < nodes.length; i++) {
132 var el = nodes[i];
133 var until = new Date(el.getAttribute('data-new-until'));
134 if (isNaN(until.getTime())) {
135 el.removeAttribute('data-new-until');
136 continue;
137 }
138 if (today <= until) {
139 el.classList.add('is-new');
140 } else {
141 el.removeAttribute('data-new-until');
142 el.classList.remove('is-new');
143 }
144 }
145 }
146
147 // ── Init ──────────────────────────────────────────────────────────
148
149 if (document.readyState === 'loading') {
150 document.addEventListener('DOMContentLoaded', function () {
151 applyNewBadges();
152 maybeAutoShowWhatsNew();
153 });
154 } else {
155 applyNewBadges();
156 maybeAutoShowWhatsNew();
157 }
158 })();
159