watercooler 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watercooler",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "A beautiful 3D visualization of your mailbox messages as a village of coworkers",
5
5
  "type": "module",
6
6
  "main": "server.ts",
package/public/app.js CHANGED
@@ -47,6 +47,13 @@ function init() {
47
47
  controls.maxPolarAngle = Math.PI / 2 - 0.1;
48
48
  controls.minDistance = 20;
49
49
  controls.maxDistance = 80;
50
+ controls.enableZoom = true;
51
+ controls.zoomSpeed = 0.8;
52
+ controls.enablePan = false; // Disable pan on touch for better mobile UX
53
+ controls.touches = {
54
+ ONE: THREE.TOUCH.ROTATE,
55
+ TWO: THREE.TOUCH.DOLLY_PAN
56
+ };
50
57
 
51
58
  // Lighting
52
59
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
@@ -90,6 +97,18 @@ function init() {
90
97
  mouse = new THREE.Vector2();
91
98
  renderer.domElement.addEventListener('click', onHouseClick);
92
99
 
100
+ // Add touch support for mobile
101
+ renderer.domElement.addEventListener('touchstart', onHouseTouchStart, { passive: false });
102
+ renderer.domElement.addEventListener('touchend', onHouseTouchEnd, { passive: false });
103
+
104
+ // Disable context menu on mobile for better UX
105
+ renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
106
+
107
+ // Handle orientation change
108
+ window.addEventListener('orientationchange', () => {
109
+ setTimeout(onWindowResize, 100);
110
+ });
111
+
93
112
  animate();
94
113
  }
95
114
 
@@ -354,9 +373,21 @@ function animate() {
354
373
  }
355
374
 
356
375
  function onWindowResize() {
357
- camera.aspect = window.innerWidth / window.innerHeight;
376
+ const width = window.innerWidth;
377
+ const height = window.innerHeight;
378
+ camera.aspect = width / height;
358
379
  camera.updateProjectionMatrix();
359
- renderer.setSize(window.innerWidth, window.innerHeight);
380
+ renderer.setSize(width, height);
381
+
382
+ // Adjust camera position for better mobile view
383
+ if (width < 768) {
384
+ // On mobile, position camera slightly higher and further back
385
+ camera.position.y = Math.max(camera.position.y, 35);
386
+ camera.position.z = Math.max(camera.position.z, 45);
387
+ controls.minDistance = 30; // Prevent zooming too close on mobile
388
+ } else {
389
+ controls.minDistance = 20;
390
+ }
360
391
  }
361
392
 
362
393
  // API and UI Functions
@@ -370,9 +401,14 @@ async function loadData() {
370
401
  ]);
371
402
 
372
403
  config = await configRes.json();
373
- messages = await messagesRes.json();
374
- recipients = await coworkersRes.json(); // Use coworkers endpoint
375
- allMessages = await allMessagesRes.json();
404
+ const messagesData = await messagesRes.json();
405
+ const recipientsData = await coworkersRes.json();
406
+ const allMessagesData = await allMessagesRes.json();
407
+
408
+ // Validate responses are arrays (not error objects)
409
+ messages = Array.isArray(messagesData) ? messagesData : [];
410
+ recipients = Array.isArray(recipientsData) ? recipientsData : [];
411
+ allMessages = Array.isArray(allMessagesData) ? allMessagesData : [];
376
412
 
377
413
  updateUI();
378
414
  updateVillage();
