-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
239 lines (209 loc) · 8.88 KB
/
Copy pathscript.js
File metadata and controls
239 lines (209 loc) · 8.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
ThinTrack - single-file modular logic (vanilla JS)
Fix: ensure hardware control buttons work and integrate with in-phone notifications
- Binds to #btn-near / #btn-far / #btn-out
- Updates model, UI (sticker position, proximity badge, phone UI), and phone-scoped notifications
- Lightweight, CS50-friendly, commented
*/
(() => {
// --- CONFIG ---
const CONFIG = {
RSSI: { NEAR: -52, FAR: -74, OUT: -90, LEFT_BEHIND: -85, HYSTERESIS: 3 },
TIMERS: { ALERT_DELAY_MS: 700, RECOVER_CONFIRM_MS: 1000, UI_ANIM_MS: 300, MOVE_MS: 400 },
UX: { toastMs: 2200 }
};
// --- DOM CACHE ---
const btnNear = document.getElementById('btn-near');
const btnFar = document.getElementById('btn-far');
const btnOut = document.getElementById('btn-out');
const rssiValue = document.getElementById('rssi-value');
const distanceLabel = document.getElementById('distance-label');
const sticker = document.getElementById('sticker');
const proximityBadge = document.getElementById('proximity-badge');
const signalBar = document.getElementById('signal-bar');
const signalDesc = document.getElementById('signal-desc');
const metaRssi = document.getElementById('meta-rssi');
const metaDistance = document.getElementById('meta-distance');
const alertBanner = document.getElementById('alert-banner');
const dismissBtn = document.getElementById('dismiss');
const openMapBtn = document.getElementById('open-map');
// phone-scoped notifications
const phoneNotifBell = document.getElementById('phone-notif-bell');
const phoneNotifCount = document.getElementById('phone-notif-count');
const phoneNotifList = document.getElementById('phone-notif-list');
const phoneScreenInner = document.querySelector('.phone-screen-inner');
// guard for required elements
if (!btnNear || !btnFar || !btnOut) {
console.warn('ThinTrack: simulation buttons missing');
}
// --- MODEL & STATES ---
const STATES = {
NEAR: { id: 'near', rssi: CONFIG.RSSI.NEAR, dist: '~1–2 m', bar: '88%', color: 'linear-gradient(90deg,#34d399,#059669)', desc: 'Strong signal — item nearby', badge: 'Connected' },
FAR: { id: 'far', rssi: CONFIG.RSSI.FAR, dist: '~6–8 m', bar: '56%', color: 'linear-gradient(90deg,#fbbf24,#f97316)', desc: "Signal dropping — you're walking away", badge: 'Weakening' },
OUT: { id: 'out', rssi: CONFIG.RSSI.OUT, dist: '> 15 m', bar: '6%', color: 'linear-gradient(90deg,#ef4444,#f43f5e)', desc: 'Out of range — last known position shown', badge: 'Left Behind' }
};
let state = STATES.NEAR; // current simulated state
let alertTimer = null;
let suppressedUntil = 0;
// --- UTILITIES ---
const now = () => new Date().toISOString();
function setBodyStateCls(s) {
document.body.classList.remove('state-near','state-far','state-out');
if (s === 'near') document.body.classList.add('state-near');
if (s === 'far') document.body.classList.add('state-far');
if (s === 'out') document.body.classList.add('state-out');
}
// --- UI UPDATES ---
function updateHardwareUI(s) {
// rssi & distance labels under the room
if (rssiValue) rssiValue.textContent = s.rssi;
if (distanceLabel) distanceLabel.textContent = s.dist;
// proximity badge under sticker
if (proximityBadge) {
proximityBadge.textContent = s.badge;
proximityBadge.dataset.state = s.id;
}
// sticker position via body class (CSS handles visual)
setBodyStateCls(s.id);
}
function updatePhoneUI(s) {
if (metaRssi) metaRssi.textContent = `${s.rssi} dBm`;
if (metaDistance) metaDistance.textContent = s.dist;
if (signalBar) {
signalBar.style.transition = `width ${CONFIG.TIMERS.UI_ANIM_MS}ms ${'cubic-bezier(.2,.9,.25,1)'}`;
signalBar.style.width = s.bar;
signalBar.style.background = s.color;
}
if (signalDesc) signalDesc.textContent = s.desc;
// alert banner logic
if (s.rssi <= CONFIG.RSSI.LEFT_BEHIND) {
// show with delay to avoid flicker
if (alertTimer) clearTimeout(alertTimer);
alertTimer = setTimeout(() => {
if (Date.now() < suppressedUntil) return;
if (alertBanner) alertBanner.classList.remove('hidden');
}, CONFIG.TIMERS.ALERT_DELAY_MS);
} else {
if (alertTimer) { clearTimeout(alertTimer); alertTimer = null; }
if (alertBanner) alertBanner.classList.add('hidden');
}
}
// --- Notifications (phone-scoped) ---
function pushPhoneNotification(type, title, body) {
// create list item
if (!phoneNotifList || !phoneNotifCount) return;
const li = document.createElement('div');
li.className = 'phone-notif-item';
li.innerHTML = `<div class="notif-head">${title}</div><div class="notif-body">${body}</div><div class="notif-meta">${new Date().toLocaleTimeString()}</div>`;
phoneNotifList.prepend(li);
// update count badge
const prev = parseInt(phoneNotifCount.dataset.count || '0', 10);
const next = prev + 1;
phoneNotifCount.dataset.count = String(next);
phoneNotifCount.textContent = String(next);
phoneNotifCount.classList.remove('hidden');
// transient toast inside phone
showPhoneToast(title, body);
}
function clearPhoneNotifications() {
if (!phoneNotifList || !phoneNotifCount) return;
phoneNotifList.innerHTML = '';
phoneNotifCount.dataset.count = '0';
phoneNotifCount.textContent = '0';
phoneNotifCount.classList.add('hidden');
}
function toggleNotifList() {
if (!phoneNotifList) return;
const isHidden = phoneNotifList.classList.contains('hidden');
phoneNotifList.classList.toggle('hidden');
phoneNotifList.setAttribute('aria-hidden', String(!isHidden));
// mark as seen
if (!isHidden) return;
phoneNotifCount.dataset.count = '0';
phoneNotifCount.textContent = '0';
phoneNotifCount.classList.add('hidden');
}
function showPhoneToast(title, body) {
if (!phoneScreenInner) return;
const t = document.createElement('div');
t.className = 'phone-toast';
t.innerHTML = `<strong>${title}</strong><div class="toast-body">${body}</div>`;
phoneScreenInner.appendChild(t);
// auto-remove
setTimeout(() => {
t.classList.add('fade-out');
setTimeout(() => t.remove(), 300);
}, CONFIG.UX.toastMs);
}
// --- TRANSITION HANDLER ---
function applyState(targetState) {
state = targetState;
// update UI immediately
updateHardwareUI(state);
updatePhoneUI(state);
// push user-facing notification when moving to FAR/OUT
if (state.id === 'far') {
pushPhoneNotification('info', 'Item moving away', 'The ThinTrack is getting farther from your phone.');
} else if (state.id === 'out') {
pushPhoneNotification('warning', 'Item left behind', 'ThinTrack is out of range — last known position shown.');
}
}
// --- Event bindings ---
if (btnNear) btnNear.addEventListener('click', () => applyState(STATES.NEAR));
if (btnFar) btnFar.addEventListener('click', () => applyState(STATES.FAR));
if (btnOut) btnOut.addEventListener('click', () => applyState(STATES.OUT));
// keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.key === '1') { applyState(STATES.NEAR); }
if (e.key === '2') { applyState(STATES.FAR); }
if (e.key === '3') { applyState(STATES.OUT); }
});
// phone bell
if (phoneNotifBell) phoneNotifBell.addEventListener('click', toggleNotifList);
// alert banner actions
if (dismissBtn) dismissBtn.addEventListener('click', () => {
if (alertBanner) alertBanner.classList.add('hidden');
// suppress same alert for a short duration
suppressedUntil = Date.now() + 2 * 60 * 1000; // 2 minutes
});
if (openMapBtn) openMapBtn.addEventListener('click', () => {
// simple demo behavior: highlight sticker briefly
const prev = sticker && sticker.style.transform;
if (sticker) {
sticker.classList.add('pulse');
setTimeout(() => sticker.classList.remove('pulse'), 800);
}
// also push a notification that map opened (demo)
pushPhoneNotification('action', 'Map (demo)', 'A map would open in a real app.');
});
// clear all notifications if user long-presses (demo) - optional
if (phoneNotifList) {
phoneNotifList.addEventListener('contextmenu', (ev) => {
ev.preventDefault();
clearPhoneNotifications();
});
}
// --- Initialization ---
function init() {
// initial UI render
applyState(state);
// ensure phoneNotifCount initial state
if (phoneNotifCount) {
phoneNotifCount.dataset.count = '0';
phoneNotifCount.classList.add('hidden');
}
}
// run init on DOMContentLoaded if script loaded early
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// expose small API for console (debug/demo)
window.ThinTrack = {
setNear: () => applyState(STATES.NEAR),
setFar: () => applyState(STATES.FAR),
setOut: () => applyState(STATES.OUT),
clearNotifications: clearPhoneNotifications
};
})();