@@ -391,7 +427,10 @@ window.toggleSendPanel = function() {
391
427
 
392
428
  window.toggleMessagesPanel = function() {
393
429
  const panel = document.getElementById('messages-panel');
430
+ const btn = document.getElementById('toggle-messages-btn');
394
431
  panel.classList.toggle('open');
432
+ btn.style.opacity = panel.classList.contains('open') ? '0' : '1';
433
+ btn.style.pointerEvents = panel.classList.contains('open') ? 'none' : 'auto';
395
434
  };
396
435
 
397
436
  function updateUI() {
@@ -524,9 +563,47 @@ async function sendMessage() {
524
563
 
525
564
  // House click handler
526
565
  function onHouseClick(event) {
527
- // Calculate mouse position
528
- mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
529
- mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
566
+ handleHouseInteraction(event.clientX, event.clientY);
567
+ }
568
+
569
+ // Touch handlers for mobile
570
+ let touchStartX = 0;
571
+ let touchStartY = 0;
572
+
573
+ function onHouseTouchStart(event) {
574
+ if (event.touches.length === 1) {
575
+ touchStartX = event.touches[0].clientX;
576
+ touchStartY = event.touches[0].clientY;
577
+ }
578
+ }
579
+
580
+ function onHouseTouchEnd(event) {
581
+ if (event.changedTouches.length === 1) {
582
+ const touchEndX = event.changedTouches[0].clientX;
583
+ const touchEndY = event.changedTouches[0].clientY;
584
+
585
+ // Check if touch moved significantly (if so, it's a drag/pan, not a tap)
586
+ const moveDistance = Math.sqrt(
587
+ Math.pow(touchEndX - touchStartX, 2) +
588
+ Math.pow(touchEndY - touchStartY, 2)
589
+ );
590
+
591
+ // Only trigger if touch didn't move much (tap vs swipe)
592
+ if (moveDistance < 20) {
593
+ handleHouseInteraction(touchEndX, touchEndY);
594
+ }
595
+ }
596
+ }
597
+
598
+ // Common house interaction handler
599
+ function handleHouseInteraction(clientX, clientY) {
600
+ // Calculate normalized device coordinates
601
+ const rect = renderer.domElement.getBoundingClientRect();
602
+ const x = ((clientX - rect.left) / rect.width) * 2 - 1;
603
+ const y = -((clientY - rect.top) / rect.height) * 2 + 1;
604
+
605
+ mouse.x = x;
606
+ mouse.y = y;
530
607
 
531
608
  raycaster.setFromCamera(mouse, camera);
532
609
 
package/public/index.html CHANGED
@@ -2,8 +2,8 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Watercooler Village</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
+ <title>Watercooler</title>
7
7
  <style>
8
8
  * {
9
9
  margin: 0;
@@ -320,7 +320,7 @@
320
320
  font-size: 0.9rem;
321
321
  font-weight: 600;
322
322
  cursor: pointer;
323
- z-index: 99;
323
+ z-index: 101;
324
324
  transition: all 0.3s ease;
325
325
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
326
326
  }
@@ -382,6 +382,239 @@
382
382
  border-radius: 3px;
383
383
  }
384
384
 
385
+ /* Mobile responsive styles */
386
+ @media (max-width: 768px) {
387
+ /* Send panel - full width at bottom */
388
+ .send-panel {
389
+ top: auto;
390
+ bottom: 0;
391
+ left: 0;
392
+ right: 0;
393
+ width: 100%;
394
+ max-width: 100%;
395
+ border-radius: 16px 16px 0 0;
396
+ border-bottom: none;
397
+ border-left: none;
398
+ border-right: none;
399
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
400
+ }
401
+
402
+ .send-panel.collapsed {
403
+ width: 100%;
404
+ min-width: 100%;
405
+ transform: translateY(calc(100% - 75px));
406
+ }
407
+
408
+ .send-header {
409
+ padding: 14px 20px;
410
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
411
+ }
412
+
413
+ .send-header h2 {
414
+ font-size: 1rem;
415
+ }
416
+
417
+ .collapse-btn {
418
+ font-size: 1.4rem;
419
+ padding: 8px 16px;
420
+ }
421
+
422
+ .send-form {
423
+ padding: 20px;
424
+ max-height: 50vh;
425
+ overflow-y: auto;
426
+ }
427
+
428
+ .recipient-select, .message-input, .send-btn {
429
+ font-size: 16px; /* Prevents zoom on iOS */
430
+ padding: 14px 16px;
431
+ margin-bottom: 12px;
432
+ }
433
+
434
+ .message-input {
435
+ min-height: 80px;
436
+ }
437
+
438
+ /* Messages panel - nearly full screen on mobile */
439
+ .messages-panel {
440
+ top: auto;
441
+ bottom: 0;
442
+ left: 0;
443
+ right: 0;
444
+ width: 100%;
445
+ max-width: 100%;
446
+ max-height: 90vh;
447
+ height: 90vh;
448
+ border-radius: 16px 16px 0 0;
449
+ transform: translateY(100%);
450
+ transition: transform 0.3s ease;
451
+ display: flex;
452
+ flex-direction: column;
453
+ }
454
+
455
+ .messages-panel.open {
456
+ right: 0;
457
+ transform: translateY(0);
458
+ }
459
+
460
+ .messages-header {
461
+ padding: 20px 24px;
462
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
463
+ flex-shrink: 0;
464
+ }
465
+
466
+ .messages-header h2 {
467
+ font-size: 1.2rem;
468
+ }
469
+
470
+ .close-btn {
471
+ font-size: 1.8rem;
472
+ padding: 8px;
473
+ }
474
+
475
+ .messages-container {
476
+ padding: 20px;
477
+ flex: 1;
478
+ max-height: calc(90vh - 80px);
479
+ }
480
+
481
+ .message-card {
482
+ padding: 16px 16px 16px 24px;
483
+ margin-bottom: 12px;
484
+ }
485
+
486
+ .message-sender {
487
+ font-size: 0.9rem;
488
+ }
489
+
490
+ .message-time {
491
+ font-size: 0.75rem;
492
+ }
493
+
494
+ .message-text {
495
+ font-size: 0.9rem;
496
+ }
497
+
498
+ /* Toggle messages button - repositioned */
499
+ .toggle-messages-btn {
500
+ top: 20px;
501
+ right: 20px;
502
+ padding: 14px 18px;
503
+ font-size: 1rem;
504
+ border-radius: 30px;
505
+ z-index: 101;
506
+ }
507
+
508
+ /* Toast notification - centered */
509
+ .toast {
510
+ left: 20px;
511
+ right: 20px;
512
+ bottom: auto;
513
+ top: 50%;
514
+ transform: translateY(-50%) translateY(20px);
515
+ text-align: center;
516
+ padding: 16px 24px;
517
+ font-size: 1rem;
518
+ }
519
+
520
+ .toast.show {
521
+ transform: translateY(-50%) translateY(0);
522
+ }
523
+
524
+ /* House dialog - full screen on mobile */
525
+ .house-dialog-content {
526
+ width: 100%;
527
+ max-width: 100%;
528
+ max-height: 95vh;
529
+ height: 95vh;
530
+ border-radius: 16px 16px 0 0;
531
+ margin: 0;
532
+ position: fixed;
533
+ bottom: 0;
534
+ left: 0;
535
+ right: 0;
536
+ display: flex;
537
+ flex-direction: column;
538
+ }
539
+
540
+ .house-dialog-header {
541
+ padding: 20px 24px;
542
+ flex-shrink: 0;
543
+ }
544
+
545
+ .house-dialog-header h2 {
546
+ font-size: 1.2rem;
547
+ }
548
+
549
+ .house-dialog-tabs {
550
+ padding: 0 12px;
551
+ flex-shrink: 0;
552
+ }
553
+
554
+ .tab-btn {
555
+ padding: 16px 12px;
556
+ font-size: 0.9rem;
557
+ }
558
+
559
+ .house-dialog-body {
560
+ padding: 20px;
561
+ max-height: calc(95vh - 140px);
562
+ flex: 1;
563
+ overflow-y: auto;
564
+ }
565
+
566
+ .house-dialog-body .message-card {
567
+ padding: 16px 16px 16px 24px;
568
+ }
569
+
570
+ /* Empty state */
571
+ .empty-state {
572
+ padding: 60px 20px;
573
+ }
574
+
575
+ .empty-state p {
576
+ font-size: 1rem;
577
+ }
578
+ }
579
+
580
+ /* Extra small screens */
581
+ @media (max-width: 480px) {
582
+ .send-panel.collapsed {
583
+ transform: translateY(calc(100% - 70px));
584
+ }
585
+
586
+ .send-header {
587
+ padding: 12px 16px;
588
+ }
589
+
590
+ .send-header h2 {
591
+ font-size: 0.9rem;
592
+ }
593
+
594
+ .send-form {
595
+ padding: 16px;
596
+ }
597
+
598
+ .recipient-select, .message-input, .send-btn {
599
+ font-size: 16px;
600
+ padding: 12px 14px;
601
+ }
602
+
603
+ .toggle-messages-btn {
604
+ padding: 12px 16px;
605
+ font-size: 0.9rem;
606
+ }
607
+
608
+ .messages-panel {
609
+ max-height: 75vh;
610
+ }
611
+
612
+ .tab-btn {
613
+ font-size: 0.8rem;
614
+ padding: 12px 8px;
615
+ }
616
+ }
617
+
385
618
  /* House Dialog */
386
619
  .house-dialog {
387
620
  display: none;
@@ -392,7 +625,7 @@
392
625
  bottom: 0;
393
626
  background: rgba(0, 0, 0, 0.7);
394
627
  z-index: 300;
395
- align-items: center;
628
+ align-items: flex-end;
396
629
  justify-content: center;
397
630
  }
398
631
 
@@ -400,15 +633,43 @@
400
633
  display: flex;
401
634
  }
402
635
 
636
+ /* Desktop override for house dialog */
637
+ @media (min-width: 769px) {
638
+ .house-dialog {
639
+ align-items: center;
640
+ }
641
+
642
+ .house-dialog-content {
643
+ position: relative;
644
+ bottom: auto;
645
+ left: auto;
646
+ right: auto;
647
+ border-radius: 20px;
648
+ }
649
+ }
650
+
651
+ /* Desktop house dialog styles */
652
+ @media (min-width: 769px) {
653
+ .house-dialog-content {
654
+ background: rgba(255, 255, 255, 0.15);
655
+ backdrop-filter: blur(20px);
656
+ border-radius: 20px;
657
+ border: 1px solid rgba(255, 255, 255, 0.2);
658
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
659
+ width: 500px;
660
+ max-width: 90%;
661
+ max-height: 80vh;
662
+ display: flex;
663
+ flex-direction: column;
664
+ }
665
+ }
666
+
667
+ /* Base house dialog styles (shared) */
403
668
  .house-dialog-content {
404
669
  background: rgba(255, 255, 255, 0.15);
405
670
  backdrop-filter: blur(20px);
406
- border-radius: 20px;
407
671
  border: 1px solid rgba(255, 255, 255, 0.2);
408
672
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
409
- width: 500px;
410
- max-width: 90%;
411
- max-height: 80vh;
412
673
  display: flex;
413
674
  flex-direction: column;
414
675
  }
package/server.ts CHANGED
@@ -7,13 +7,14 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
 
9
9
  const app = express();
10
- const PORT = process.env.PORT || 3000;
11
10
 
12
11
  // Parse CLI args
13
12
  const args = process.argv.slice(2);
14
13
  let user: string | null = null;
15
14
  let mailboxPath: string | null = null;
16
15
  let coworkerPath: string | null = null;
16
+ let port: number = parseInt(process.env.PORT || '3000', 10);
17
+ let host: string = process.env.HOST || '0.0.0.0';
17
18
 
18
19
  for (let i = 0; i < args.length; i++) {
19
20
  if (args[i] === '--user' || args[i] === '-u') {
@@ -22,11 +23,16 @@ for (let i = 0; i < args.length; i++) {
22
23
  mailboxPath = args[++i];
23
24
  } else if (args[i] === '--coworkers' || args[i] === '-c') {
24
25
  coworkerPath = args[++i];
26
+ } else if (args[i] === '--port' || args[i] === '-p') {
27
+ const p = parseInt(args[++i], 10);
28
+ if (!isNaN(p)) port = p;
29
+ } else if (args[i] === '--host' || args[i] === '-h') {
30
+ host = args[++i];
25
31
  }
26
32
  }
27
33
 
28
34
  if (!user || !mailboxPath) {
29
- console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>]');
35
+ console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--port <number>] [--host <address>]');
30
36
  process.exit(1);
31
37
  }
32
38
 
@@ -35,7 +41,7 @@ console.log(` Mailbox: ${mailboxPath}`);
35
41
  if (coworkerPath) {
36
42
  console.log(` Coworker DB: ${coworkerPath}`);
37
43
  }
38
- console.log(` URL: http://localhost:${PORT}`);
44
+ console.log(` URL: http://${host}:${port}`);
39
45
 
40
46
  // Databases
41
47
  let db: Database.Database | null = null;
@@ -58,6 +64,20 @@ if (coworkerPath) {
58
64
  }
59
65
  }
60
66
 
67
+ // Helper: Check if table exists
68
+ function tableExists(database: Database.Database | null, tableName: string): boolean {
69
+ if (!database) return false;
70
+ try {
71
+ const stmt = database.prepare(`
72
+ SELECT name FROM sqlite_master
73
+ WHERE type='table' AND name=?
74
+ `);
75
+ return !!stmt.get(tableName);
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
61
81
  // Middleware
62
82
  app.use(express.json());
63
83
  app.use(express.static(path.join(__dirname, 'public')));
@@ -66,6 +86,10 @@ app.use(express.static(path.join(__dirname, 'public')));
66
86
  app.get('/api/messages', (req, res) => {
67
87
  try {
68
88
  if (!db) throw new Error('Database not connected');
89
+ if (!tableExists(db, 'messages')) {
90
+ res.json([]);
91
+ return;
92
+ }
69
93
  const stmt = db.prepare(`
70
94
  SELECT * FROM messages
71
95
  WHERE recipient = ?
@@ -81,6 +105,10 @@ app.get('/api/messages', (req, res) => {
81
105
  app.get('/api/messages/sent', (req, res) => {
82
106
  try {
83
107
  if (!db) throw new Error('Database not connected');
108
+ if (!tableExists(db, 'messages')) {
109
+ res.json([]);
110
+ return;
111
+ }
84
112
  const stmt = db.prepare(`
85
113
  SELECT * FROM messages
86
114
  WHERE sender = ?
@@ -96,6 +124,10 @@ app.get('/api/messages/sent', (req, res) => {
96
124
  app.get('/api/messages/all', (req, res) => {
97
125
  try {
98
126
  if (!db) throw new Error('Database not connected');
127
+ if (!tableExists(db, 'messages')) {
128
+ res.json([]);
129
+ return;
130
+ }
99
131
  const stmt = db.prepare(`
100
132
  SELECT * FROM messages
101
133
  ORDER BY timestamp DESC
@@ -125,12 +157,7 @@ app.get('/api/coworkers', (req, res) => {
125
157
  console.log('No coworkerDb connection available');
126
158
  }
127
159
 
128
- // Add from message history
129
- const recipientRows = db.prepare('SELECT DISTINCT recipient FROM messages').all() as Array<{recipient: string}>;
130
- recipientRows.forEach(row => allCoworkers.add(row.recipient.toLowerCase()));
131
-
132
- const senderRows = db.prepare('SELECT DISTINCT sender FROM messages').all() as Array<{sender: string}>;
133
- senderRows.forEach(row => allCoworkers.add(row.sender.toLowerCase()));
160
+ // Note: Messages table is in a different database, not queried here
134
161
 
135
162
  // Remove current user
136
163
  allCoworkers.delete(user!.toLowerCase());
@@ -147,6 +174,10 @@ app.get('/api/coworkers', (req, res) => {
147
174
  app.get('/api/recipients', (req, res) => {
148
175
  try {
149
176
  if (!db) throw new Error('Database not connected');
177
+ if (!tableExists(db, 'messages')) {
178
+ res.json([]);
179
+ return;
180
+ }
150
181
  const stmt = db.prepare(`SELECT DISTINCT recipient FROM messages`);
151
182
  res.json(stmt.all().map((r: any) => r.recipient));
152
183
  } catch (err: any) {
@@ -158,6 +189,21 @@ app.get('/api/recipients', (req, res) => {
158
189
  app.post('/api/send', (req, res) => {
159
190
  try {
160
191
  if (!db) throw new Error('Database not connected');
192
+
193
+ // Auto-create messages table if it doesn't exist
194
+ if (!tableExists(db, 'messages')) {
195
+ db.exec(`
196
+ CREATE TABLE messages (
197
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
198
+ recipient TEXT NOT NULL,
199
+ sender TEXT NOT NULL,
200
+ message TEXT NOT NULL,
201
+ timestamp INTEGER NOT NULL,
202
+ read INTEGER DEFAULT 0
203
+ )
204
+ `);
205
+ }
206
+
161
207
  const { to, message } = req.body;
162
208
  const stmt = db.prepare(`
163
209
  INSERT INTO messages (recipient, sender, message, timestamp, read)
@@ -174,6 +220,10 @@ app.post('/api/send', (req, res) => {
174
220
  app.post('/api/messages/:id/read', (req, res) => {
175
221
  try {
176
222
  if (!db) throw new Error('Database not connected');
223
+ if (!tableExists(db, 'messages')) {
224
+ res.status(404).json({ error: 'Messages table not found' });
225
+ return;
226
+ }
177
227
  db.prepare('UPDATE messages SET read = 1 WHERE id = ?').run(req.params.id);
178
228
  res.json({ success: true });
179
229
  } catch (err: any) {
@@ -186,6 +236,6 @@ app.get('/api/config', (req, res) => {
186
236
  res.json({ user, mailbox: mailboxPath, coworker: coworkerPath });
187
237
  });
188
238
 
189
- app.listen(PORT, () => {
239
+ app.listen(port, host, () => {
190
240
  console.log('\n✅ Watercooler running!');
191
241
  });